From e63dfb6c58fce60c35096ca7d530e409da3efd23 Mon Sep 17 00:00:00 2001 From: FRICKO Oliver Date: Fri, 30 Jun 2023 08:56:13 +0200 Subject: [PATCH 01/26] Adjust calculation of PRICE_EMISSION --- message_ix/model/MESSAGE/model_core.gms | 20 +++++++++++--------- message_ix/model/MESSAGE/model_solve.gms | 14 +++++++------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/message_ix/model/MESSAGE/model_core.gms b/message_ix/model/MESSAGE/model_core.gms index 8fcb836e6..b1c4b6c7b 100644 --- a/message_ix/model/MESSAGE/model_core.gms +++ b/message_ix/model/MESSAGE/model_core.gms @@ -131,15 +131,16 @@ Variables * * Auxiliary variables * ^^^^^^^^^^^^^^^^^^^ -* =========================================================================== ====================================================================================================== -* Variable Explanatory text -* =========================================================================== ====================================================================================================== -* :math:`\text{DEMAND}_{n,c,l,y,h} \in \mathbb{R}` Demand level (in equilibrium with MACRO integration) -* :math:`\text{PRICE_COMMODITY}_{n,c,l,y,h} \in \mathbb{R}` Commodity price (undiscounted marginals of :ref:`commodity_balance_gt` and :ref:`commodity_balance_lt`) -* :math:`\text{PRICE_EMISSION}_{n,\widehat{e},\widehat{t},y} \in \mathbb{R}` Emission price (undiscounted marginals of :ref:`emission_constraint`) -* :math:`\text{COST_NODAL_NET}_{n,y} \in \mathbb{R}` System costs at the node level net of energy trade revenues/cost -* :math:`\text{GDP}_{n,y} \in \mathbb{R}` Gross domestic product (GDP) in market exchange rates for MACRO reporting -* =========================================================================== ====================================================================================================== +* ======================================================================================= ======================================================================================================= +* Variable Explanatory text +* ======================================================================================= ======================================================================================================= +* :math:`\text{DEMAND}_{n,c,l,y,h} \in \mathbb{R}` Demand level (in equilibrium with MACRO integration) +* :math:`\text{EMISSION_CONSTRAINT_RESCALE}_{n,\widehat{e},\widehat{t},y} \in \mathbb{R}` Annual average marginal of :ref:`emission_constraint` +* :math:`\text{PRICE_COMMODITY}_{n,c,l,y,h} \in \mathbb{R}` Commodity price (undiscounted marginals of :ref:`commodity_balance_gt` and :ref:`commodity_balance_lt`) +* :math:`\text{PRICE_EMISSION}_{n,\widehat{e},\widehat{t},y} \in \mathbb{R}` Emission price (undiscounted marginals of :ref:`emission_constraint`) +* :math:`\text{COST_NODAL_NET}_{n,y} \in \mathbb{R}` System costs at the node level net of energy trade revenues/cost +* :math:`\text{GDP}_{n,y} \in \mathbb{R}` Gross domestic product (GDP) in market exchange rates for MACRO reporting +* ======================================================================================= ======================================================================================================= * * .. warning:: * Please be aware that transitioning from one period length to another for consecutive periods may result in false values of :math:`\text{PRICE_EMISSION}`. @@ -149,6 +150,7 @@ Variables Variables * auxiliary variables for demand, prices, costs and GDP (for reporting when MESSAGE is run with MACRO) DEMAND(node,commodity,level,year_all,time) demand + EMISSION_CONSTRAINT_RESCALE(node,type_emission,type_tec,type_year) rescaled of marginals of EMISSION_CONSTRAINT constraint PRICE_COMMODITY(node,commodity,level,year_all,time) commodity price (derived from marginals of COMMODITY_BALANCE constraint) PRICE_EMISSION(node,type_emission,type_tec,year_all) emission price (derived from marginals of EMISSION_BOUND constraint) COST_NODAL_NET(node,year_all) system costs at the node level over time including effects of energy trade diff --git a/message_ix/model/MESSAGE/model_solve.gms b/message_ix/model/MESSAGE/model_solve.gms index 6fda3e23f..44455cda8 100644 --- a/message_ix/model/MESSAGE/model_solve.gms +++ b/message_ix/model/MESSAGE/model_solve.gms @@ -14,7 +14,7 @@ if (%foresight% = 0, * This is the standard option; the GAMS global variable ``%foresight%=0`` by default. * * .. math:: -* \min_x \text{OBJ} = \sum_{y \in Y} \text{OBJ}_y(x_y) +* \min_x OBJ = \sum_{y \in Y} OBJ_y(x_y) *** * reset year in case it was set by MACRO to include the base year before @@ -40,11 +40,11 @@ if (%foresight% = 0, ) ; * rescale the dual of the emission constraint to account that the constraint is defined on the average year, not total -EMISSION_CONSTRAINT.m(node,type_emission,type_tec,type_year)$( +EMISSION_CONSTRAINT_RESCALE.l(node,type_emission,type_tec,type_year)$( EMISSION_CONSTRAINT.m(node,type_emission,type_tec,type_year) ) = EMISSION_CONSTRAINT.m(node,type_emission,type_tec,type_year) / SUM(year$( cat_year(type_year,year) ), duration_period(year) ) - * SUM(year$( map_first_period(type_year,year) ), duration_period(year) / df_period(year) * df_year(year) ); +; * assign auxiliary variables DEMAND, PRICE_COMMODITY and PRICE_EMISSION for integration with MACRO and reporting @@ -54,8 +54,8 @@ EMISSION_CONSTRAINT.m(node,type_emission,type_tec,type_year)$( / df_period(year) ; PRICE_EMISSION.l(node,type_emission,type_tec,year)$( SUM(type_year$( cat_year(type_year,year) ), 1 ) ) = SMAX(type_year$( cat_year(type_year,year) ), - - EMISSION_CONSTRAINT.m(node,type_emission,type_tec,type_year) ) - / df_year(year) ; + - EMISSION_CONSTRAINT_RESCALE.l(node,type_emission,type_tec,type_year) ) + / df_period(year) * duration_period(year); PRICE_EMISSION.l(node,type_emission,type_tec,year)$( PRICE_EMISSION.l(node,type_emission,type_tec,year) = - inf ) = 0 ; @@ -77,10 +77,10 @@ else * Loop over :math:`\hat{y} \in Y`, solving * * .. math:: -* \min_x \ \text{OBJ} = \sum_{y \in \hat{Y}(\hat{y})} \text{OBJ}_y(x_y) \\ +* \min_x \ OBJ = \sum_{y \in \hat{Y}(\hat{y})} OBJ_y(x_y) \\ * \text{s.t. } x_{y'} = x_{y'}^* \quad \forall \ y' < y * -* where :math:`\hat{Y}(\hat{y}) = \{y \in Y | \ |\hat{y}| - |y| < \text{optimization_horizon} \}` and +* where :math:`\hat{Y}(\hat{y}) = \{y \in Y | \ |\hat{y}| - |y| < optimization\_horizon \}` and * :math:`x_{y'}^*` is the optimal value of :math:`x_{y'}` in iteration :math:`|y'|` of the iterative loop. * * The advantage of this implementation is that there is no need to 'store' the optimal values of all decision From ab05cd5a8d255dbe0c949ae0aaeba124603bfda0 Mon Sep 17 00:00:00 2001 From: Ben Date: Fri, 30 Jun 2023 12:16:27 +0200 Subject: [PATCH 02/26] Add temporary price emission variable in model_core --- message_ix/model/MESSAGE/model_core.gms | 1 + 1 file changed, 1 insertion(+) diff --git a/message_ix/model/MESSAGE/model_core.gms b/message_ix/model/MESSAGE/model_core.gms index b1c4b6c7b..858f33fd1 100644 --- a/message_ix/model/MESSAGE/model_core.gms +++ b/message_ix/model/MESSAGE/model_core.gms @@ -153,6 +153,7 @@ Variables EMISSION_CONSTRAINT_RESCALE(node,type_emission,type_tec,type_year) rescaled of marginals of EMISSION_CONSTRAINT constraint PRICE_COMMODITY(node,commodity,level,year_all,time) commodity price (derived from marginals of COMMODITY_BALANCE constraint) PRICE_EMISSION(node,type_emission,type_tec,year_all) emission price (derived from marginals of EMISSION_BOUND constraint) + PRICE_EMISSION_NEW(node,type_emission,type_tec,year_all) emission price (derived from marginals of EMISSION_EQUIVALENCE constraint) COST_NODAL_NET(node,year_all) system costs at the node level over time including effects of energy trade GDP(node,year_all) gross domestic product (GDP) in market exchange rates for MACRO reporting ; From 1e9c1b4079c73808614f1f47732f512938c2843f Mon Sep 17 00:00:00 2001 From: Ben Date: Fri, 30 Jun 2023 12:17:05 +0200 Subject: [PATCH 03/26] Refactor price emission to marginals of EMISSION_EQUIVALENCE --- message_ix/model/MESSAGE/model_solve.gms | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/message_ix/model/MESSAGE/model_solve.gms b/message_ix/model/MESSAGE/model_solve.gms index 44455cda8..f0f9bf13b 100644 --- a/message_ix/model/MESSAGE/model_solve.gms +++ b/message_ix/model/MESSAGE/model_solve.gms @@ -58,6 +58,15 @@ EMISSION_CONSTRAINT_RESCALE.l(node,type_emission,type_tec,type_year)$( / df_period(year) * duration_period(year); PRICE_EMISSION.l(node,type_emission,type_tec,year)$( PRICE_EMISSION.l(node,type_emission,type_tec,year) = - inf ) = 0 ; +* Calculate PRICE_EMISSION based on the marginals of EMISSION_EQUIVALENCE + PRICE_EMISSION_NEW.l(node,type_emission,type_tec,year)$( SUM(emission$( cat_emission(type_emission,emission) ), + EMISSION_EQUIVALENCE.m(node,emission,type_tec,year) ) ) = + SMAX(emission$( cat_emission(type_emission,emission) ), + EMISSION_EQUIVALENCE.m(node,emission,type_tec,year) / emission_scaling(type_emission,emission) ) + / df_period(year); + PRICE_EMISSION_NEW.l(node,type_emission,type_tec,year)$( + ( PRICE_EMISSION_NEW.l(node,type_emission,type_tec,year) = eps ) or + ( PRICE_EMISSION_NEW.l(node,type_emission,type_tec,year) = -inf ) ) = 0 ; %AUX_BOUNDS% AUX_ACT_BOUND_LO(node,tec,year_all,year_all2,mode,time)$( ACT.l(node,tec,year_all,year_all2,mode,time) < 0 AND %AUX_BOUNDS% ACT.l(node,tec,year_all,year_all2,mode,time) = -%AUX_BOUND_VALUE% ) = yes ; From 90ec84030353ca580929ed77af4fa016d4e5c983 Mon Sep 17 00:00:00 2001 From: FRICKO Oliver Date: Fri, 30 Jun 2023 14:33:23 +0200 Subject: [PATCH 04/26] Adjust existing test to comply with changes to PRICE_EMISSION --- .../tests/test_feature_price_emission.py | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/message_ix/tests/test_feature_price_emission.py b/message_ix/tests/test_feature_price_emission.py index b790965dc..8eed298cd 100644 --- a/message_ix/tests/test_feature_price_emission.py +++ b/message_ix/tests/test_feature_price_emission.py @@ -4,6 +4,11 @@ MODEL = "test_emissions_price" +solve_args = { + "equ_list": ["EMISSION_EQUIVALENCE"], + "par_list": ["df_period", "df_year", "levelized_cost"], +} + def model_setup(scen, years, simple_tecs=True): """generate a minimal model to test the behaviour of the emission prices""" @@ -160,14 +165,22 @@ def test_cumulative_variable_periodlength(test_mp, request): scen.add_cat("year", "cumulative", years) scen.add_par("bound_emission", ["World", "ghg", "all", "cumulative"], 0, "tCO2") scen.commit("initialize test scenario") - scen.solve(quiet=True) + scen.solve(quiet=True, **solve_args) # with an emissions constraint, the technology with costs satisfies demand assert scen.var("OBJ")["lvl"] > 0 # under a cumulative constraint, the price must increase with the discount # rate starting from the marginal relaxation in the first year obs = scen.var("PRICE_EMISSION")["lvl"].values - npt.assert_allclose(obs, [1.05 ** (y - years[0]) for y in years]) + + # Retrieve `EMISSION_EQUIVALENCE` and divide by `df_period` + emi_equ = scen.equ("EMISSION_EQUIVALENCE", {"node": "World"}).mrg.tolist() + # Excluded until parameter can be loaded directly from scenario-object. + # df_period = scen.par("df_period").value.tolist() + df_period = [5.52563125, 4.329476671, 3.392258259, 4.740475413] + exp = [i / j for i, j in zip(emi_equ, df_period)] + + npt.assert_allclose(obs, exp) def test_per_period_variable_periodlength(test_mp, request): @@ -198,14 +211,24 @@ def test_custom_type_variable_periodlength(test_mp, request): scen.add_par("bound_emission", ["World", "ghg", "all", "custom"], 0, "tCO2") scen.commit("initialize test scenario") - scen.solve(quiet=True) + scen.solve(quiet=True, **solve_args) # with an emissions constraint, the technology with costs satisfies demand assert scen.var("OBJ")["lvl"] > 0 # under a cumulative constraint, the price must increase with the discount # rate starting from the marginal relaxation in the first year obs = scen.var("PRICE_EMISSION")["lvl"].values - npt.assert_allclose(obs, [1.05 ** (y - custom[0]) for y in custom]) + + # Retrieve `EMISSION_EQUIVALENCE` and divide by `df_period` + emi_equ = scen.equ( + "EMISSION_EQUIVALENCE", {"node": "World", "year": custom} + ).mrg.tolist() + # Excluded until parameter can be loaded directly from scenario-object. + # df_period = scen.par("df_period").value.tolist() + df_period = [4.329476671, 3.392258259, 4.740475413] + exp = [i / j for i, j in zip(emi_equ, df_period)] + + npt.assert_allclose(obs, exp) def test_price_duality(test_mp, request): From e9b1dacfd1cd957a8a4476c9a4c9ab4ff9bc6f5a Mon Sep 17 00:00:00 2001 From: FRICKO Oliver Date: Mon, 3 Jul 2023 15:27:33 +0200 Subject: [PATCH 05/26] Extend price_emission tests --- .../tests/test_feature_price_emission.py | 93 +++++++++++++------ 1 file changed, 67 insertions(+), 26 deletions(-) diff --git a/message_ix/tests/test_feature_price_emission.py b/message_ix/tests/test_feature_price_emission.py index 8eed298cd..7403e5af8 100644 --- a/message_ix/tests/test_feature_price_emission.py +++ b/message_ix/tests/test_feature_price_emission.py @@ -1,4 +1,5 @@ import numpy.testing as npt +import pytest from message_ix import Scenario, make_df @@ -7,6 +8,7 @@ solve_args = { "equ_list": ["EMISSION_EQUIVALENCE"], "par_list": ["df_period", "df_year", "levelized_cost"], + "var_list": ["PRICE_EMISSION_NEW"], } @@ -95,7 +97,7 @@ def add_many_tecs(scen, years, n=50): """add a range of dirty-to-clean technologies to the scenario""" output_specs = ["node", "comm", "level", "year", "year"] - for i in range(n + 1): + for i in range(1, n + 1): t = "tec{}".format(i) scen.add_set("technology", t) for y in years: @@ -231,13 +233,12 @@ def test_custom_type_variable_periodlength(test_mp, request): npt.assert_allclose(obs, exp) -def test_price_duality(test_mp, request): +@pytest.mark.parametrize("cumulative_bound", [0.25, 0.5, 0.75]) +def test_price_duality(test_mp, request, cumulative_bound): years = [2020, 2025, 2030, 2040, 2050] for c in [0.25, 0.5, 0.75]: # set up a scenario for cumulative constraints - scen = Scenario( - test_mp, MODEL, scenario=request.node.name + "_cum_many_tecs", version="new" - ) + scen = Scenario(test_mp, MODEL, "cum_many_tecs", version="new") model_setup(scen, years, simple_tecs=False) scen.add_cat("year", "cumulative", years) scen.add_par( @@ -246,25 +247,65 @@ def test_price_duality(test_mp, request): scen.commit("initialize test scenario") scen.solve(quiet=True) - # set up a new scenario with emissions taxes - tax_scen = Scenario( - test_mp, MODEL, scenario=request.node.name + "_tax_many_tecs", version="new" - ) - model_setup(tax_scen, years, simple_tecs=False) - for y in years: - tax_scen.add_cat("year", y, y) + # ---------------------------------------------------------- + # Run scenario with `tax_emission` based on `PRICE_EMISSION` + # from cumulative constraint scenario. + # ---------------------------------------------------------- - # use emission prices from cumulative-constraint scenario as taxes - taxes = scen.var("PRICE_EMISSION").rename( - columns={"year": "type_year", "lvl": "value"} - ) - taxes["unit"] = "USD/tCO2" - tax_scen.add_par("tax_emission", taxes) - tax_scen.commit("initialize test scenario for taxes") - tax_scen.solve(quiet=True) - - # check that emissions are close between cumulative and tax scenario - filters = {"node": "World"} - emiss = scen.var("EMISS", filters).set_index("year").lvl - emiss_tax = tax_scen.var("EMISS", filters).set_index("year").lvl - npt.assert_allclose(emiss, emiss_tax, rtol=0.20) + tax_scen = Scenario( + test_mp, MODEL, scenario=request.node.name + "_tax_many_tecs", version="new" + ) + model_setup(tax_scen, years, simple_tecs=False) + for y in years: + tax_scen.add_cat("year", y, y) + + # use emission prices from cumulative-constraint scenario as taxes + taxes = scen.var("PRICE_EMISSION").rename( + columns={"year": "type_year", "lvl": "value"} + ) + taxes["unit"] = "USD/tCO2" + tax_scen.add_par("tax_emission", taxes) + tax_scen.commit("initialize test scenario for taxes") + tax_scen.solve(quiet=True, **solve_args) + + # check that emissions are close between cumulative and tax scenario + filters = {"node": "World"} + emiss = scen.var("EMISS", filters).set_index("year").lvl + emiss_tax = tax_scen.var("EMISS", filters).set_index("year").lvl + npt.assert_allclose(emiss, emiss_tax) + + # check that "PRICE_EMISSION" are close between cumulative and tax scenario + filters = {"node": "World"} + pemiss = scen.var("PRICE_EMISSION_NEW", filters).set_index("year").lvl + pemiss_tax = tax_scen.var("PRICE_EMISSION_NEW", filters).set_index("year").lvl + npt.assert_allclose(pemiss, pemiss_tax) + + # -------------------------------------------------------- + # Run scenario with annual-emission bound based on `EMISS` + # from cumulative constraint scenario. + # -------------------------------------------------------- + + perbnd_scen = Scenario(test_mp, MODEL, "period-bnd_many_tecs", version="new") + model_setup(perbnd_scen, years, simple_tecs=False) + for y in years: + perbnd_scen.add_cat("year", y, y) + + # use emission prices from cumulative-constraint scenario as taxes + bnd_emiss = ( + scen.var("EMISS", {"node": "World"}) + .rename(columns={"year": "type_year", "lvl": "value"}) + .drop("emission", axis=1) + ) + bnd_emiss["type_emission"] = "ghg" + bnd_emiss["unit"] = "tCO2" + perbnd_scen.add_par("bound_emission", bnd_emiss) + perbnd_scen.commit("initialize test scenario for periodic emission bound") + perbnd_scen.solve(quiet=True, **solve_args) + + # check that emissions are close between cumulative and tax scenario + emiss_bnd = perbnd_scen.var("EMISS", filters).set_index("year").lvl + npt.assert_allclose(emiss, emiss_bnd) + + # check that "PRICE_EMISSION" are close between cumulative and tax scenario + pemiss_bnd = perbnd_scen.var("PRICE_EMISSION_NEW", filters).set_index("year").lvl + npt.assert_allclose(pemiss, pemiss_bnd) From 5ae58a9a4087e948d5fc9c5b8fa2bc7fdfe944d4 Mon Sep 17 00:00:00 2001 From: FRICKO Oliver Date: Wed, 5 Jul 2023 14:10:50 +0200 Subject: [PATCH 06/26] Trial setup for price emission test --- .../tests/test_feature_price_emission.py | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/message_ix/tests/test_feature_price_emission.py b/message_ix/tests/test_feature_price_emission.py index 7403e5af8..0ca9e9684 100644 --- a/message_ix/tests/test_feature_price_emission.py +++ b/message_ix/tests/test_feature_price_emission.py @@ -1,4 +1,5 @@ import numpy.testing as npt +import pandas as pd import pytest from message_ix import Scenario, make_df @@ -97,8 +98,9 @@ def add_many_tecs(scen, years, n=50): """add a range of dirty-to-clean technologies to the scenario""" output_specs = ["node", "comm", "level", "year", "year"] + # tec: [emissions, var_cost, bound_activity_up] for i in range(1, n + 1): - t = "tec{}".format(i) + t = f"tec{i}" scen.add_set("technology", t) for y in years: tec_specs = ["node", t, y, y, "mode"] @@ -110,6 +112,26 @@ def add_many_tecs(scen, years, n=50): scen.add_par("var_cost", tec_specs + ["year"], c, "USD/GWa") scen.add_par("emission_factor", tec_specs + ["CO2"], e, "tCO2") + scen.add_set("type_addon", "mitigation") + scen.add_set("map_tec_addon", ["tec1", "mitigation"]) + if t != "tec1": + scen.add_set("addon", t) + scen.add_cat("addon", "mitigation", t) + df = pd.DataFrame( + { + "node": "node", + "technology": "tec1", + "year_vtg": years, + "year_act": years, + "mode": "mode", + "time": "year", + "type_addon": "mitigation", + "value": 1, + "unit": "-", + } + ) + scen.add_par("addon_conversion", df) + def test_no_constraint(test_mp, request): scen = Scenario(test_mp, MODEL, scenario=request.node.name, version="new") @@ -272,7 +294,7 @@ def test_price_duality(test_mp, request, cumulative_bound): filters = {"node": "World"} emiss = scen.var("EMISS", filters).set_index("year").lvl emiss_tax = tax_scen.var("EMISS", filters).set_index("year").lvl - npt.assert_allclose(emiss, emiss_tax) + npt.assert_allclose(emiss, emiss_tax, rtol=0.05) # check that "PRICE_EMISSION" are close between cumulative and tax scenario filters = {"node": "World"} From d59634a3fcb90d630962f07ad6f05d72bb071a1c Mon Sep 17 00:00:00 2001 From: FRICKO Oliver Date: Thu, 6 Jul 2023 11:02:27 +0200 Subject: [PATCH 07/26] Revise duality test setup --- .../tests/test_feature_price_emission.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/message_ix/tests/test_feature_price_emission.py b/message_ix/tests/test_feature_price_emission.py index 0ca9e9684..e055d5a4e 100644 --- a/message_ix/tests/test_feature_price_emission.py +++ b/message_ix/tests/test_feature_price_emission.py @@ -99,6 +99,20 @@ def add_many_tecs(scen, years, n=50): output_specs = ["node", "comm", "level", "year", "year"] # tec: [emissions, var_cost, bound_activity_up] + # tecs = { + # "tec1": [10, 5, 1], + # "tec2": [-1, 10, 0.4], + # "tec3": [-5, 200, 0.3], + # "tec4": [-15, 1200, 0.2], + # "tec5": [-50, 6000, 0.1], + # } + # tecs = { + # "tec1": [10, 5, 1], + # "tec2": [-10, 10, 0.4], + # "tec3": [-12, 20, 0.3], + # "tec4": [-14, 30, 0.2], + # "tec5": [-16, 40, 0.1], + # } for i in range(1, n + 1): t = f"tec{i}" scen.add_set("technology", t) @@ -112,6 +126,10 @@ def add_many_tecs(scen, years, n=50): scen.add_par("var_cost", tec_specs + ["year"], c, "USD/GWa") scen.add_par("emission_factor", tec_specs + ["CO2"], e, "tCO2") + # scen.add_par("var_cost", tec_specs + ["year"], tecs[t][1], "USD/GWa") + # scen.add_par("emission_factor", tec_specs + ["co2"], tecs[t][0], "tCO2") + # scen.add_par("bound_activity_up", ["node", t, y, "mode", "year"], tecs[t][2], "GWa") + scen.add_set("type_addon", "mitigation") scen.add_set("map_tec_addon", ["tec1", "mitigation"]) if t != "tec1": From 73db819970a9ae9b22fba79ae0f393f71a2fc22e Mon Sep 17 00:00:00 2001 From: Fridolin Glatter Date: Fri, 1 Dec 2023 10:40:20 +0100 Subject: [PATCH 08/26] Refactor existing changes to fit latest code structure --- message_ix/models.py | 14 ++++-- .../tests/test_feature_price_emission.py | 46 +++++++++++-------- 2 files changed, 38 insertions(+), 22 deletions(-) diff --git a/message_ix/models.py b/message_ix/models.py index 32e45aaf7..526da1270 100644 --- a/message_ix/models.py +++ b/message_ix/models.py @@ -1,6 +1,6 @@ import logging from collections import ChainMap -from collections.abc import Mapping, MutableMapping +from collections.abc import MutableMapping from copy import copy from dataclasses import InitVar, dataclass, field from functools import partial @@ -176,7 +176,7 @@ class GAMSModel(ixmp.model.gams.GAMSModel): #: Mapping from model item (equation, parameter, set, or variable) names to #: :class:`.Item` describing the item. - items: Mapping[str, Item] + items: MutableMapping[str, Item] def __init__(self, name=None, **model_options): # Update the default options with any user-provided options @@ -408,6 +408,8 @@ def initialize(cls, scenario): par("commodity_stock", "n c l y") par("construction_time", "nl t yv") par("demand", "n c l y h") +par("df_year", "y") +par("df_period", "y") par("duration_period", "y") par("duration_time", "h") par("dynamic_land_lo", "n s y u") @@ -456,6 +458,7 @@ def initialize(cls, scenario): par("level_cost_activity_soft_up", "nl t ya h") par("level_cost_new_capacity_soft_lo", "nl t yv") par("level_cost_new_capacity_soft_up", "nl t yv") +par("levelized_cost", "n t y h") par("min_utilization_factor", "nl t yv ya") par("operation_factor", "nl t yv ya") par("output", "nl t yv ya m nd c l h hd") @@ -560,6 +563,11 @@ def initialize(cls, scenario): "n type_emission type_tec y", "Emission price (derived from marginals of EMISSION_BOUND constraint)", ) +var( + "PRICE_EMISSION_NEW", + "n type_emission type_tec y", + "TEMPORARY test for Emission price fix", +) var( "REL", "r nr yr", @@ -766,7 +774,7 @@ def initialize(cls, scenario): ) equ( "EMISSION_EQUIVALENCE", - "", + "n e type_tec y", "Auxiliary equation to simplify the notation of emissions", ) equ("EXTRACTION_BOUND_UP", "", "Upper bound on extraction (by grade)") diff --git a/message_ix/tests/test_feature_price_emission.py b/message_ix/tests/test_feature_price_emission.py index e055d5a4e..c3a97c708 100644 --- a/message_ix/tests/test_feature_price_emission.py +++ b/message_ix/tests/test_feature_price_emission.py @@ -98,7 +98,8 @@ def add_many_tecs(scen, years, n=50): """add a range of dirty-to-clean technologies to the scenario""" output_specs = ["node", "comm", "level", "year", "year"] - # tec: [emissions, var_cost, bound_activity_up] + # Add some hardcoded tecs for temporary testing + # tec: [emission_factor, var_cost, bound_activity_up] # tecs = { # "tec1": [10, 5, 1], # "tec2": [-1, 10, 0.4], @@ -106,29 +107,36 @@ def add_many_tecs(scen, years, n=50): # "tec4": [-15, 1200, 0.2], # "tec5": [-50, 6000, 0.1], # } - # tecs = { - # "tec1": [10, 5, 1], - # "tec2": [-10, 10, 0.4], - # "tec3": [-12, 20, 0.3], - # "tec4": [-14, 30, 0.2], - # "tec5": [-16, 40, 0.1], - # } - for i in range(1, n + 1): - t = f"tec{i}" + tecs = { + "tec1": [10, 5, 1], + "tec2": [-10, 10, 0.4], + "tec3": [-12, 20, 0.3], + "tec4": [-14, 30, 0.2], + "tec5": [-16, 40, 0.1], + } + # This is the original and we want to convert back to it after testing: + # for i in range(1, n + 1): + # t = f"tec{i}" + # ----------------- + for t in tecs: scen.add_set("technology", t) for y in years: tec_specs = ["node", t, y, y, "mode"] - # variable costs grow quadratically over technologies - # to get rid of the curse of linearity - c = (10 * i / n) ** 2 * (1.045) ** (y - years[0]) - e = 1 - i / n + # This is the original, convert back after testing: + # # variable costs grow quadratically over technologies + # # to get rid of the curse of linearity + # c = (10 * i / n) ** 2 * (1.045) ** (y - years[0]) + # e = 1 - i / n + # scen.add_par("var_cost", tec_specs + ["year"], c, "USD/GWa") + # scen.add_par("emission_factor", tec_specs + ["co2"], e, "tCO2") + # ------------- scen.add_par("output", tec_specs + output_specs, 1, "GWa") - scen.add_par("var_cost", tec_specs + ["year"], c, "USD/GWa") - scen.add_par("emission_factor", tec_specs + ["CO2"], e, "tCO2") - # scen.add_par("var_cost", tec_specs + ["year"], tecs[t][1], "USD/GWa") - # scen.add_par("emission_factor", tec_specs + ["co2"], tecs[t][0], "tCO2") - # scen.add_par("bound_activity_up", ["node", t, y, "mode", "year"], tecs[t][2], "GWa") + scen.add_par("var_cost", tec_specs + ["year"], tecs[t][1], "USD/GWa") + scen.add_par("emission_factor", tec_specs + ["CO2"], tecs[t][0], "tCO2") + scen.add_par( + "bound_activity_up", ["node", t, y, "mode", "year"], tecs[t][2], "GWa" + ) scen.add_set("type_addon", "mitigation") scen.add_set("map_tec_addon", ["tec1", "mitigation"]) From 7bd384512dfb5edd4a1fbd19176cc200e04ae9a6 Mon Sep 17 00:00:00 2001 From: Behnam Date: Thu, 11 Apr 2024 10:14:50 +0200 Subject: [PATCH 09/26] Refactor the calculation of PRICE_EMISSION and remove PRICE_EMISSION_NEW --- message_ix/model/MESSAGE/model_core.gms | 9 +----- message_ix/model/MESSAGE/model_solve.gms | 29 ++++++------------- message_ix/models.py | 10 +------ .../tests/test_feature_price_emission.py | 6 ++-- 4 files changed, 15 insertions(+), 39 deletions(-) diff --git a/message_ix/model/MESSAGE/model_core.gms b/message_ix/model/MESSAGE/model_core.gms index 858f33fd1..18adb4976 100644 --- a/message_ix/model/MESSAGE/model_core.gms +++ b/message_ix/model/MESSAGE/model_core.gms @@ -135,25 +135,18 @@ Variables * Variable Explanatory text * ======================================================================================= ======================================================================================================= * :math:`\text{DEMAND}_{n,c,l,y,h} \in \mathbb{R}` Demand level (in equilibrium with MACRO integration) -* :math:`\text{EMISSION_CONSTRAINT_RESCALE}_{n,\widehat{e},\widehat{t},y} \in \mathbb{R}` Annual average marginal of :ref:`emission_constraint` * :math:`\text{PRICE_COMMODITY}_{n,c,l,y,h} \in \mathbb{R}` Commodity price (undiscounted marginals of :ref:`commodity_balance_gt` and :ref:`commodity_balance_lt`) * :math:`\text{PRICE_EMISSION}_{n,\widehat{e},\widehat{t},y} \in \mathbb{R}` Emission price (undiscounted marginals of :ref:`emission_constraint`) * :math:`\text{COST_NODAL_NET}_{n,y} \in \mathbb{R}` System costs at the node level net of energy trade revenues/cost * :math:`\text{GDP}_{n,y} \in \mathbb{R}` Gross domestic product (GDP) in market exchange rates for MACRO reporting * ======================================================================================= ======================================================================================================= -* -* .. warning:: -* Please be aware that transitioning from one period length to another for consecutive periods may result in false values of :math:`\text{PRICE_EMISSION}`. -* Please see `this issue `_ for further information. We are currently working on a fix. *** Variables * auxiliary variables for demand, prices, costs and GDP (for reporting when MESSAGE is run with MACRO) DEMAND(node,commodity,level,year_all,time) demand - EMISSION_CONSTRAINT_RESCALE(node,type_emission,type_tec,type_year) rescaled of marginals of EMISSION_CONSTRAINT constraint PRICE_COMMODITY(node,commodity,level,year_all,time) commodity price (derived from marginals of COMMODITY_BALANCE constraint) - PRICE_EMISSION(node,type_emission,type_tec,year_all) emission price (derived from marginals of EMISSION_BOUND constraint) - PRICE_EMISSION_NEW(node,type_emission,type_tec,year_all) emission price (derived from marginals of EMISSION_EQUIVALENCE constraint) + PRICE_EMISSION(node,type_emission,type_tec,year_all) emission price (derived from marginals of EMISSION_EQUIVALENCE constraint) COST_NODAL_NET(node,year_all) system costs at the node level over time including effects of energy trade GDP(node,year_all) gross domestic product (GDP) in market exchange rates for MACRO reporting ; diff --git a/message_ix/model/MESSAGE/model_solve.gms b/message_ix/model/MESSAGE/model_solve.gms index f0f9bf13b..7b7bdf5c0 100644 --- a/message_ix/model/MESSAGE/model_solve.gms +++ b/message_ix/model/MESSAGE/model_solve.gms @@ -39,34 +39,23 @@ if (%foresight% = 0, ABORT "MESSAGEix did not solve to optimality!" ) ; -* rescale the dual of the emission constraint to account that the constraint is defined on the average year, not total -EMISSION_CONSTRAINT_RESCALE.l(node,type_emission,type_tec,type_year)$( - EMISSION_CONSTRAINT.m(node,type_emission,type_tec,type_year) ) = - EMISSION_CONSTRAINT.m(node,type_emission,type_tec,type_year) - / SUM(year$( cat_year(type_year,year) ), duration_period(year) ) -; - - -* assign auxiliary variables DEMAND, PRICE_COMMODITY and PRICE_EMISSION for integration with MACRO and reporting +* assign auxiliary variables DEMAND for integration with MACRO DEMAND.l(node,commodity,level,year,time) = demand_fixed(node,commodity,level,year,time) ; + +* assign auxiliary variables PRICE_COMMODITY and PRICE_EMISSION for reporting PRICE_COMMODITY.l(node,commodity,level,year,time) = ( COMMODITY_BALANCE_GT.m(node,commodity,level,year,time) + COMMODITY_BALANCE_LT.m(node,commodity,level,year,time) ) / df_period(year) ; - PRICE_EMISSION.l(node,type_emission,type_tec,year)$( SUM(type_year$( cat_year(type_year,year) ), 1 ) ) = - SMAX(type_year$( cat_year(type_year,year) ), - - EMISSION_CONSTRAINT_RESCALE.l(node,type_emission,type_tec,type_year) ) - / df_period(year) * duration_period(year); - PRICE_EMISSION.l(node,type_emission,type_tec,year)$( - PRICE_EMISSION.l(node,type_emission,type_tec,year) = - inf ) = 0 ; -* Calculate PRICE_EMISSION based on the marginals of EMISSION_EQUIVALENCE - PRICE_EMISSION_NEW.l(node,type_emission,type_tec,year)$( SUM(emission$( cat_emission(type_emission,emission) ), + +* calculate PRICE_EMISSION based on the marginals of EMISSION_EQUIVALENCE + PRICE_EMISSION.l(node,type_emission,type_tec,year)$( SUM(emission$( cat_emission(type_emission,emission) ), EMISSION_EQUIVALENCE.m(node,emission,type_tec,year) ) ) = SMAX(emission$( cat_emission(type_emission,emission) ), EMISSION_EQUIVALENCE.m(node,emission,type_tec,year) / emission_scaling(type_emission,emission) ) / df_period(year); - PRICE_EMISSION_NEW.l(node,type_emission,type_tec,year)$( - ( PRICE_EMISSION_NEW.l(node,type_emission,type_tec,year) = eps ) or - ( PRICE_EMISSION_NEW.l(node,type_emission,type_tec,year) = -inf ) ) = 0 ; + PRICE_EMISSION.l(node,type_emission,type_tec,year)$( + ( PRICE_EMISSION.l(node,type_emission,type_tec,year) = eps ) or + ( PRICE_EMISSION.l(node,type_emission,type_tec,year) = -inf ) ) = 0 ; %AUX_BOUNDS% AUX_ACT_BOUND_LO(node,tec,year_all,year_all2,mode,time)$( ACT.l(node,tec,year_all,year_all2,mode,time) < 0 AND %AUX_BOUNDS% ACT.l(node,tec,year_all,year_all2,mode,time) = -%AUX_BOUND_VALUE% ) = yes ; diff --git a/message_ix/models.py b/message_ix/models.py index 526da1270..48148bea5 100644 --- a/message_ix/models.py +++ b/message_ix/models.py @@ -408,8 +408,6 @@ def initialize(cls, scenario): par("commodity_stock", "n c l y") par("construction_time", "nl t yv") par("demand", "n c l y h") -par("df_year", "y") -par("df_period", "y") par("duration_period", "y") par("duration_time", "h") par("dynamic_land_lo", "n s y u") @@ -458,7 +456,6 @@ def initialize(cls, scenario): par("level_cost_activity_soft_up", "nl t ya h") par("level_cost_new_capacity_soft_lo", "nl t yv") par("level_cost_new_capacity_soft_up", "nl t yv") -par("levelized_cost", "n t y h") par("min_utilization_factor", "nl t yv ya") par("operation_factor", "nl t yv ya") par("output", "nl t yv ya m nd c l h hd") @@ -561,12 +558,7 @@ def initialize(cls, scenario): var( "PRICE_EMISSION", "n type_emission type_tec y", - "Emission price (derived from marginals of EMISSION_BOUND constraint)", -) -var( - "PRICE_EMISSION_NEW", - "n type_emission type_tec y", - "TEMPORARY test for Emission price fix", + "Emission price (derived from marginals of EMISSION_EQUIVALENCE constraint)", ) var( "REL", diff --git a/message_ix/tests/test_feature_price_emission.py b/message_ix/tests/test_feature_price_emission.py index c3a97c708..e1822adb0 100644 --- a/message_ix/tests/test_feature_price_emission.py +++ b/message_ix/tests/test_feature_price_emission.py @@ -8,8 +8,10 @@ solve_args = { "equ_list": ["EMISSION_EQUIVALENCE"], - "par_list": ["df_period", "df_year", "levelized_cost"], - "var_list": ["PRICE_EMISSION_NEW"], + # "par_list": ["df_period", "df_year", "levelized_cost"], + # At the moment, it is not possible to retrieve auxilliary parameters that + # are created in GAMS back to ixmp. The default "par_list" is coded in Java + # backened, and specifying a list here does not add/modify the default list. } From eff6ebaa61f3077f87074e11d5f70a9856940520 Mon Sep 17 00:00:00 2001 From: Behnam Date: Thu, 11 Apr 2024 10:46:16 +0200 Subject: [PATCH 10/26] Update the length of reporting graph --- message_ix/tests/test_report.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/message_ix/tests/test_report.py b/message_ix/tests/test_report.py index dc95c5852..6919691a5 100644 --- a/message_ix/tests/test_report.py +++ b/message_ix/tests/test_report.py @@ -85,7 +85,7 @@ def test_reporter_from_scenario(message_test_mp): assert_qty_equal(obs, demand, check_attrs=False) # ixmp.Reporter pre-populated with only model quantities and aggregates - assert 6462 == len(rep_ix.graph) + assert 6477 == len(rep_ix.graph) # message_ix.Reporter pre-populated with additional, derived quantities # This is the same value as in test_tutorials.py From c59d3d51989132c0c60c72e5465679211cfcfad0 Mon Sep 17 00:00:00 2001 From: Behnam Zakeri Date: Thu, 11 Apr 2024 13:32:51 +0200 Subject: [PATCH 11/26] Improve test_price_emission to be ready for review --- .../tests/test_feature_price_emission.py | 103 ++++++++++-------- 1 file changed, 60 insertions(+), 43 deletions(-) diff --git a/message_ix/tests/test_feature_price_emission.py b/message_ix/tests/test_feature_price_emission.py index e1822adb0..c20c28a20 100644 --- a/message_ix/tests/test_feature_price_emission.py +++ b/message_ix/tests/test_feature_price_emission.py @@ -14,6 +14,8 @@ # backened, and specifying a list here does not add/modify the default list. } +interest_rate = 0.05 + def model_setup(scen, years, simple_tecs=True): """generate a minimal model to test the behaviour of the emission prices""" @@ -21,6 +23,7 @@ def model_setup(scen, years, simple_tecs=True): scen.add_set("commodity", "comm") scen.add_set("level", "level") scen.add_set("year", years) + scen.add_cat("year", "firstmodelyear", years[0]) scen.add_set("mode", "mode") @@ -28,7 +31,7 @@ def model_setup(scen, years, simple_tecs=True): scen.add_cat("emission", "ghg", "CO2") for y in years: - scen.add_par("interestrate", y, 0.05, "-") + scen.add_par("interestrate", y, interest_rate, "-") scen.add_par("demand", ["node", "comm", "level", y, "year"], 1, "GWa") if simple_tecs: @@ -140,25 +143,25 @@ def add_many_tecs(scen, years, n=50): "bound_activity_up", ["node", t, y, "mode", "year"], tecs[t][2], "GWa" ) - scen.add_set("type_addon", "mitigation") - scen.add_set("map_tec_addon", ["tec1", "mitigation"]) - if t != "tec1": - scen.add_set("addon", t) - scen.add_cat("addon", "mitigation", t) - df = pd.DataFrame( - { - "node": "node", - "technology": "tec1", - "year_vtg": years, - "year_act": years, - "mode": "mode", - "time": "year", - "type_addon": "mitigation", - "value": 1, - "unit": "-", - } - ) - scen.add_par("addon_conversion", df) + # scen.add_set("type_addon", "mitigation") + # scen.add_set("map_tec_addon", ["tec1", "mitigation"]) + # if t != "tec1": + # scen.add_set("addon", t) + # scen.add_cat("addon", "mitigation", t) + # df = pd.DataFrame( + # { + # "node": "node", + # "technology": "tec1", + # "year_vtg": years, + # "year_act": years, + # "mode": "mode", + # "time": "year", + # "type_addon": "mitigation", + # "value": 1, + # "unit": "-", + # } + # ) + # scen.add_par("addon_conversion", df) def test_no_constraint(test_mp, request): @@ -188,7 +191,7 @@ def test_cumulative_equidistant(test_mp, request): # under a cumulative constraint, the price must increase with the discount # rate starting from the marginal relaxation in the first year obs = scen.var("PRICE_EMISSION")["lvl"].values - npt.assert_allclose(obs, [1.05 ** (y - years[0]) for y in years]) + npt.assert_allclose(obs, [(1 + interest_rate) ** (y - years[0]) for y in years]) def test_per_period_equidistant(test_mp, request): @@ -282,20 +285,31 @@ def test_custom_type_variable_periodlength(test_mp, request): npt.assert_allclose(obs, exp) - -@pytest.mark.parametrize("cumulative_bound", [0.25, 0.5, 0.75]) -def test_price_duality(test_mp, request, cumulative_bound): - years = [2020, 2025, 2030, 2040, 2050] - for c in [0.25, 0.5, 0.75]: - # set up a scenario for cumulative constraints - scen = Scenario(test_mp, MODEL, "cum_many_tecs", version="new") - model_setup(scen, years, simple_tecs=False) - scen.add_cat("year", "cumulative", years) - scen.add_par( - "bound_emission", ["World", "ghg", "all", "cumulative"], 0.5, "tCO2" +@pytest.mark.parametrize( + "cumulative_bound, years, tag", + ( + (0.25, [2020, 2030, 2040, 2050], "0.25_equal"), + (0.25, [2020, 2025, 2030, 2040, 2050], "0.25_varying"), + (0.50, [2020, 2030, 2040, 2050], "0.5_equal") + (0.50, [2020, 2025, 2030, 2040, 2050], "0.5_varying") + (0.75, [2020, 2030, 2040, 2050], "0.75_equal") + (0.75, [2020, 2025, 2030, 2040, 2050], "0.75_varying"), ) - scen.commit("initialize test scenario") - scen.solve(quiet=True) + ) +def test_price_duality(test_mp, request, cumulative_bound, years, tag): + + # set up a scenario for cumulative constraints + scen = Scenario(test_mp, MODEL, "cum_many_tecs_" + tag, version="new") + model_setup(scen, years, simple_tecs=False) + scen.add_cat("year", "cumulative", years) + scen.add_par( + "bound_emission", + ["World", "ghg", "all", "cumulative"], + cumulative_bound, + "tCO2" + ) + scen.commit("initialize test scenario") + scen.solve(quiet=True) # ---------------------------------------------------------- # Run scenario with `tax_emission` based on `PRICE_EMISSION` @@ -303,7 +317,7 @@ def test_price_duality(test_mp, request, cumulative_bound): # ---------------------------------------------------------- tax_scen = Scenario( - test_mp, MODEL, scenario=request.node.name + "_tax_many_tecs", version="new" + test_mp, MODEL, scenario="tax_many_tecs_" + tag, version="new" ) model_setup(tax_scen, years, simple_tecs=False) for y in years: @@ -314,20 +328,21 @@ def test_price_duality(test_mp, request, cumulative_bound): columns={"year": "type_year", "lvl": "value"} ) taxes["unit"] = "USD/tCO2" + # taxes["node"] = "node" tax_scen.add_par("tax_emission", taxes) tax_scen.commit("initialize test scenario for taxes") tax_scen.solve(quiet=True, **solve_args) - # check that emissions are close between cumulative and tax scenario + # check emissions are close between cumulative and tax scenarios filters = {"node": "World"} emiss = scen.var("EMISS", filters).set_index("year").lvl emiss_tax = tax_scen.var("EMISS", filters).set_index("year").lvl npt.assert_allclose(emiss, emiss_tax, rtol=0.05) - # check that "PRICE_EMISSION" are close between cumulative and tax scenario + # check "PRICE_EMISSION" is close between cumulative and tax scenarios filters = {"node": "World"} - pemiss = scen.var("PRICE_EMISSION_NEW", filters).set_index("year").lvl - pemiss_tax = tax_scen.var("PRICE_EMISSION_NEW", filters).set_index("year").lvl + pemiss = scen.var("PRICE_EMISSION", filters).set_index("year").lvl + pemiss_tax = tax_scen.var("PRICE_EMISSION", filters).set_index("year").lvl npt.assert_allclose(pemiss, pemiss_tax) # -------------------------------------------------------- @@ -335,7 +350,9 @@ def test_price_duality(test_mp, request, cumulative_bound): # from cumulative constraint scenario. # -------------------------------------------------------- - perbnd_scen = Scenario(test_mp, MODEL, "period-bnd_many_tecs", version="new") + perbnd_scen = Scenario( + test_mp, MODEL, "period-bnd_many_tecs_" + tag, version="new" + ) model_setup(perbnd_scen, years, simple_tecs=False) for y in years: perbnd_scen.add_cat("year", y, y) @@ -352,10 +369,10 @@ def test_price_duality(test_mp, request, cumulative_bound): perbnd_scen.commit("initialize test scenario for periodic emission bound") perbnd_scen.solve(quiet=True, **solve_args) - # check that emissions are close between cumulative and tax scenario + # check -emissions are close between cumulative and yearly-bound scenarios emiss_bnd = perbnd_scen.var("EMISS", filters).set_index("year").lvl npt.assert_allclose(emiss, emiss_bnd) - # check that "PRICE_EMISSION" are close between cumulative and tax scenario - pemiss_bnd = perbnd_scen.var("PRICE_EMISSION_NEW", filters).set_index("year").lvl + # check "PRICE_EMISSION" is close between cumulative- and yearly-bound scenarios + pemiss_bnd = perbnd_scen.var("PRICE_EMISSION", filters).set_index("year").lvl npt.assert_allclose(pemiss, pemiss_bnd) From 34f4d361f51b4f2968ac2c08d750bf19999b6fae Mon Sep 17 00:00:00 2001 From: Behnam Zakeri Date: Thu, 11 Apr 2024 13:51:01 +0200 Subject: [PATCH 12/26] Remove hardcoded data from test and apply ruff --- .../tests/test_feature_price_emission.py | 65 +++---------------- 1 file changed, 10 insertions(+), 55 deletions(-) diff --git a/message_ix/tests/test_feature_price_emission.py b/message_ix/tests/test_feature_price_emission.py index c20c28a20..1cc877caa 100644 --- a/message_ix/tests/test_feature_price_emission.py +++ b/message_ix/tests/test_feature_price_emission.py @@ -1,5 +1,4 @@ import numpy.testing as npt -import pandas as pd import pytest from message_ix import Scenario, make_df @@ -24,7 +23,6 @@ def model_setup(scen, years, simple_tecs=True): scen.add_set("level", "level") scen.add_set("year", years) scen.add_cat("year", "firstmodelyear", years[0]) - scen.add_set("mode", "mode") scen.add_set("emission", "CO2") @@ -105,13 +103,6 @@ def add_many_tecs(scen, years, n=50): # Add some hardcoded tecs for temporary testing # tec: [emission_factor, var_cost, bound_activity_up] - # tecs = { - # "tec1": [10, 5, 1], - # "tec2": [-1, 10, 0.4], - # "tec3": [-5, 200, 0.3], - # "tec4": [-15, 1200, 0.2], - # "tec5": [-50, 6000, 0.1], - # } tecs = { "tec1": [10, 5, 1], "tec2": [-10, 10, 0.4], @@ -119,22 +110,11 @@ def add_many_tecs(scen, years, n=50): "tec4": [-14, 30, 0.2], "tec5": [-16, 40, 0.1], } - # This is the original and we want to convert back to it after testing: - # for i in range(1, n + 1): - # t = f"tec{i}" - # ----------------- + for t in tecs: scen.add_set("technology", t) for y in years: tec_specs = ["node", t, y, y, "mode"] - # This is the original, convert back after testing: - # # variable costs grow quadratically over technologies - # # to get rid of the curse of linearity - # c = (10 * i / n) ** 2 * (1.045) ** (y - years[0]) - # e = 1 - i / n - # scen.add_par("var_cost", tec_specs + ["year"], c, "USD/GWa") - # scen.add_par("emission_factor", tec_specs + ["co2"], e, "tCO2") - # ------------- scen.add_par("output", tec_specs + output_specs, 1, "GWa") scen.add_par("var_cost", tec_specs + ["year"], tecs[t][1], "USD/GWa") @@ -143,26 +123,6 @@ def add_many_tecs(scen, years, n=50): "bound_activity_up", ["node", t, y, "mode", "year"], tecs[t][2], "GWa" ) - # scen.add_set("type_addon", "mitigation") - # scen.add_set("map_tec_addon", ["tec1", "mitigation"]) - # if t != "tec1": - # scen.add_set("addon", t) - # scen.add_cat("addon", "mitigation", t) - # df = pd.DataFrame( - # { - # "node": "node", - # "technology": "tec1", - # "year_vtg": years, - # "year_act": years, - # "mode": "mode", - # "time": "year", - # "type_addon": "mitigation", - # "value": 1, - # "unit": "-", - # } - # ) - # scen.add_par("addon_conversion", df) - def test_no_constraint(test_mp, request): scen = Scenario(test_mp, MODEL, scenario=request.node.name, version="new") @@ -285,19 +245,19 @@ def test_custom_type_variable_periodlength(test_mp, request): npt.assert_allclose(obs, exp) + @pytest.mark.parametrize( "cumulative_bound, years, tag", ( (0.25, [2020, 2030, 2040, 2050], "0.25_equal"), (0.25, [2020, 2025, 2030, 2040, 2050], "0.25_varying"), - (0.50, [2020, 2030, 2040, 2050], "0.5_equal") - (0.50, [2020, 2025, 2030, 2040, 2050], "0.5_varying") - (0.75, [2020, 2030, 2040, 2050], "0.75_equal") + (0.50, [2020, 2030, 2040, 2050], "0.5_equal"), + (0.50, [2020, 2025, 2030, 2040, 2050], "0.5_varying"), + (0.75, [2020, 2030, 2040, 2050], "0.75_equal"), (0.75, [2020, 2025, 2030, 2040, 2050], "0.75_varying"), - ) - ) + ), +) def test_price_duality(test_mp, request, cumulative_bound, years, tag): - # set up a scenario for cumulative constraints scen = Scenario(test_mp, MODEL, "cum_many_tecs_" + tag, version="new") model_setup(scen, years, simple_tecs=False) @@ -306,7 +266,7 @@ def test_price_duality(test_mp, request, cumulative_bound, years, tag): "bound_emission", ["World", "ghg", "all", "cumulative"], cumulative_bound, - "tCO2" + "tCO2", ) scen.commit("initialize test scenario") scen.solve(quiet=True) @@ -315,10 +275,7 @@ def test_price_duality(test_mp, request, cumulative_bound, years, tag): # Run scenario with `tax_emission` based on `PRICE_EMISSION` # from cumulative constraint scenario. # ---------------------------------------------------------- - - tax_scen = Scenario( - test_mp, MODEL, scenario="tax_many_tecs_" + tag, version="new" - ) + tax_scen = Scenario(test_mp, MODEL, scenario="tax_many_tecs_" + tag, version="new") model_setup(tax_scen, years, simple_tecs=False) for y in years: tax_scen.add_cat("year", y, y) @@ -350,9 +307,7 @@ def test_price_duality(test_mp, request, cumulative_bound, years, tag): # from cumulative constraint scenario. # -------------------------------------------------------- - perbnd_scen = Scenario( - test_mp, MODEL, "period-bnd_many_tecs_" + tag, version="new" - ) + perbnd_scen = Scenario(test_mp, MODEL, "period-bnd_many_tecs_" + tag, version="new") model_setup(perbnd_scen, years, simple_tecs=False) for y in years: perbnd_scen.add_cat("year", y, y) From 53fae79cc09ec6d53ac9ed1bd2f6e8a91248e10f Mon Sep 17 00:00:00 2001 From: Behnam Zakeri Date: Thu, 11 Apr 2024 14:07:11 +0200 Subject: [PATCH 13/26] Apply ruff format for parameterizing tests --- message_ix/tests/test_feature_price_emission.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/message_ix/tests/test_feature_price_emission.py b/message_ix/tests/test_feature_price_emission.py index 1cc877caa..b76670077 100644 --- a/message_ix/tests/test_feature_price_emission.py +++ b/message_ix/tests/test_feature_price_emission.py @@ -248,14 +248,14 @@ def test_custom_type_variable_periodlength(test_mp, request): @pytest.mark.parametrize( "cumulative_bound, years, tag", - ( + [ (0.25, [2020, 2030, 2040, 2050], "0.25_equal"), (0.25, [2020, 2025, 2030, 2040, 2050], "0.25_varying"), (0.50, [2020, 2030, 2040, 2050], "0.5_equal"), (0.50, [2020, 2025, 2030, 2040, 2050], "0.5_varying"), (0.75, [2020, 2030, 2040, 2050], "0.75_equal"), (0.75, [2020, 2025, 2030, 2040, 2050], "0.75_varying"), - ), + ], ) def test_price_duality(test_mp, request, cumulative_bound, years, tag): # set up a scenario for cumulative constraints From 30c0483c06f91212ef61355258e3b4ef655c175b Mon Sep 17 00:00:00 2001 From: Behnam Zakeri Date: Thu, 11 Apr 2024 14:15:19 +0200 Subject: [PATCH 14/26] Update the length of the reporting graph --- message_ix/tests/test_report.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/message_ix/tests/test_report.py b/message_ix/tests/test_report.py index 6919691a5..44ac94e45 100644 --- a/message_ix/tests/test_report.py +++ b/message_ix/tests/test_report.py @@ -89,7 +89,7 @@ def test_reporter_from_scenario(message_test_mp): # message_ix.Reporter pre-populated with additional, derived quantities # This is the same value as in test_tutorials.py - assert 13724 == len(rep.graph) + assert 13739 == len(rep.graph) # Derived quantities have expected dimensions vom_key = rep.full_key("vom") From 9e9a4318336bec87b4f95a290191891f59183221 Mon Sep 17 00:00:00 2001 From: Fridolin Glatter Date: Wed, 15 May 2024 14:51:24 +0200 Subject: [PATCH 15/26] Fix explanation of PRICE_EMISSION --- message_ix/model/MESSAGE/model_core.gms | 2 +- message_ix/model/MESSAGE/model_solve.gms | 2 +- tutorial/westeros/westeros_emissions_taxes.ipynb | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/message_ix/model/MESSAGE/model_core.gms b/message_ix/model/MESSAGE/model_core.gms index 18adb4976..8d18913a6 100644 --- a/message_ix/model/MESSAGE/model_core.gms +++ b/message_ix/model/MESSAGE/model_core.gms @@ -136,7 +136,7 @@ Variables * ======================================================================================= ======================================================================================================= * :math:`\text{DEMAND}_{n,c,l,y,h} \in \mathbb{R}` Demand level (in equilibrium with MACRO integration) * :math:`\text{PRICE_COMMODITY}_{n,c,l,y,h} \in \mathbb{R}` Commodity price (undiscounted marginals of :ref:`commodity_balance_gt` and :ref:`commodity_balance_lt`) -* :math:`\text{PRICE_EMISSION}_{n,\widehat{e},\widehat{t},y} \in \mathbb{R}` Emission price (undiscounted marginals of :ref:`emission_constraint`) +* :math:`\text{PRICE_EMISSION}_{n,\widehat{e},\widehat{t},y} \in \mathbb{R}` Emission price (undiscounted marginals of :ref:`emission_equivalence`) * :math:`\text{COST_NODAL_NET}_{n,y} \in \mathbb{R}` System costs at the node level net of energy trade revenues/cost * :math:`\text{GDP}_{n,y} \in \mathbb{R}` Gross domestic product (GDP) in market exchange rates for MACRO reporting * ======================================================================================= ======================================================================================================= diff --git a/message_ix/model/MESSAGE/model_solve.gms b/message_ix/model/MESSAGE/model_solve.gms index 7b7bdf5c0..594f9cc86 100644 --- a/message_ix/model/MESSAGE/model_solve.gms +++ b/message_ix/model/MESSAGE/model_solve.gms @@ -53,7 +53,7 @@ if (%foresight% = 0, SMAX(emission$( cat_emission(type_emission,emission) ), EMISSION_EQUIVALENCE.m(node,emission,type_tec,year) / emission_scaling(type_emission,emission) ) / df_period(year); - PRICE_EMISSION.l(node,type_emission,type_tec,year)$( + PRICE_EMISSION.l(node,type_emission,type_tec,year)$( ( PRICE_EMISSION.l(node,type_emission,type_tec,year) = eps ) or ( PRICE_EMISSION.l(node,type_emission,type_tec,year) = -inf ) ) = 0 ; diff --git a/tutorial/westeros/westeros_emissions_taxes.ipynb b/tutorial/westeros/westeros_emissions_taxes.ipynb index fd532291e..f3ccfaf07 100644 --- a/tutorial/westeros/westeros_emissions_taxes.ipynb +++ b/tutorial/westeros/westeros_emissions_taxes.ipynb @@ -83,7 +83,7 @@ "metadata": {}, "source": [ "When setting a cumulative bound, the undiscounted price of emission is the same in different model years (see the marginals of\n", - "equation `\"EMISSION_CONSTRAINT\"`). However, considering the year-to-year discount factor, we observe an ascending trend in\n", + "equation `\"EMISSION_EQUIVALENCE\"`). However, considering the year-to-year discount factor, we observe an ascending trend in\n", "emission prices shown in `\"PRICE_EMISSION\"` above. This means the emission price in later years is higher as the value of money in\n", "the future is lower compared to today. " ] @@ -138,6 +138,8 @@ "metadata": {}, "outputs": [], "source": [ + "from message_ix.util import make_df\n", + "\n", "horizon = [700, 710, 720]\n", "\n", "bd_emission = message_ix.make_df(\n", From 3a53a07b0137835459b4d98bda0fa4d0d58a8e8f Mon Sep 17 00:00:00 2001 From: Fridolin Glatter Date: Thu, 16 May 2024 14:35:53 +0200 Subject: [PATCH 16/26] Adapt tests trying to make test_price_duality work --- .../tests/test_feature_price_emission.py | 445 ++++++++++++++---- 1 file changed, 344 insertions(+), 101 deletions(-) diff --git a/message_ix/tests/test_feature_price_emission.py b/message_ix/tests/test_feature_price_emission.py index b76670077..959cf7d14 100644 --- a/message_ix/tests/test_feature_price_emission.py +++ b/message_ix/tests/test_feature_price_emission.py @@ -16,59 +16,52 @@ interest_rate = 0.05 -def model_setup(scen, years, simple_tecs=True): +def model_setup(scen: Scenario, years: list[int], simple_tecs=True) -> None: """generate a minimal model to test the behaviour of the emission prices""" scen.add_spatial_sets({"country": "node"}) scen.add_set("commodity", "comm") scen.add_set("level", "level") - scen.add_set("year", years) - scen.add_cat("year", "firstmodelyear", years[0]) + scen.add_horizon(years) scen.add_set("mode", "mode") - scen.add_set("emission", "CO2") - scen.add_cat("emission", "ghg", "CO2") + scen.add_cat("emission", "GHG", "CO2") for y in years: scen.add_par("interestrate", y, interest_rate, "-") - scen.add_par("demand", ["node", "comm", "level", y, "year"], 1, "GWa") - - if simple_tecs: - add_two_tecs(scen, years) - else: - add_many_tecs(scen, years) - - -def add_two_tecs(scen, years): + scen.add_par( + "demand", + make_df( + "demand", + node="node", + commodity="comm", + level="level", + time="year", + year=y, + value=60 + 2 * (y - years[0]), + unit="GWa", + ), + ) + + add_two_tecs(scen, years) if simple_tecs else add_many_tecs(scen, years) + + +def add_two_tecs(scen: Scenario, years: list[int]) -> None: """add two technologies to the scenario""" scen.add_set("technology", ["dirty_tec", "clean_tec"]) common = dict(node_loc="node", year_vtg=years, year_act=years, value=1, mode="mode") + output_specs = ["node", "comm", "level", "year", "year"] - # the dirty technology is free (no costs) but has emissions - scen.add_par( - "output", - make_df( - "output", - node_dest="node", - technology="dirty_tec", - commodity="comm", - level="level", - time="year", - time_dest="year", - unit="GWa", - **common, - ), - ) - scen.add_par( - "emission_factor", - make_df( - "emission_factor", - unit="tCO2", - technology="dirty_tec", - emission="CO2", - **common, - ), - ) + for y in years: + # the dirty technology is free (no costs) but has emissions + tec_specs = ["node", "dirty_tec", y, y, "mode"] + scen.add_par("output", tec_specs + output_specs, 1, "GWa") + scen.add_par("emission_factor", tec_specs + ["CO2"], 1, "tCO2") + + # the clean technology has variable costs but no emissions + tec_specs = ["node", "clean_tec", y, y, "mode"] + scen.add_par("output", tec_specs + output_specs, 1, "GWa") + scen.add_par("var_cost", tec_specs + ["year"], 1, "USD/GWa") # the clean technology has variable costs but no emissions scen.add_par( @@ -97,31 +90,149 @@ def add_two_tecs(scen, years): ) -def add_many_tecs(scen, years, n=50): +def add_many_tecs(scen: Scenario, years: list[int], n=50) -> None: """add a range of dirty-to-clean technologies to the scenario""" - output_specs = ["node", "comm", "level", "year", "year"] - # Add some hardcoded tecs for temporary testing - # tec: [emission_factor, var_cost, bound_activity_up] - tecs = { - "tec1": [10, 5, 1], - "tec2": [-10, 10, 0.4], - "tec3": [-12, 20, 0.3], - "tec4": [-14, 30, 0.2], - "tec5": [-16, 40, 0.1], + tecs: dict[str, dict] = { + "tec1": { + "emission_factor": 10, + "inv_cost": 500, + "fix_cost": 30, + "var_cost": 30, + "bound_activity_up": 100, + "lifetime": 20, + }, + "tec2": { + "emission_factor": -10, + "inv_cost": 1200, + "fix_cost": 10, + "var_cost": 0, + "bound_activity_up": 40, + "lifetime": 20, + }, + "tec3": { + "emission_factor": -12, + "inv_cost": 1300, + "fix_cost": 12, + "var_cost": 0, + "bound_activity_up": 30, + "lifetime": 20, + }, + "tec4": { + "emission_factor": -14, + "inv_cost": 1400, + "fix_cost": 14, + "var_cost": 0, + "bound_activity_up": 20, + "lifetime": 20, + }, + "tec5": { + "emission_factor": -16, + "inv_cost": 1500, + "fix_cost": 16, + "var_cost": 0, + "bound_activity_up": 10, + "lifetime": 20, + }, } + year_df = scen.vintage_and_active_years() + vintage_years, act_years = year_df["year_vtg"], year_df["year_act"] for t in tecs: scen.add_set("technology", t) - for y in years: - tec_specs = ["node", t, y, y, "mode"] - scen.add_par("output", tec_specs + output_specs, 1, "GWa") - - scen.add_par("var_cost", tec_specs + ["year"], tecs[t][1], "USD/GWa") - scen.add_par("emission_factor", tec_specs + ["CO2"], tecs[t][0], "tCO2") - scen.add_par( - "bound_activity_up", ["node", t, y, "mode", "year"], tecs[t][2], "GWa" - ) + scen.add_par( + "output", + make_df( + "output", + node_loc="node", + node_dest="node", + technology=t, + year_vtg=vintage_years, + year_act=act_years, + mode="mode", + commodity="comm", + level="level", + time="year", + time_dest="year", + value=1, + unit="GWa", + ), + ) + scen.add_par( + "technical_lifetime", + make_df( + "technical_lifetime", + node_loc="node", + year_vtg=vintage_years, + unit="y", + technology=t, + value=tecs[t]["lifetime"], + ), + ) + scen.add_par( + "var_cost", + make_df( + "var_cost", + node_loc="node", + year_vtg=vintage_years, + year_act=act_years, + mode="mode", + time="year", + unit="USD/kWa", + technology=t, + value=tecs[t]["fix_cost"], + ), + ) + scen.add_par( + "inv_cost", + make_df( + "inv_cost", + node_loc="node", + year_vtg=vintage_years, + unit="USD/kW", + technology=t, + value=tecs[t]["inv_cost"], + ), + ) + scen.add_par( + "fix_cost", + make_df( + "fix_cost", + node_loc="node", + year_vtg=vintage_years, + year_act=act_years, + unit="USD/kWa", + technology=t, + value=tecs[t]["fix_cost"], + ), + ) + scen.add_par( + "emission_factor", + make_df( + "emission_factor", + node_loc="node", + year_vtg=vintage_years, + year_act=act_years, + unit="tCO2/kWa", + technology=t, + mode="mode", + value=tecs[t]["emission_factor"], + emission="CO2", + ), + ) + scen.add_par( + "bound_activity_up", + make_df( + "bound_activity_up", + node_loc="node", + technology=t, + year_act=act_years, + mode="mode", + time="year", + value=tecs[t]["bound_activity_up"], + unit="GWa", + ), + ) def test_no_constraint(test_mp, request): @@ -142,7 +253,7 @@ def test_cumulative_equidistant(test_mp, request): model_setup(scen, years) scen.add_cat("year", "cumulative", years) - scen.add_par("bound_emission", ["World", "ghg", "all", "cumulative"], 0, "tCO2") + scen.add_par("bound_emission", ["World", "GHG", "all", "cumulative"], 0, "tCO2") scen.commit("initialize test scenario") scen.solve(quiet=True) @@ -161,7 +272,7 @@ def test_per_period_equidistant(test_mp, request): model_setup(scen, years) for y in years: scen.add_cat("year", y, y) - scen.add_par("bound_emission", ["World", "ghg", "all", y], 0, "tCO2") + scen.add_par("bound_emission", ["World", "GHG", "all", y], 0, "tCO2") scen.commit("initialize test scenario") scen.solve(quiet=True) @@ -178,7 +289,7 @@ def test_cumulative_variable_periodlength(test_mp, request): model_setup(scen, years) scen.add_cat("year", "cumulative", years) - scen.add_par("bound_emission", ["World", "ghg", "all", "cumulative"], 0, "tCO2") + scen.add_par("bound_emission", ["World", "GHG", "all", "cumulative"], 0, "tCO2") scen.commit("initialize test scenario") scen.solve(quiet=True, **solve_args) @@ -205,7 +316,7 @@ def test_per_period_variable_periodlength(test_mp, request): model_setup(scen, years) for y in years: scen.add_cat("year", y, y) - scen.add_par("bound_emission", ["World", "ghg", "all", y], 0, "tCO2") + scen.add_par("bound_emission", ["World", "GHG", "all", y], 0, "tCO2") scen.commit("initialize test scenario") scen.solve(quiet=True) @@ -223,7 +334,7 @@ def test_custom_type_variable_periodlength(test_mp, request): model_setup(scen, years) scen.add_cat("year", "custom", custom) - scen.add_par("bound_emission", ["World", "ghg", "all", "custom"], 0, "tCO2") + scen.add_par("bound_emission", ["World", "GHG", "all", "custom"], 0, "tCO2") scen.commit("initialize test scenario") scen.solve(quiet=True, **solve_args) @@ -247,87 +358,219 @@ def test_custom_type_variable_periodlength(test_mp, request): @pytest.mark.parametrize( - "cumulative_bound, years, tag", + "cumulative_bound, years", [ - (0.25, [2020, 2030, 2040, 2050], "0.25_equal"), - (0.25, [2020, 2025, 2030, 2040, 2050], "0.25_varying"), - (0.50, [2020, 2030, 2040, 2050], "0.5_equal"), - (0.50, [2020, 2025, 2030, 2040, 2050], "0.5_varying"), - (0.75, [2020, 2030, 2040, 2050], "0.75_equal"), - (0.75, [2020, 2025, 2030, 2040, 2050], "0.75_varying"), + (2.5, [2020, 2030, 2040, 2050]), + (0.25, [2020, 2025, 2030, 2040, 2050]), + (0.50, [2020, 2030, 2040, 2050]), + (0.50, [2020, 2025, 2030, 2040, 2050]), + (0.75, [2020, 2030, 2040, 2050]), + (0.75, [2020, 2025, 2030, 2040, 2050]), ], ) -def test_price_duality(test_mp, request, cumulative_bound, years, tag): +def test_price_duality(test_mp, request, cumulative_bound, years): # set up a scenario for cumulative constraints - scen = Scenario(test_mp, MODEL, "cum_many_tecs_" + tag, version="new") + scen = Scenario( + test_mp, + MODEL, + scenario=f"{request.node.name}_cum_many_tecs", + version="new", + ) model_setup(scen, years, simple_tecs=False) scen.add_cat("year", "cumulative", years) scen.add_par( "bound_emission", - ["World", "ghg", "all", "cumulative"], + ["World", "GHG", "all", "cumulative"], cumulative_bound, "tCO2", ) scen.commit("initialize test scenario") - scen.solve(quiet=True) + scen.solve(quiet=True, **solve_args) # ---------------------------------------------------------- # Run scenario with `tax_emission` based on `PRICE_EMISSION` # from cumulative constraint scenario. # ---------------------------------------------------------- - tax_scen = Scenario(test_mp, MODEL, scenario="tax_many_tecs_" + tag, version="new") - model_setup(tax_scen, years, simple_tecs=False) - for y in years: - tax_scen.add_cat("year", y, y) + scen_tax = Scenario( + test_mp, + MODEL, + scenario=f"{request.node.name}_tax_many_tecs", + version="new", + ) + model_setup(scen_tax, years, simple_tecs=False) + for year in years: + scen_tax.add_cat("year", year, year) # use emission prices from cumulative-constraint scenario as taxes taxes = scen.var("PRICE_EMISSION").rename( columns={"year": "type_year", "lvl": "value"} ) taxes["unit"] = "USD/tCO2" - # taxes["node"] = "node" - tax_scen.add_par("tax_emission", taxes) - tax_scen.commit("initialize test scenario for taxes") - tax_scen.solve(quiet=True, **solve_args) + taxes["node"] = "node" + scen_tax.add_par("tax_emission", taxes) + scen_tax.commit("initialize test scenario for taxes") + scen_tax.solve(quiet=True, **solve_args) + # scen_tax.solve(**solve_args) + + print(scen.var("PRICE_EMISSION")) + print(scen_tax.var("PRICE_EMISSION")) + print(scen_tax.par("tax_emission")) + print(scen.var("EMISS")) + print(scen_tax.var("EMISS")) # check emissions are close between cumulative and tax scenarios filters = {"node": "World"} emiss = scen.var("EMISS", filters).set_index("year").lvl - emiss_tax = tax_scen.var("EMISS", filters).set_index("year").lvl + emiss_tax = scen_tax.var("EMISS", filters).set_index("year").lvl npt.assert_allclose(emiss, emiss_tax, rtol=0.05) # check "PRICE_EMISSION" is close between cumulative and tax scenarios filters = {"node": "World"} - pemiss = scen.var("PRICE_EMISSION", filters).set_index("year").lvl - pemiss_tax = tax_scen.var("PRICE_EMISSION", filters).set_index("year").lvl - npt.assert_allclose(pemiss, pemiss_tax) + price_emission = scen.var("PRICE_EMISSION", filters).set_index("year").lvl + price_emission_tax = scen_tax.var("PRICE_EMISSION", filters).set_index("year").lvl + npt.assert_allclose(price_emission, price_emission_tax) # -------------------------------------------------------- # Run scenario with annual-emission bound based on `EMISS` # from cumulative constraint scenario. # -------------------------------------------------------- - perbnd_scen = Scenario(test_mp, MODEL, "period-bnd_many_tecs_" + tag, version="new") - model_setup(perbnd_scen, years, simple_tecs=False) - for y in years: - perbnd_scen.add_cat("year", y, y) + scenario_period_bound = Scenario( + test_mp, + MODEL, + f"{request.node.name}_period_bound_many_tecs", + version="new", + ) + model_setup(scenario_period_bound, years, simple_tecs=False) + for year in years: + scenario_period_bound.add_cat("year", year, year) - # use emission prices from cumulative-constraint scenario as taxes - bnd_emiss = ( + # use emissions from cumulative-constraint scenario as period-emission bounds + emiss_period_bound = ( scen.var("EMISS", {"node": "World"}) .rename(columns={"year": "type_year", "lvl": "value"}) .drop("emission", axis=1) ) - bnd_emiss["type_emission"] = "ghg" - bnd_emiss["unit"] = "tCO2" - perbnd_scen.add_par("bound_emission", bnd_emiss) - perbnd_scen.commit("initialize test scenario for periodic emission bound") - perbnd_scen.solve(quiet=True, **solve_args) + emiss_period_bound["type_emission"] = "GHG" + emiss_period_bound["unit"] = "tCO2" + # TODO: see above, _per_period_: bound_emission added for every single year. Does + # it work like this (for all at once), too? + scenario_period_bound.add_par("bound_emission", emiss_period_bound) + scenario_period_bound.commit("initialize test scenario for periodic emission bound") + scenario_period_bound.solve(quiet=True, **solve_args) # check -emissions are close between cumulative and yearly-bound scenarios - emiss_bnd = perbnd_scen.var("EMISS", filters).set_index("year").lvl - npt.assert_allclose(emiss, emiss_bnd) + emiss_period_bound = ( + scenario_period_bound.var("EMISS", filters).set_index("year").lvl + ) + npt.assert_allclose(emiss, emiss_period_bound) # check "PRICE_EMISSION" is close between cumulative- and yearly-bound scenarios - pemiss_bnd = perbnd_scen.var("PRICE_EMISSION", filters).set_index("year").lvl - npt.assert_allclose(pemiss, pemiss_bnd) + price_emission_period_bound = ( + scenario_period_bound.var("PRICE_EMISSION", filters).set_index("year").lvl + ) + npt.assert_allclose(price_emission, price_emission_period_bound) + + +# idea: try running the same with the westeros scenario +# Let's not do that, though, because make_westeros has the westeros years hardcoded +# Instead, try to ensure this model setup follows westeros as a sanity check +# @pytest.mark.parametrize( +# "cumulative_bound, years", +# [ +# (400, [2020, 2030, 2040, 2050]), +# (400, [2020, 2025, 2030, 2040, 2050]), +# (500, [2020, 2030, 2040, 2050]), +# (500, [2020, 2025, 2030, 2040, 2050]), +# (600, [2020, 2030, 2040, 2050]), +# (600, [2020, 2025, 2030, 2040, 2050]), +# ], +# ) +# def test_price_duality_westeros(test_mp, request, cumulative_bound, years): +# baseline = make_westeros( +# mp=test_mp, model_horizon=years, emissions=True, request=request +# ) +# year_df = baseline.vintage_and_active_years() +# vintage_years, act_years = year_df["year_vtg"], year_df["year_act"] +# country = "Westeros" +# test_mp.add_unit("MtCO2") +# emission_factor = make_df( +# "emission_factor", +# node_loc=country, +# year_vtg=vintage_years, +# year_act=act_years, +# mode="standard", +# unit="tCO2/kWa", +# technology="coal_ppl", +# emission="CO2", +# value=7.4, +# ) + +# # Define cumulate emission bound and solve that Scenario +# scen_cumulative_bound = baseline.clone( +# MODEL, +# "emission_bound", +# "introducing an upper bound on emissions", +# keep_solution=False, +# ) +# scen_cumulative_bound.check_out() +# scen_cumulative_bound.add_set("emission", "CO2") +# scen_cumulative_bound.add_cat("emission", "GHG", "CO2") +# scen_cumulative_bound.add_par("emission_factor", emission_factor) +# scen_cumulative_bound.add_par( +# "bound_emission", +# [country, "GHG", "all", "cumulative"], +# value=cumulative_bound, +# unit="MtCO2", +# ) +# scen_cumulative_bound.commit( +# comment="Introducing emissions and setting an upper bound" +# ) +# scen_cumulative_bound.set_as_default() +# scen_cumulative_bound.solve(quiet=True, **solve_args) + +# # ---------------------------------------------------------- +# # Run scenario with `tax_emission` based on `PRICE_EMISSION` +# # from cumulative constraint scenario. +# # ---------------------------------------------------------- +# scen_tax = baseline.clone( +# MODEL, +# "tax_emission", +# "introducing a fixed tax on emissions", +# keep_solution=False, +# ) +# scen_tax.check_out() +# scen_cumulative_bound.add_set("emission", "CO2") +# scen_cumulative_bound.add_cat("emission", "GHG", "CO2") +# scen_cumulative_bound.add_par("emission_factor", emission_factor) + +# # use emission prices from cumulative-constraint scenario as taxes +# taxes = scen_cumulative_bound.var("PRICE_EMISSION").rename( +# columns={"year": "type_year", "lvl": "value"} +# ) +# taxes["unit"] = "USD/tCO2" +# taxes["node"] = "node" +# scen_tax.add_par("tax_emission", taxes) +# scen_tax.commit("initialize test scenario for taxes") +# scen_tax.solve(quiet=True, **solve_args) +# # scen_tax.solve(**solve_args) + +# print(scen_cumulative_bound.var("PRICE_EMISSION")) +# print(scen_tax.var("PRICE_EMISSION")) +# print(scen_tax.par("tax_emission")) +# print(scen_cumulative_bound.var("EMISS")) +# print(scen_tax.var("EMISS")) + +# # check emissions are close between cumulative and tax scenarios +# filters = {"node": "World"} +# emiss = scen_cumulative_bound.var("EMISS", filters).set_index("year").lvl +# emiss_tax = scen_tax.var("EMISS", filters).set_index("year").lvl +# npt.assert_allclose(emiss, emiss_tax, rtol=0.05) + +# # check "PRICE_EMISSION" is close between cumulative and tax scenarios +# filters = {"node": "World"} +# price_emission = ( +# scen_cumulative_bound.var("PRICE_EMISSION", filters).set_index("year").lvl +# ) +# price_emission_tax = scen_tax.var("PRICE_EMISSION", filters).set_index("year").lvl +# npt.assert_allclose(price_emission, price_emission_tax) From ed3aca77806e38772b5a5361335cabf795a3b63d Mon Sep 17 00:00:00 2001 From: Fridolin Glatter Date: Thu, 16 May 2024 14:36:36 +0200 Subject: [PATCH 17/26] TEMPORARY Add notebook to troubleshoot test_price_duality --- message_ix/tests/feature_price_emission.ipynb | 327 ++++++++++++++++++ 1 file changed, 327 insertions(+) create mode 100644 message_ix/tests/feature_price_emission.ipynb diff --git a/message_ix/tests/feature_price_emission.ipynb b/message_ix/tests/feature_price_emission.ipynb new file mode 100644 index 000000000..2826007f0 --- /dev/null +++ b/message_ix/tests/feature_price_emission.ipynb @@ -0,0 +1,327 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from ixmp import Platform\n", + "\n", + "from message_ix import Scenario\n", + "\n", + "MODEL = \"test_emissions_price\"\n", + "\n", + "mp = Platform(\"test_feature_price_emission\")\n", + "mp.add_unit(\"MtCO2\")\n", + "mp.add_unit(\"tCO2/kWa\")\n", + "mp.add_unit(\"USD/kW\")\n", + "\n", + "scen = Scenario(\n", + " mp,\n", + " MODEL,\n", + " scenario=\"many_tecs\",\n", + " version=\"new\",\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from message_ix.tests.test_feature_price_emission import model_setup\n", + "\n", + "# 450 is too much; this bound will not affect the first model year\n", + "# (EMISS is not reduced from without cumulative bound to with it)\n", + "# In a model year without binding bound, no price_emission is produced ->\n", + "# there is one value missing for price_emission in period-specific bound\n", + "cumulative_bound = 1\n", + "# cumulative_bound = 500\n", + "# cumulative_bound = 550\n", + "years = [2020, 2030, 2040, 2050]\n", + "# years = [2020, 2025, 2030, 2040, 2050]\n", + "# years = [2020, 2030, 2040, 2045, 2050]\n", + "\n", + "filters = {\"node\": \"World\"}\n", + "\n", + "model_setup(scen=scen, years=years, simple_tecs=False)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from message_ix.tests.test_feature_price_emission import solve_args\n", + "\n", + "scen.commit(\"initialize test scenario\")\n", + "scen.solve(quiet=True, **solve_args)\n", + "scen.var(\"EMISS\", filters)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "scenario_cumulative_bound = scen.clone(\n", + " MODEL,\n", + " \"cumulative_emission_bound\",\n", + " \"introducing a cumulative emissions bound\",\n", + " keep_solution=False,\n", + ")\n", + "scenario_cumulative_bound.check_out()\n", + "\n", + "scenario_cumulative_bound.add_cat(\"year\", \"cumulative\", years)\n", + "scenario_cumulative_bound.add_par(\n", + " \"bound_emission\",\n", + " [\"World\", \"GHG\", \"all\", \"cumulative\"],\n", + " cumulative_bound,\n", + " \"MtCO2\",\n", + ")\n", + "scenario_cumulative_bound.commit(\"initialize test scenario\")\n", + "scenario_cumulative_bound.solve(quiet=True, **solve_args)\n", + "scenario_cumulative_bound.var(\"EMISS\", filters)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "emiss = scenario_cumulative_bound.var(\"EMISS\", filters).set_index(\"year\").lvl\n", + "price_emission = (\n", + " scenario_cumulative_bound.var(\"PRICE_EMISSION\", filters).set_index(\"year\").lvl\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# --------------------------------------------------------\n", + "# Run scenario with annual-emission bound based on `EMISS`\n", + "# from cumulative constraint scenario.\n", + "# --------------------------------------------------------\n", + "\n", + "scenario_period_bound = scen.clone(\n", + " MODEL,\n", + " \"period_bound_many_tecs\",\n", + " \"introducing a period-specific emission_bound\",\n", + " keep_solution=False,\n", + ")\n", + "scenario_period_bound.check_out()\n", + "for year in years:\n", + " scenario_period_bound.add_cat(\"year\", year, year)\n", + "\n", + "# use emissions from cumulative-constraint scenario as period-emission bounds\n", + "emiss_period_bound = (\n", + " scenario_cumulative_bound.var(\"EMISS\", {\"node\": \"World\"})\n", + " .rename(columns={\"year\": \"type_year\", \"lvl\": \"value\"})\n", + " .drop(\"emission\", axis=1)\n", + ")\n", + "emiss_period_bound[\"type_emission\"] = \"GHG\"\n", + "emiss_period_bound[\"unit\"] = \"MtCO2\"\n", + "scenario_period_bound.add_par(\"bound_emission\", emiss_period_bound)\n", + "scenario_period_bound.commit(\"initialize test scenario for periodic emission bound\")\n", + "scenario_period_bound.solve(quiet=True, **solve_args)\n", + "scenario_period_bound.var(\"EMISS\", filters)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy.testing as npt\n", + "\n", + "# check -emissions are close between cumulative and yearly-bound scenarios\n", + "emiss_period_bound = scenario_period_bound.var(\"EMISS\", filters).set_index(\"year\").lvl\n", + "npt.assert_allclose(emiss, emiss_period_bound)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "scenario_cumulative_bound.par(\"emission_factor\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(price_emission)\n", + "scenario_period_bound.var(\"PRICE_EMISSION\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# check \"PRICE_EMISSION\" is close between cumulative- and yearly-bound scenarios\n", + "price_emission_period_bound = (\n", + " scenario_period_bound.var(\"PRICE_EMISSION\", filters).set_index(\"year\").lvl\n", + ")\n", + "npt.assert_allclose(price_emission, price_emission_period_bound)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "scenario_cumulative_bound.par(\"bound_emission\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "scen_tax = Scenario(\n", + " mp,\n", + " MODEL,\n", + " scenario=\"tax_many_tecs\",\n", + " version=\"new\",\n", + ")\n", + "model_setup(scen_tax, years, simple_tecs=False)\n", + "for year in years:\n", + " scen_tax.add_cat(\"year\", year, year)\n", + "# use emission prices from cumulative-constraint scenario as taxes\n", + "taxes = scenario_cumulative_bound.var(\"PRICE_EMISSION\").rename(\n", + " columns={\"year\": \"type_year\", \"lvl\": \"value\"}\n", + ")\n", + "taxes[\"unit\"] = \"USD/tCO2\"\n", + "taxes[\"node\"] = \"node\"\n", + "scen_tax.add_par(\"tax_emission\", taxes)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "scen_tax.commit(\"initialize test scenario for taxes\")\n", + "scen_tax.solve(quiet=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(scenario_cumulative_bound.var(\"PRICE_EMISSION\"))\n", + "print(scen_tax.var(\"PRICE_EMISSION\"))\n", + "print(scen_tax.par(\"tax_emission\"))\n", + "print(scenario_cumulative_bound.var(\"EMISS\"))\n", + "print(scen_tax.var(\"EMISS\"))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(scenario_cumulative_bound.var(\"ACT\"))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(scen_tax.var(\"ACT\"))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "scenario_cumulative_bound.par(\"demand\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "scenario_cumulative_bound.equ(\"EMISSION_EQUIVALENCE\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "price_emission_tax = scen_tax.var(\"PRICE_EMISSION\").set_index(\"year\").lvl\n", + "npt.assert_allclose(price_emission, price_emission_tax)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# check emissions are close between cumulative and tax scenarios\n", + "emiss_tax = scen_tax.var(\"EMISS\", filters).set_index(\"year\").lvl\n", + "npt.assert_allclose(emiss, emiss_tax, rtol=0.05)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "mp.close_db()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "mix312", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} From 5bc2ba75911d3272b80e2be3588680c59e03ef29 Mon Sep 17 00:00:00 2001 From: Behnam Date: Mon, 20 May 2024 02:57:24 +0200 Subject: [PATCH 18/26] Improve test for different bound types --- .../tests/test_feature_price_emission.py | 36 +++++++++++-------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/message_ix/tests/test_feature_price_emission.py b/message_ix/tests/test_feature_price_emission.py index 959cf7d14..acc4d6e22 100644 --- a/message_ix/tests/test_feature_price_emission.py +++ b/message_ix/tests/test_feature_price_emission.py @@ -357,18 +357,22 @@ def test_custom_type_variable_periodlength(test_mp, request): npt.assert_allclose(obs, exp) +bound = 0.2 +cumulative = False +years = [2020, 2021, 2022, 2023] +tag = "yearly_" + str(bound) + "_equal" @pytest.mark.parametrize( - "cumulative_bound, years", + "bound, cumulative, years, tag", [ - (2.5, [2020, 2030, 2040, 2050]), - (0.25, [2020, 2025, 2030, 2040, 2050]), - (0.50, [2020, 2030, 2040, 2050]), - (0.50, [2020, 2025, 2030, 2040, 2050]), - (0.75, [2020, 2030, 2040, 2050]), - (0.75, [2020, 2025, 2030, 2040, 2050]), + (0.25, True, [2020, 2030, 2040, 2050], "0.25_equal"), + (0.25, True, [2020, 2025, 2030, 2040, 2050], "0.25_varying"), + (0.50, True, [2020, 2030, 2040, 2050], "0.5_equal"), + (0.50, True, [2020, 2025, 2030, 2040, 2050], "0.5_varying"), + (0.75, True, [2020, 2030, 2040, 2050], "0.75_equal"), + (0.75, True, [2020, 2025, 2030, 2040, 2050], "0.75_varying"), ], ) -def test_price_duality(test_mp, request, cumulative_bound, years): +def test_price_duality(test_mp, request, bound, cumulative, years, tag): # set up a scenario for cumulative constraints scen = Scenario( test_mp, @@ -377,13 +381,15 @@ def test_price_duality(test_mp, request, cumulative_bound, years): version="new", ) model_setup(scen, years, simple_tecs=False) - scen.add_cat("year", "cumulative", years) - scen.add_par( - "bound_emission", - ["World", "GHG", "all", "cumulative"], - cumulative_bound, - "tCO2", - ) + if cumulative: + scen.add_cat("year", "cumulative", years) + scen.add_par( + "bound_emission", ["World", "ghg", "all", "cumulative"], bound, "tCO2", + ) + else: + for y in years: + scen.add_cat("year", y, y) + scen.add_par("bound_emission", ["World", "ghg", "all", y], bound, "tCO2") scen.commit("initialize test scenario") scen.solve(quiet=True, **solve_args) From 15a70d5f1c503c5f2e819f8f77dffe5703e42a6f Mon Sep 17 00:00:00 2001 From: Fridolin Glatter Date: Wed, 22 May 2024 15:04:35 +0200 Subject: [PATCH 19/26] Remove unused tag parameter --- .../tests/test_feature_price_emission.py | 94 ++++++++++--------- 1 file changed, 51 insertions(+), 43 deletions(-) diff --git a/message_ix/tests/test_feature_price_emission.py b/message_ix/tests/test_feature_price_emission.py index acc4d6e22..a8e4a3e11 100644 --- a/message_ix/tests/test_feature_price_emission.py +++ b/message_ix/tests/test_feature_price_emission.py @@ -1,6 +1,9 @@ +from typing import List + import numpy.testing as npt import pytest +# from message_ix.testing import make_westeros from message_ix import Scenario, make_df MODEL = "test_emissions_price" @@ -16,7 +19,7 @@ interest_rate = 0.05 -def model_setup(scen: Scenario, years: list[int], simple_tecs=True) -> None: +def model_setup(scen: Scenario, years: List[int], simple_tecs=True) -> None: """generate a minimal model to test the behaviour of the emission prices""" scen.add_spatial_sets({"country": "node"}) scen.add_set("commodity", "comm") @@ -45,52 +48,44 @@ def model_setup(scen: Scenario, years: list[int], simple_tecs=True) -> None: add_two_tecs(scen, years) if simple_tecs else add_many_tecs(scen, years) -def add_two_tecs(scen: Scenario, years: list[int]) -> None: +def add_two_tecs(scen: Scenario, years: List[int]) -> None: """add two technologies to the scenario""" scen.add_set("technology", ["dirty_tec", "clean_tec"]) - common = dict(node_loc="node", year_vtg=years, year_act=years, value=1, mode="mode") - output_specs = ["node", "comm", "level", "year", "year"] - - for y in years: - # the dirty technology is free (no costs) but has emissions - tec_specs = ["node", "dirty_tec", y, y, "mode"] - scen.add_par("output", tec_specs + output_specs, 1, "GWa") - scen.add_par("emission_factor", tec_specs + ["CO2"], 1, "tCO2") + common_base = dict( + node_loc="node", year_vtg=years, year_act=years, mode="mode", value=1 + ) + common_output = dict( + node_dest="node", + commodity="comm", + level="level", + time="year", + time_dest="year", + unit="GWa", + ) - # the clean technology has variable costs but no emissions - tec_specs = ["node", "clean_tec", y, y, "mode"] - scen.add_par("output", tec_specs + output_specs, 1, "GWa") - scen.add_par("var_cost", tec_specs + ["year"], 1, "USD/GWa") + # the dirty technology is free (no costs) but has emissions + scen.add_par( + "output", + make_df("output", technology="dirty_tec", **common_base, **common_output), + ) + scen.add_par( + "emission_factor", + make_df("emission_factor", technology="dirty_tec", emission="CO2", unit="tCO2"), + ) # the clean technology has variable costs but no emissions scen.add_par( "output", - make_df( - "output", - node_dest="node", - technology="clean_tec", - commodity="comm", - level="level", - time="year", - time_dest="year", - unit="GWa", - **common, - ), + make_df("output", technology="clean_tec", **common_base, **common_output), ) scen.add_par( "var_cost", - make_df( - "var_cost", - time="year", - unit="USD/GWa", - technology="clean_tec", - **common, - ), + make_df("var_cost", technology="clean_tec", time="year", unit="USD/GWa"), ) -def add_many_tecs(scen: Scenario, years: list[int], n=50) -> None: +def add_many_tecs(scen: Scenario, years: List[int], n=50) -> None: """add a range of dirty-to-clean technologies to the scenario""" # Add some hardcoded tecs for temporary testing tecs: dict[str, dict] = { @@ -361,18 +356,20 @@ def test_custom_type_variable_periodlength(test_mp, request): cumulative = False years = [2020, 2021, 2022, 2023] tag = "yearly_" + str(bound) + "_equal" + + @pytest.mark.parametrize( - "bound, cumulative, years, tag", + "bound, cumulative, years", [ - (0.25, True, [2020, 2030, 2040, 2050], "0.25_equal"), - (0.25, True, [2020, 2025, 2030, 2040, 2050], "0.25_varying"), - (0.50, True, [2020, 2030, 2040, 2050], "0.5_equal"), - (0.50, True, [2020, 2025, 2030, 2040, 2050], "0.5_varying"), - (0.75, True, [2020, 2030, 2040, 2050], "0.75_equal"), - (0.75, True, [2020, 2025, 2030, 2040, 2050], "0.75_varying"), + (0.25, True, [2020, 2030, 2040, 2050]), + (0.25, True, [2020, 2025, 2030, 2040, 2050]), + (0.50, True, [2020, 2030, 2040, 2050]), + (0.50, True, [2020, 2025, 2030, 2040, 2050]), + (0.75, True, [2020, 2030, 2040, 2050]), + (0.75, True, [2020, 2025, 2030, 2040, 2050]), ], ) -def test_price_duality(test_mp, request, bound, cumulative, years, tag): +def test_price_duality(test_mp, request, bound, cumulative, years): # set up a scenario for cumulative constraints scen = Scenario( test_mp, @@ -381,15 +378,26 @@ def test_price_duality(test_mp, request, bound, cumulative, years, tag): version="new", ) model_setup(scen, years, simple_tecs=False) + bound_emission_base = dict( + node="World", type_emission="ghg", type_tec="all", value=bound, unit="tCO2" + ) if cumulative: scen.add_cat("year", "cumulative", years) scen.add_par( - "bound_emission", ["World", "ghg", "all", "cumulative"], bound, "tCO2", + "bound_emission", + make_df( + "bound_emission", + type_year="cumulative", + **bound_emission_base, + ), ) else: for y in years: scen.add_cat("year", y, y) - scen.add_par("bound_emission", ["World", "ghg", "all", y], bound, "tCO2") + scen.add_par( + "bound_emission", + make_df("bound_emission", type_year=y, **bound_emission_base), + ) scen.commit("initialize test scenario") scen.solve(quiet=True, **solve_args) From 05d3aeac3f4af65078b118aa3d72c3925f044169 Mon Sep 17 00:00:00 2001 From: Fridolin Glatter Date: Tue, 17 Sep 2024 11:47:41 +0200 Subject: [PATCH 20/26] Create units missing on test platforms --- .../tests/test_feature_price_emission.py | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/message_ix/tests/test_feature_price_emission.py b/message_ix/tests/test_feature_price_emission.py index a8e4a3e11..60df29b3d 100644 --- a/message_ix/tests/test_feature_price_emission.py +++ b/message_ix/tests/test_feature_price_emission.py @@ -71,7 +71,13 @@ def add_two_tecs(scen: Scenario, years: List[int]) -> None: ) scen.add_par( "emission_factor", - make_df("emission_factor", technology="dirty_tec", emission="CO2", unit="tCO2"), + make_df( + "emission_factor", + technology="dirty_tec", + emission="CO2", + unit="tCO2", + **common_base, + ), ) # the clean technology has variable costs but no emissions @@ -81,7 +87,13 @@ def add_two_tecs(scen: Scenario, years: List[int]) -> None: ) scen.add_par( "var_cost", - make_df("var_cost", technology="clean_tec", time="year", unit="USD/GWa"), + make_df( + "var_cost", + technology="clean_tec", + time="year", + unit="USD/GWa", + **common_base, + ), ) @@ -132,6 +144,9 @@ def add_many_tecs(scen: Scenario, years: List[int], n=50) -> None: } year_df = scen.vintage_and_active_years() vintage_years, act_years = year_df["year_vtg"], year_df["year_act"] + mp = scen.platform + mp.add_unit("USD/kW") + mp.add_unit("tCO2/kWa") for t in tecs: scen.add_set("technology", t) @@ -379,7 +394,7 @@ def test_price_duality(test_mp, request, bound, cumulative, years): ) model_setup(scen, years, simple_tecs=False) bound_emission_base = dict( - node="World", type_emission="ghg", type_tec="all", value=bound, unit="tCO2" + node="World", type_emission="GHG", type_tec="all", value=bound, unit="tCO2" ) if cumulative: scen.add_cat("year", "cumulative", years) From 3e59e8d7f1514a3bbec211f005c2eb21c00deb3e Mon Sep 17 00:00:00 2001 From: Fridolin Glatter Date: Wed, 18 Sep 2024 11:42:20 +0200 Subject: [PATCH 21/26] Revert unjustified latex re-formatting --- message_ix/model/MESSAGE/model_solve.gms | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/message_ix/model/MESSAGE/model_solve.gms b/message_ix/model/MESSAGE/model_solve.gms index 594f9cc86..b354e419c 100644 --- a/message_ix/model/MESSAGE/model_solve.gms +++ b/message_ix/model/MESSAGE/model_solve.gms @@ -14,7 +14,7 @@ if (%foresight% = 0, * This is the standard option; the GAMS global variable ``%foresight%=0`` by default. * * .. math:: -* \min_x OBJ = \sum_{y \in Y} OBJ_y(x_y) +* \min_x \text{OBJ} = \sum_{y \in Y} \text{OBJ}_y(x_y) *** * reset year in case it was set by MACRO to include the base year before @@ -75,10 +75,10 @@ else * Loop over :math:`\hat{y} \in Y`, solving * * .. math:: -* \min_x \ OBJ = \sum_{y \in \hat{Y}(\hat{y})} OBJ_y(x_y) \\ +* \min_x \ \text{OBJ} = \sum_{y \in \hat{Y}(\hat{y})} \text{OBJ}_y(x_y) \\ * \text{s.t. } x_{y'} = x_{y'}^* \quad \forall \ y' < y * -* where :math:`\hat{Y}(\hat{y}) = \{y \in Y | \ |\hat{y}| - |y| < optimization\_horizon \}` and +* where :math:`\hat{Y}(\hat{y}) = \{y \in Y | \ |\hat{y}| - |y| < \text{optimization_horizon} \}` and * :math:`x_{y'}^*` is the optimal value of :math:`x_{y'}` in iteration :math:`|y'|` of the iterative loop. * * The advantage of this implementation is that there is no need to 'store' the optimal values of all decision From 3eef99827193670dd499e57997e0d87e304bb336 Mon Sep 17 00:00:00 2001 From: Fridolin Glatter Date: Fri, 20 Sep 2024 12:59:27 +0200 Subject: [PATCH 22/26] Reintroduce rescaling of EMISSION_CONSTRAINT --- message_ix/model/MESSAGE/model_solve.gms | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/message_ix/model/MESSAGE/model_solve.gms b/message_ix/model/MESSAGE/model_solve.gms index b354e419c..93cdacd47 100644 --- a/message_ix/model/MESSAGE/model_solve.gms +++ b/message_ix/model/MESSAGE/model_solve.gms @@ -39,7 +39,14 @@ if (%foresight% = 0, ABORT "MESSAGEix did not solve to optimality!" ) ; -* assign auxiliary variables DEMAND for integration with MACRO +* rescale the dual of the emission constraint to account that the constraint is defined on the average year, not total +EMISSION_CONSTRAINT.m(node,type_emission,type_tec,type_year)$( + EMISSION_CONSTRAINT.m(node,type_emission,type_tec,type_year) ) = + EMISSION_CONSTRAINT.m(node,type_emission,type_tec,type_year) + / SUM(year$( cat_year(type_year,year) ), duration_period(year) ) + * SUM(year$( map_first_period(type_year,year) ), duration_period(year) / df_period(year) * df_year(year) ); + +* assign auxiliary variable DEMAND for integration with MACRO DEMAND.l(node,commodity,level,year,time) = demand_fixed(node,commodity,level,year,time) ; * assign auxiliary variables PRICE_COMMODITY and PRICE_EMISSION for reporting From cdcdf9d86c18b03bc2118ee00c8025f0b8c95146 Mon Sep 17 00:00:00 2001 From: Fridolin Glatter Date: Fri, 20 Sep 2024 13:00:24 +0200 Subject: [PATCH 23/26] Adjust TEMPORARY debugging-notebook to westeros-inspired setup --- message_ix/tests/feature_price_emission.ipynb | 97 ++++++++++++++++--- 1 file changed, 82 insertions(+), 15 deletions(-) diff --git a/message_ix/tests/feature_price_emission.ipynb b/message_ix/tests/feature_price_emission.ipynb index 2826007f0..b0fcfea8b 100644 --- a/message_ix/tests/feature_price_emission.ipynb +++ b/message_ix/tests/feature_price_emission.ipynb @@ -22,12 +22,18 @@ " MODEL,\n", " scenario=\"many_tecs\",\n", " version=\"new\",\n", - ")" + ")\n", + "# scen = Scenario(\n", + "# mp,\n", + "# MODEL,\n", + "# scenario=\"two_tecs\",\n", + "# version=\"new\",\n", + "# )" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -37,16 +43,17 @@ "# (EMISS is not reduced from without cumulative bound to with it)\n", "# In a model year without binding bound, no price_emission is produced ->\n", "# there is one value missing for price_emission in period-specific bound\n", - "cumulative_bound = 1\n", + "cumulative_bound = 40\n", "# cumulative_bound = 500\n", "# cumulative_bound = 550\n", - "years = [2020, 2030, 2040, 2050]\n", - "# years = [2020, 2025, 2030, 2040, 2050]\n", + "# years = [2020, 2030, 2040, 2050]\n", + "years = [2020, 2025, 2030, 2040, 2045, 2050]\n", "# years = [2020, 2030, 2040, 2045, 2050]\n", "\n", "filters = {\"node\": \"World\"}\n", "\n", - "model_setup(scen=scen, years=years, simple_tecs=False)" + "model_setup(scen=scen, years=years, simple_tecs=False)\n", + "# model_setup(scen=scen, years=years)" ] }, { @@ -74,6 +81,12 @@ " \"introducing a cumulative emissions bound\",\n", " keep_solution=False,\n", ")\n", + "# scenario_cumulative_bound = scen.clone(\n", + "# MODEL,\n", + "# \"cumulative_emission_bound_two_tecs\",\n", + "# \"introducing a cumulative emissions bound\",\n", + "# keep_solution=False,\n", + "# )\n", "scenario_cumulative_bound.check_out()\n", "\n", "scenario_cumulative_bound.add_cat(\"year\", \"cumulative\", years)\n", @@ -90,7 +103,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "metadata": {}, "outputs": [], "source": [ @@ -117,6 +130,12 @@ " \"introducing a period-specific emission_bound\",\n", " keep_solution=False,\n", ")\n", + "# scenario_period_bound = scen.clone(\n", + "# MODEL,\n", + "# \"period_bound_two_tecs\",\n", + "# \"introducing a period-specific emission_bound\",\n", + "# keep_solution=False,\n", + "# )\n", "scenario_period_bound.check_out()\n", "for year in years:\n", " scenario_period_bound.add_cat(\"year\", year, year)\n", @@ -137,7 +156,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "metadata": {}, "outputs": [], "source": [ @@ -154,7 +173,7 @@ "metadata": {}, "outputs": [], "source": [ - "scenario_cumulative_bound.par(\"emission_factor\")" + "scenario_cumulative_bound.equ(\"EMISSION_EQUIVALENCE\")" ] }, { @@ -180,6 +199,16 @@ "npt.assert_allclose(price_emission, price_emission_period_bound)" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "scenario_period_bound.equ(\"EMISSION_EQUIVALENCE\")\n", + "# NOTE: mrg == df_period for two_tec" + ] + }, { "cell_type": "code", "execution_count": null, @@ -194,6 +223,15 @@ "execution_count": null, "metadata": {}, "outputs": [], + "source": [ + "scenario_period_bound.par(\"bound_emission\")" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [], "source": [ "scen_tax = Scenario(\n", " mp,\n", @@ -201,7 +239,15 @@ " scenario=\"tax_many_tecs\",\n", " version=\"new\",\n", ")\n", + "# scen_tax = Scenario(\n", + "# mp,\n", + "# MODEL,\n", + "# scenario=\"tax_two_tecs\",\n", + "# version=\"new\",\n", + "# )\n", "model_setup(scen_tax, years, simple_tecs=False)\n", + "# model_setup(scen_tax, years)\n", + "\n", "for year in years:\n", " scen_tax.add_cat(\"year\", year, year)\n", "# use emission prices from cumulative-constraint scenario as taxes\n", @@ -231,8 +277,17 @@ "source": [ "print(scenario_cumulative_bound.var(\"PRICE_EMISSION\"))\n", "print(scen_tax.var(\"PRICE_EMISSION\"))\n", - "print(scen_tax.par(\"tax_emission\"))\n", + "print(scen_tax.par(\"tax_emission\"))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ "print(scenario_cumulative_bound.var(\"EMISS\"))\n", + "# print(scenario_period_bound.var(\"EMISS\"))\n", "print(scen_tax.var(\"EMISS\"))" ] }, @@ -242,7 +297,8 @@ "metadata": {}, "outputs": [], "source": [ - "print(scenario_cumulative_bound.var(\"ACT\"))" + "cumulative_activity = scenario_cumulative_bound.var(\"ACT\")\n", + "print(cumulative_activity.loc[cumulative_activity.lvl > 0])" ] }, { @@ -251,7 +307,8 @@ "metadata": {}, "outputs": [], "source": [ - "print(scen_tax.var(\"ACT\"))" + "period_activity = scenario_period_bound.var(\"ACT\")\n", + "print(period_activity.loc[period_activity.lvl > 0])" ] }, { @@ -260,7 +317,8 @@ "metadata": {}, "outputs": [], "source": [ - "scenario_cumulative_bound.par(\"demand\")" + "tax_activity = scen_tax.var(\"ACT\")\n", + "print(tax_activity.loc[tax_activity.lvl > 0])" ] }, { @@ -269,7 +327,7 @@ "metadata": {}, "outputs": [], "source": [ - "scenario_cumulative_bound.equ(\"EMISSION_EQUIVALENCE\")" + "scen_tax.par(\"bound_emission\")" ] }, { @@ -277,6 +335,15 @@ "execution_count": null, "metadata": {}, "outputs": [], + "source": [ + "scenario_cumulative_bound.par(\"demand\")" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [], "source": [ "price_emission_tax = scen_tax.var(\"PRICE_EMISSION\").set_index(\"year\").lvl\n", "npt.assert_allclose(price_emission, price_emission_tax)" @@ -295,7 +362,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 27, "metadata": {}, "outputs": [], "source": [ From 121c6e1d27d4b2655d14f264c63339c88e30c28d Mon Sep 17 00:00:00 2001 From: Fridolin Glatter Date: Fri, 20 Sep 2024 13:01:00 +0200 Subject: [PATCH 24/26] Use westeros-inspired values for many_tec test setup --- .../tests/test_feature_price_emission.py | 202 +++++------------- 1 file changed, 48 insertions(+), 154 deletions(-) diff --git a/message_ix/tests/test_feature_price_emission.py b/message_ix/tests/test_feature_price_emission.py index 60df29b3d..be9d23b11 100644 --- a/message_ix/tests/test_feature_price_emission.py +++ b/message_ix/tests/test_feature_price_emission.py @@ -102,7 +102,7 @@ def add_many_tecs(scen: Scenario, years: List[int], n=50) -> None: # Add some hardcoded tecs for temporary testing tecs: dict[str, dict] = { "tec1": { - "emission_factor": 10, + "emission_factor": 7.4, "inv_cost": 500, "fix_cost": 30, "var_cost": 30, @@ -110,35 +110,35 @@ def add_many_tecs(scen: Scenario, years: List[int], n=50) -> None: "lifetime": 20, }, "tec2": { - "emission_factor": -10, - "inv_cost": 1200, + "emission_factor": 0, + "inv_cost": 1500, "fix_cost": 10, "var_cost": 0, - "bound_activity_up": 40, + "bound_activity_up": 100, "lifetime": 20, }, "tec3": { - "emission_factor": -12, - "inv_cost": 1300, - "fix_cost": 12, - "var_cost": 0, - "bound_activity_up": 30, + "emission_factor": 2.5, + "inv_cost": 700, + "fix_cost": 30, + "var_cost": 30, + "bound_activity_up": 1000, "lifetime": 20, }, "tec4": { - "emission_factor": -14, - "inv_cost": 1400, - "fix_cost": 14, + "emission_factor": 0, + "inv_cost": 2000, + "fix_cost": 5, "var_cost": 0, - "bound_activity_up": 20, - "lifetime": 20, + "bound_activity_up": 10, + "lifetime": 40, }, "tec5": { - "emission_factor": -16, - "inv_cost": 1500, - "fix_cost": 16, - "var_cost": 0, - "bound_activity_up": 10, + "emission_factor": 10, + "inv_cost": 400, + "fix_cost": 30, + "var_cost": 30, + "bound_activity_up": 100, "lifetime": 20, }, } @@ -190,7 +190,7 @@ def add_many_tecs(scen: Scenario, years: List[int], n=50) -> None: time="year", unit="USD/kWa", technology=t, - value=tecs[t]["fix_cost"], + value=tecs[t]["var_cost"], ), ) scen.add_par( @@ -367,24 +367,20 @@ def test_custom_type_variable_periodlength(test_mp, request): npt.assert_allclose(obs, exp) -bound = 0.2 -cumulative = False -years = [2020, 2021, 2022, 2023] -tag = "yearly_" + str(bound) + "_equal" - - +# TODO as per Oliver's description: +# - Only need cumulative (no need to parametrize) @pytest.mark.parametrize( - "bound, cumulative, years", + "bound, years", [ - (0.25, True, [2020, 2030, 2040, 2050]), - (0.25, True, [2020, 2025, 2030, 2040, 2050]), - (0.50, True, [2020, 2030, 2040, 2050]), - (0.50, True, [2020, 2025, 2030, 2040, 2050]), - (0.75, True, [2020, 2030, 2040, 2050]), - (0.75, True, [2020, 2025, 2030, 2040, 2050]), + (10, [2010, 2020, 2025, 2030, 2035, 2040, 2050]), + (10, [2010, 2015, 2020, 2030, 2040, 2045, 2050]), + (40, [2010, 2020, 2025, 2030, 2035, 2040, 2050]), + (40, [2010, 2015, 2020, 2030, 2040, 2045, 2050]), + (75, [2010, 2020, 2025, 2030, 2035, 2040, 2050]), + (75, [2010, 2015, 2020, 2030, 2040, 2045, 2050]), ], ) -def test_price_duality(test_mp, request, bound, cumulative, years): +def test_price_duality(test_mp, request, bound, years): # set up a scenario for cumulative constraints scen = Scenario( test_mp, @@ -396,23 +392,16 @@ def test_price_duality(test_mp, request, bound, cumulative, years): bound_emission_base = dict( node="World", type_emission="GHG", type_tec="all", value=bound, unit="tCO2" ) - if cumulative: - scen.add_cat("year", "cumulative", years) - scen.add_par( + # We use a `cumulative` base + scen.add_cat("year", "cumulative", years) + scen.add_par( + "bound_emission", + make_df( "bound_emission", - make_df( - "bound_emission", - type_year="cumulative", - **bound_emission_base, - ), - ) - else: - for y in years: - scen.add_cat("year", y, y) - scen.add_par( - "bound_emission", - make_df("bound_emission", type_year=y, **bound_emission_base), - ) + type_year="cumulative", + **bound_emission_base, + ), + ) scen.commit("initialize test scenario") scen.solve(quiet=True, **solve_args) @@ -427,6 +416,8 @@ def test_price_duality(test_mp, request, bound, cumulative, years): version="new", ) model_setup(scen_tax, years, simple_tecs=False) + # TODO Why do we do this? We applied the bound as cumulative before, but the tax + # should be per-period? for year in years: scen_tax.add_cat("year", year, year) @@ -435,6 +426,7 @@ def test_price_duality(test_mp, request, bound, cumulative, years): columns={"year": "type_year", "lvl": "value"} ) taxes["unit"] = "USD/tCO2" + # TODO Check: bound is on "World", taxes are on "node" taxes["node"] = "node" scen_tax.add_par("tax_emission", taxes) scen_tax.commit("initialize test scenario for taxes") @@ -459,6 +451,12 @@ def test_price_duality(test_mp, request, bound, cumulative, years): price_emission_tax = scen_tax.var("PRICE_EMISSION", filters).set_index("year").lvl npt.assert_allclose(price_emission, price_emission_tax) + # check "tax_emission" and "PRICE_EMISSION" are equal in tax-scenario + filters = {"node": "World"} + tax_emission = scen_tax.par("tax_emission", filters).set_index("type_year").value + price_emission_tax = scen_tax.var("PRICE_EMISSION", filters).set_index("year").lvl + npt.assert_allclose(tax_emission, price_emission_tax) + # -------------------------------------------------------- # Run scenario with annual-emission bound based on `EMISS` # from cumulative constraint scenario. @@ -499,107 +497,3 @@ def test_price_duality(test_mp, request, bound, cumulative, years): scenario_period_bound.var("PRICE_EMISSION", filters).set_index("year").lvl ) npt.assert_allclose(price_emission, price_emission_period_bound) - - -# idea: try running the same with the westeros scenario -# Let's not do that, though, because make_westeros has the westeros years hardcoded -# Instead, try to ensure this model setup follows westeros as a sanity check -# @pytest.mark.parametrize( -# "cumulative_bound, years", -# [ -# (400, [2020, 2030, 2040, 2050]), -# (400, [2020, 2025, 2030, 2040, 2050]), -# (500, [2020, 2030, 2040, 2050]), -# (500, [2020, 2025, 2030, 2040, 2050]), -# (600, [2020, 2030, 2040, 2050]), -# (600, [2020, 2025, 2030, 2040, 2050]), -# ], -# ) -# def test_price_duality_westeros(test_mp, request, cumulative_bound, years): -# baseline = make_westeros( -# mp=test_mp, model_horizon=years, emissions=True, request=request -# ) -# year_df = baseline.vintage_and_active_years() -# vintage_years, act_years = year_df["year_vtg"], year_df["year_act"] -# country = "Westeros" -# test_mp.add_unit("MtCO2") -# emission_factor = make_df( -# "emission_factor", -# node_loc=country, -# year_vtg=vintage_years, -# year_act=act_years, -# mode="standard", -# unit="tCO2/kWa", -# technology="coal_ppl", -# emission="CO2", -# value=7.4, -# ) - -# # Define cumulate emission bound and solve that Scenario -# scen_cumulative_bound = baseline.clone( -# MODEL, -# "emission_bound", -# "introducing an upper bound on emissions", -# keep_solution=False, -# ) -# scen_cumulative_bound.check_out() -# scen_cumulative_bound.add_set("emission", "CO2") -# scen_cumulative_bound.add_cat("emission", "GHG", "CO2") -# scen_cumulative_bound.add_par("emission_factor", emission_factor) -# scen_cumulative_bound.add_par( -# "bound_emission", -# [country, "GHG", "all", "cumulative"], -# value=cumulative_bound, -# unit="MtCO2", -# ) -# scen_cumulative_bound.commit( -# comment="Introducing emissions and setting an upper bound" -# ) -# scen_cumulative_bound.set_as_default() -# scen_cumulative_bound.solve(quiet=True, **solve_args) - -# # ---------------------------------------------------------- -# # Run scenario with `tax_emission` based on `PRICE_EMISSION` -# # from cumulative constraint scenario. -# # ---------------------------------------------------------- -# scen_tax = baseline.clone( -# MODEL, -# "tax_emission", -# "introducing a fixed tax on emissions", -# keep_solution=False, -# ) -# scen_tax.check_out() -# scen_cumulative_bound.add_set("emission", "CO2") -# scen_cumulative_bound.add_cat("emission", "GHG", "CO2") -# scen_cumulative_bound.add_par("emission_factor", emission_factor) - -# # use emission prices from cumulative-constraint scenario as taxes -# taxes = scen_cumulative_bound.var("PRICE_EMISSION").rename( -# columns={"year": "type_year", "lvl": "value"} -# ) -# taxes["unit"] = "USD/tCO2" -# taxes["node"] = "node" -# scen_tax.add_par("tax_emission", taxes) -# scen_tax.commit("initialize test scenario for taxes") -# scen_tax.solve(quiet=True, **solve_args) -# # scen_tax.solve(**solve_args) - -# print(scen_cumulative_bound.var("PRICE_EMISSION")) -# print(scen_tax.var("PRICE_EMISSION")) -# print(scen_tax.par("tax_emission")) -# print(scen_cumulative_bound.var("EMISS")) -# print(scen_tax.var("EMISS")) - -# # check emissions are close between cumulative and tax scenarios -# filters = {"node": "World"} -# emiss = scen_cumulative_bound.var("EMISS", filters).set_index("year").lvl -# emiss_tax = scen_tax.var("EMISS", filters).set_index("year").lvl -# npt.assert_allclose(emiss, emiss_tax, rtol=0.05) - -# # check "PRICE_EMISSION" is close between cumulative and tax scenarios -# filters = {"node": "World"} -# price_emission = ( -# scen_cumulative_bound.var("PRICE_EMISSION", filters).set_index("year").lvl -# ) -# price_emission_tax = scen_tax.var("PRICE_EMISSION", filters).set_index("year").lvl -# npt.assert_allclose(price_emission, price_emission_tax) From 28501f153e868817694d69581dabbea3770396ae Mon Sep 17 00:00:00 2001 From: Fridolin Glatter Date: Mon, 23 Sep 2024 12:30:36 +0200 Subject: [PATCH 25/26] Make explanatory comment more concise --- message_ix/model/MESSAGE/data_load.gms | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/message_ix/model/MESSAGE/data_load.gms b/message_ix/model/MESSAGE/data_load.gms index 1f094303b..d31306c54 100644 --- a/message_ix/model/MESSAGE/data_load.gms +++ b/message_ix/model/MESSAGE/data_load.gms @@ -169,7 +169,7 @@ addon_up(node,tec,year_all,mode,time,type_addon)$( AND map_tec_act(node,tec,year_all,mode,time) AND NOT addon_up(node,tec,year_all,mode,time,type_addon) ) = 1 ; -* set the emission scaling parameter to 1 if only one emission is included in a category +* set the emission scaling parameter to 1 by default emission_scaling(type_emission,emission)$( cat_emission(type_emission,emission) and not emission_scaling(type_emission,emission) ) = 1 ; From da69c94308c93dd89fc97678a0359afbc7198a89 Mon Sep 17 00:00:00 2001 From: Fridolin Glatter Date: Wed, 22 Jan 2025 11:46:09 +0100 Subject: [PATCH 26/26] Revert unmotivated comment reformating --- message_ix/model/MESSAGE/model_core.gms | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/message_ix/model/MESSAGE/model_core.gms b/message_ix/model/MESSAGE/model_core.gms index 8d18913a6..e0e719b50 100644 --- a/message_ix/model/MESSAGE/model_core.gms +++ b/message_ix/model/MESSAGE/model_core.gms @@ -131,15 +131,15 @@ Variables * * Auxiliary variables * ^^^^^^^^^^^^^^^^^^^ -* ======================================================================================= ======================================================================================================= -* Variable Explanatory text -* ======================================================================================= ======================================================================================================= -* :math:`\text{DEMAND}_{n,c,l,y,h} \in \mathbb{R}` Demand level (in equilibrium with MACRO integration) -* :math:`\text{PRICE_COMMODITY}_{n,c,l,y,h} \in \mathbb{R}` Commodity price (undiscounted marginals of :ref:`commodity_balance_gt` and :ref:`commodity_balance_lt`) -* :math:`\text{PRICE_EMISSION}_{n,\widehat{e},\widehat{t},y} \in \mathbb{R}` Emission price (undiscounted marginals of :ref:`emission_equivalence`) -* :math:`\text{COST_NODAL_NET}_{n,y} \in \mathbb{R}` System costs at the node level net of energy trade revenues/cost -* :math:`\text{GDP}_{n,y} \in \mathbb{R}` Gross domestic product (GDP) in market exchange rates for MACRO reporting -* ======================================================================================= ======================================================================================================= +* =========================================================================== ======================================================================================================= +* Variable Explanatory text +* =========================================================================== ======================================================================================================= +* :math:`\text{DEMAND}_{n,c,l,y,h} \in \mathbb{R}` Demand level (in equilibrium with MACRO integration) +* :math:`\text{PRICE_COMMODITY}_{n,c,l,y,h} \in \mathbb{R}` Commodity price (undiscounted marginals of :ref:`commodity_balance_gt` and :ref:`commodity_balance_lt`) +* :math:`\text{PRICE_EMISSION}_{n,\widehat{e},\widehat{t},y} \in \mathbb{R}` Emission price (undiscounted marginals of :ref:`emission_equivalence`) +* :math:`\text{COST_NODAL_NET}_{n,y} \in \mathbb{R}` System costs at the node level net of energy trade revenues/cost +* :math:`\text{GDP}_{n,y} \in \mathbb{R}` Gross domestic product (GDP) in market exchange rates for MACRO reporting +* =========================================================================== ======================================================================================================= *** Variables