From 5b98313bcc48cb17486932fcc870bc43e58f5096 Mon Sep 17 00:00:00 2001 From: Abel Soares Siqueira Date: Tue, 28 Oct 2025 16:57:39 +0100 Subject: [PATCH 1/2] Delay computation of cost using commodity_price out of t_objective_flows Part of #1269 --- src/objective.jl | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/objective.jl b/src/objective.jl index 4de2f3866..65e869d17 100644 --- a/src/objective.jl +++ b/src/objective.jl @@ -231,7 +231,7 @@ function add_objective!(connection, model, variables, expressions, model_paramet * rp_weight.weight_sum * rp_res.resolution * (var.time_block_end - var.time_block_start + 1) - * obj.total_variable_cost + * (obj.commodity_price / obj.efficiency + obj.operational_cost) AS cost, FROM var_flow AS var LEFT JOIN t_objective_flows as obj @@ -529,8 +529,6 @@ function _create_objective_auxiliary_table(connection, constants) asset_commission.efficiency, flow_milestone.operational_cost, -- computed - (asset_milestone.commodity_price / asset_commission.efficiency) AS fuel_cost, - (fuel_cost + flow_milestone.operational_cost) AS total_variable_cost, CASE -- the below closed-form equation does not accept 0 in the denominator when flow.discount_rate = 0 WHEN flow.discount_rate = 0 From 0dc983f83bf39d18bba8deb4440ee02d9173f28c Mon Sep 17 00:00:00 2001 From: Abel Soares Siqueira Date: Tue, 28 Oct 2025 20:12:53 +0100 Subject: [PATCH 2/2] Allow commodity_price to be a profile Part of #1269 --- src/create-model.jl | 1 + src/input-schemas.json | 3 +- src/objective.jl | 37 +++++++++++++++++++--- test/test-objective-profiles.jl | 54 +++++++++++++++++++++++++++++++++ 4 files changed, 89 insertions(+), 6 deletions(-) create mode 100644 test/test-objective-profiles.jl diff --git a/src/create-model.jl b/src/create-model.jl index 3a4f62a4c..0781eda3f 100644 --- a/src/create-model.jl +++ b/src/create-model.jl @@ -154,6 +154,7 @@ function create_model( model, variables, expressions, + profiles, model_parameters, ) diff --git a/src/input-schemas.json b/src/input-schemas.json index 1b02ba1e4..abad48e46 100644 --- a/src/input-schemas.json +++ b/src/input-schemas.json @@ -686,7 +686,8 @@ "profile_type": { "constraints": { "oneOf": [ - "availability" + "availability", + "commodity_price" ] }, "description": "Type of profile, used to determine DuckDB table with source profile", diff --git a/src/objective.jl b/src/objective.jl index 65e869d17..e528cd639 100644 --- a/src/objective.jl +++ b/src/objective.jl @@ -1,4 +1,4 @@ -function add_objective!(connection, model, variables, expressions, model_parameters) +function add_objective!(connection, model, variables, expressions, profiles, model_parameters) assets_investment = variables[:assets_investment] assets_investment_energy = variables[:assets_investment_energy] flows_investment = variables[:flows_investment] @@ -230,9 +230,15 @@ function add_objective!(connection, model, variables, expressions, model_paramet obj.weight_for_operation_discounts * rp_weight.weight_sum * rp_res.resolution - * (var.time_block_end - var.time_block_start + 1) - * (obj.commodity_price / obj.efficiency + obj.operational_cost) - AS cost, + AS cost_coefficient, + var.time_block_start, + var.time_block_end, + var.year, + var.rep_period, + obj.commodity_price, + obj.efficiency, + obj.operational_cost, + commodity_price_profiles.profile_name, FROM var_flow AS var LEFT JOIN t_objective_flows as obj ON var.from_asset = obj.from_asset @@ -246,6 +252,11 @@ function add_objective!(connection, model, variables, expressions, model_paramet AND var.rep_period = rp_res.rep_period LEFT JOIN asset ON asset.asset = var.from_asset + LEFT JOIN flows_profiles AS commodity_price_profiles + ON commodity_price_profiles.from_asset = var.from_asset + AND commodity_price_profiles.to_asset = var.to_asset + AND commodity_price_profiles.year = var.year + AND commodity_price_profiles.profile_type = 'commodity_price' -- TODO: inconsistent year naming to assets_profiles WHERE asset.investment_method != 'semi-compact' ", ) @@ -255,7 +266,23 @@ function add_objective!(connection, model, variables, expressions, model_paramet # i.e., we only consider the costs of the flows that are not in semi-compact method var_flow = variables[:flow].container - flows_operational_cost = @expression(model, sum(row.cost * var_flow[row.id] for row in indices)) + flows_operational_cost = @expression( + model, + sum( + row.cost_coefficient * + ( + row.commodity_price * _profile_aggregate( # commodity_price aggregation + profiles.rep_period, + (row.profile_name, row.year, row.rep_period), + row.time_block_start:row.time_block_end, + sum, + 1.0, + ) / row.efficiency + + row.operational_cost * (row.time_block_end - row.time_block_start + 1) + ) * + var_flow[row.id] for row in indices + ) + ) indices = DuckDB.query( connection, diff --git a/test/test-objective-profiles.jl b/test/test-objective-profiles.jl new file mode 100644 index 000000000..a7f5809bf --- /dev/null +++ b/test/test-objective-profiles.jl @@ -0,0 +1,54 @@ +@testitem "Commodity price is used correctly" setup = [CommonSetup] tags = [:case_study, :slow] begin + dir = joinpath(INPUT_FOLDER, "MIMO") + connection = DBInterface.connect(DuckDB.DB) + _read_csv_folder(connection, dir) + + TulipaEnergyModel.populate_with_defaults!(connection) + + # Copied over from test-case-studies.jl + energy_problem = TulipaEnergyModel.run_scenario(connection; show_log = false) + @test energy_problem.objective_value ≈ 89360.638146 atol = 1e-5 + + # Changing commodity_price to make sure it makes a difference + DuckDB.query( + connection, + """ + UPDATE asset_milestone + SET commodity_price = 10.0 + WHERE asset = 'biomass' + """, + ) + energy_problem = TulipaEnergyModel.run_scenario(connection; show_log = false) + @test energy_problem.objective_value ≈ 89971.40505 atol = 1e-5 + + # Testing commodity_price profile + # We duplicate the biomass_profile replacing the value + DuckDB.query( + connection, + """ + WITH cte_one_profile AS ( + SELECT + profiles_rep_periods.* EXCLUDE (profile_name, value) + FROM profiles_rep_periods + WHERE profile_name = 'biomass_profile' + ) + INSERT INTO profiles_rep_periods BY NAME + SELECT + 'commodity_price' AS profile_name, + 0.1 AS value, + cte_one_profile.*, + FROM cte_one_profile + """, + ) + # Now we assign it to a flow + DuckDB.query( + connection, + """ + INSERT INTO flows_profiles (from_asset, to_asset, year, profile_type, profile_name) + VALUES ('biomass', 'power_plant', 2030, 'commodity_price', 'commodity_price'); + """, + ) + + energy_problem = TulipaEnergyModel.run_scenario(connection; show_log = false) + @test energy_problem.objective_value ≈ 89421.71484 atol = 1e-5 +end