From 3da48c737c0ad0e1c5aaa32716af2600d90c5705 Mon Sep 17 00:00:00 2001 From: Matthew Gidden Date: Wed, 26 Jun 2019 15:28:39 +0900 Subject: [PATCH 01/70] update macro init for vars and equs --- message_ix/macro.py | 18 +++++++++++++----- message_ix/tests/test_macro.py | 4 ++++ 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/message_ix/macro.py b/message_ix/macro.py index cefff3386..be250862d 100644 --- a/message_ix/macro.py +++ b/message_ix/macro.py @@ -54,8 +54,16 @@ def init(s): s.init_set(*args) for args in MACRO_INIT['pars']: s.init_par(*args) - # for args in MACRO_INIT['vars']: - # print(args) - # s.init_var(*args) - # for args in MACRO_INIT['equs']: - # s.init_equ(*args) + for args in MACRO_INIT['vars']: + if not s.has_var(args[0]): + try: + # TODO: this seems required because for some reason DEMAND (and + # perhaps others) seem to already be listed in the java code, + # but still needs to be initialized in the python code. However, + # you cannot init it with dimensions, only with the variable + # name. + s.init_var(*args) + except: + s.init_var(args[0]) + for args in MACRO_INIT['equs']: + s.init_equ(*args) diff --git a/message_ix/tests/test_macro.py b/message_ix/tests/test_macro.py index d463bc2c6..83164bbe8 100644 --- a/message_ix/tests/test_macro.py +++ b/message_ix/tests/test_macro.py @@ -14,3 +14,7 @@ def test_init(message_test_mp): scen.solve() assert np.isclose(scen.var('OBJ')['lvl'], 153.675) + assert 'mapping_macro_sector' in scen.set_list() + assert 'aeei' in scen.par_list() + assert 'DEMAND' in scen.var_list() + assert 'COST_ACCOUNTING_NODAL' in scen.equ_list() From 6d1ce175541416de799cae5eeeddf5f95104d1e3 Mon Sep 17 00:00:00 2001 From: Matthew Gidden Date: Tue, 23 Jul 2019 11:46:09 +0200 Subject: [PATCH 02/70] move from lists to dicts in init --- message_ix/macro.py | 122 ++++++++++++++++++++++++-------------------- 1 file changed, 66 insertions(+), 56 deletions(-) diff --git a/message_ix/macro.py b/message_ix/macro.py index be250862d..d3da4397d 100644 --- a/message_ix/macro.py +++ b/message_ix/macro.py @@ -1,69 +1,79 @@ MACRO_INIT = { - 'sets': [ - ['sector', ], - ['mapping_macro_sector', ['sector', 'commodity', 'level', ], ], - ], - 'pars': [ - ['demand_MESSAGE', ['node', 'sector', 'year', ], ], - ['price_MESSAGE', ['node', 'sector', 'year', ], ], - ['cost_MESSAGE', ['node', 'year', ], ], - ['gdp_calibrate', ['node', 'year', ], ], - ['MERtoPPP', ['node', 'year', ], ], - ['kgdp', ['node', ], ], - ['kpvs', ['node', ], ], - ['depr', ['node', ], ], - ['drate', ['node', ], ], - ['esub', ['node', ], ], - ['lotol', ['node', ], ], - ['p_ref', ['node', 'sector', ], ], - ['lakl', ['node', ], ], - ['prfconst', ['node', 'sector', ], ], - ['grow', ['node', 'year', ], ], - ['aeei', ['node', 'sector', 'year', ], ], - ], - 'vars': [ - ['DEMAND', ['node', 'commodity', 'level', 'year', 'time', ], ], - ['PRICE', ['node', 'commodity', 'level', 'year', 'time', ], ], - ['COST_NODAL', ['node', 'year', ], ], - ['COST_NODAL_NET', ['node', 'year', ], ], - ['GDP', ['node', 'year', ], ], - ['I', ['node', 'year', ], ], - ['C', ['node', 'year', ], ], - ['K', ['node', 'year', ], ], - ['KN', ['node', 'year', ], ], - ['Y', ['node', 'year', ], ], - ['YN', ['node', 'year', ], ], - ['EC', ['node', 'year', ], ], - ['UTILITY', ], - ['PHYSENE', ['node', 'sector', 'year', ], ], - ['PRODENE', ['node', 'sector', 'year', ], ], - ['NEWENE', ['node', 'sector', 'year', ], ], - ['EC', ['node', 'year', ], ], - ['grow_calibrate', ['node', 'year', ], ], - ['aeei_calibrate', ['node', 'sector', 'year', ], ], - ], - 'equs': [ - ['COST_ACCOUNTING_NODAL', ['node', 'year', ], ] - ], + 'sets': { + 'sector': None, + 'mapping_macro_sector': ['sector', 'commodity', 'level', ], + }, + 'pars': { + 'demand_MESSAGE': ['node', 'sector', 'year', ], + 'price_MESSAGE': ['node', 'sector', 'year', ], + 'cost_MESSAGE': ['node', 'year', ], + 'gdp_calibrate': ['node', 'year', ], + 'MERtoPPP': ['node', 'year', ], + 'kgdp': ['node', ], + 'kpvs': ['node', ], + 'depr': ['node', ], + 'drate': ['node', ], + 'esub': ['node', ], + 'lotol': ['node', ], + 'p_ref': ['node', 'sector', ], + 'lakl': ['node', ], + 'prfconst': ['node', 'sector', ], + 'grow': ['node', 'year', ], + 'aeei': ['node', 'sector', 'year', ], + }, + 'vars': { + 'DEMAND': ['node', 'commodity', 'level', 'year', 'time', ], + 'PRICE': ['node', 'commodity', 'level', 'year', 'time', ], + 'COST_NODAL': ['node', 'year', ], + 'COST_NODAL_NET': ['node', 'year', ], + 'GDP': ['node', 'year', ], + 'I': ['node', 'year', ], + 'C': ['node', 'year', ], + 'K': ['node', 'year', ], + 'KN': ['node', 'year', ], + 'Y': ['node', 'year', ], + 'YN': ['node', 'year', ], + 'EC': ['node', 'year', ], + 'UTILITY': None, + 'PHYSENE': ['node', 'sector', 'year', ], + 'PRODENE': ['node', 'sector', 'year', ], + 'NEWENE': ['node', 'sector', 'year', ], + 'EC': ['node', 'year', ], + 'grow_calibrate': ['node', 'year', ], + 'aeei_calibrate': ['node', 'sector', 'year', ], + }, + 'equs': { + 'COST_ACCOUNTING_NODAL': ['node', 'year', ], + }, } +MACRO_DATA_FOR_DERIVATION = { + 'cost_ref': ['node', 'sector', ], + 'demand_ref': ['node', 'sector', ], +} + +VERIFY_INPUT_DATA = [ + 'p_ref', 'lotol', 'esub', 'drate', 'depr', 'kpvs', 'kgdp', + 'gdp_calibrate', 'aeei', 'cost_ref', 'demand_ref', 'MERtoPPP', +] + def init(s): - for args in MACRO_INIT['sets']: - s.init_set(*args) - for args in MACRO_INIT['pars']: - s.init_par(*args) - for args in MACRO_INIT['vars']: - if not s.has_var(args[0]): + for key, values in MACRO_INIT['sets'].items(): + s.init_set(key, values) + for key, values in MACRO_INIT['pars'].items(): + s.init_par(key, values) + for key, values in MACRO_INIT['vars'].items(): + if not s.has_var(key): try: # TODO: this seems required because for some reason DEMAND (and # perhaps others) seem to already be listed in the java code, # but still needs to be initialized in the python code. However, # you cannot init it with dimensions, only with the variable # name. - s.init_var(*args) + s.init_var(key, values) except: - s.init_var(args[0]) - for args in MACRO_INIT['equs']: - s.init_equ(*args) + s.init_var(key) + for key, values in MACRO_INIT['equs'].items(): + s.init_equ(key, values) From a24f783cd9185dcb21a0571afaa5af51614e868b Mon Sep 17 00:00:00 2001 From: Matthew Gidden Date: Tue, 23 Jul 2019 13:52:51 +0200 Subject: [PATCH 03/70] starting data checks --- message_ix/macro.py | 64 ++++++++++++++++++++++++++++++++++ message_ix/tests/test_macro.py | 4 +++ 2 files changed, 68 insertions(+) diff --git a/message_ix/macro.py b/message_ix/macro.py index d3da4397d..0d8a1cc6a 100644 --- a/message_ix/macro.py +++ b/message_ix/macro.py @@ -77,3 +77,67 @@ def init(s): s.init_var(key) for key, values in MACRO_INIT['equs'].items(): s.init_equ(key, values) + + +def _validate_data(name, df, nodes, sectors, years): + def validate(kind, values, df): + if kind not in df: + return + + diff = set(values) - set(df[kind]) + if diff: + raise ValueError( + 'Not all {}s included in {} data: {}'.format(kind, name, diff) + ) + + # check required columns + if name in MACRO_DATA_FOR_DERIVATION: + cols = MACRO_DATA_FOR_DERIVATION[name] + else: + cols = MACRO_INIT['pars'][name] + col_diff = set(cols) - set(df.columns) + if col_diff: + msg = 'Missing expected columns for {}: {}' + raise ValueError(msg.format(name, col_diff)) + + # check required column values + checks = ( + ('node', nodes), + ('sector', sectors), + ('year', years), + ) + + for kind, values in checks: + validate(kind, values, df) + + +class Calculate(object): + + def __init__(self, s, data): + """ + s : solved message scenario + data : dict of parameter names to dataframes + """ + self.data = data + self.s = s + + if not s.has_solution(): + raise RuntimeError('Scenario must have a solution to add MACRO') + + demand = s.var('DEMAND') + self.nodes = demand['nodes'].unique() + self.sectors = demand['sector'].unique() + self.years = demand['year'].unique() + + def read_data(self): + if os.path.exists(self.data): + self.data = pd.read_excel(self.data, sheet_name=None) + + par_diff = set(VERIFY_INPUT_DATA) - set(self.data) + if par_diff: + raise ValueError( + 'Missing required input data: {}'.format(par_diff)) + + for name in self.data: + _validate_data(name, self.data[name], + self.nodes, self.sectors, self.years) diff --git a/message_ix/tests/test_macro.py b/message_ix/tests/test_macro.py index 83164bbe8..3c3ac81b0 100644 --- a/message_ix/tests/test_macro.py +++ b/message_ix/tests/test_macro.py @@ -18,3 +18,7 @@ def test_init(message_test_mp): assert 'aeei' in scen.par_list() assert 'DEMAND' in scen.var_list() assert 'COST_ACCOUNTING_NODAL' in scen.equ_list() + + +def test_calc_valid_data(test_mp): + pass From 6bbe93f43a7405778cbf8b600f8104b3e61be319 Mon Sep 17 00:00:00 2001 From: Matthew Gidden Date: Wed, 24 Jul 2019 13:15:42 +0200 Subject: [PATCH 04/70] add initial test for macro data read --- message_ix/macro.py | 21 +++++++++++++++------ message_ix/tests/test_macro.py | 12 ++++++++++-- tests/data/westeros_macro_input.xlsx | Bin 0 -> 15319 bytes 3 files changed, 25 insertions(+), 8 deletions(-) create mode 100644 tests/data/westeros_macro_input.xlsx diff --git a/message_ix/macro.py b/message_ix/macro.py index 0d8a1cc6a..36d7d4fcd 100644 --- a/message_ix/macro.py +++ b/message_ix/macro.py @@ -1,3 +1,7 @@ +import collections +import os + +import pandas as pd MACRO_INIT = { 'sets': { @@ -49,7 +53,7 @@ } MACRO_DATA_FOR_DERIVATION = { - 'cost_ref': ['node', 'sector', ], + 'cost_ref': ['node', ], 'demand_ref': ['node', 'sector', ], } @@ -121,18 +125,23 @@ def __init__(self, s, data): self.data = data self.s = s + good = isinstance(data, collections.Mapping) or os.path.exists(data) + if not good: + raise ValueError('Data argument is not a dictionary nor a file') + if os.path.exists(data): + if not str(self.data).endswith('xlsx'): + raise ValueError('Must provide excel-based data file') + self.data = pd.read_excel(self.data, sheet_name=None) + if not s.has_solution(): raise RuntimeError('Scenario must have a solution to add MACRO') demand = s.var('DEMAND') - self.nodes = demand['nodes'].unique() - self.sectors = demand['sector'].unique() + self.nodes = demand['node'].unique() + self.sectors = demand['commodity'].unique() self.years = demand['year'].unique() def read_data(self): - if os.path.exists(self.data): - self.data = pd.read_excel(self.data, sheet_name=None) - par_diff = set(VERIFY_INPUT_DATA) - set(self.data) if par_diff: raise ValueError( diff --git a/message_ix/tests/test_macro.py b/message_ix/tests/test_macro.py index 3c3ac81b0..976937a3a 100644 --- a/message_ix/tests/test_macro.py +++ b/message_ix/tests/test_macro.py @@ -1,7 +1,12 @@ import numpy as np +try: + from pathlib import Path +except ImportError: + from pathlib2 import Path + from message_ix import Scenario, macro -from message_ix.testing import SCENARIO +from message_ix.testing import make_westeros def test_init(message_test_mp): @@ -21,4 +26,7 @@ def test_init(message_test_mp): def test_calc_valid_data(test_mp): - pass + s = make_westeros(test_mp, solve=True) + path = Path(__file__).parent / 'data' / 'westeros_macro_input.xlsx' + c = macro.Calculate(s, path) + c.read_data() diff --git a/tests/data/westeros_macro_input.xlsx b/tests/data/westeros_macro_input.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..6c966d6c133a0bfaaad4dad97bf2d3ee0b56c316 GIT binary patch literal 15319 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 Date: Wed, 24 Jul 2019 14:20:18 +0200 Subject: [PATCH 05/70] add tests for all input data --- message_ix/macro.py | 2 +- message_ix/tests/test_macro.py | 48 +++++++++++++++++++++++++++++++++- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/message_ix/macro.py b/message_ix/macro.py index 36d7d4fcd..3378f240b 100644 --- a/message_ix/macro.py +++ b/message_ix/macro.py @@ -128,7 +128,7 @@ def __init__(self, s, data): good = isinstance(data, collections.Mapping) or os.path.exists(data) if not good: raise ValueError('Data argument is not a dictionary nor a file') - if os.path.exists(data): + if not isinstance(data, collections.Mapping) and os.path.exists(data): if not str(self.data).endswith('xlsx'): raise ValueError('Must provide excel-based data file') self.data = pd.read_excel(self.data, sheet_name=None) diff --git a/message_ix/tests/test_macro.py b/message_ix/tests/test_macro.py index 976937a3a..097b7aef5 100644 --- a/message_ix/tests/test_macro.py +++ b/message_ix/tests/test_macro.py @@ -1,4 +1,7 @@ +import pytest + import numpy as np +import pandas as pd try: from pathlib import Path @@ -25,8 +28,51 @@ def test_init(message_test_mp): assert 'COST_ACCOUNTING_NODAL' in scen.equ_list() -def test_calc_valid_data(test_mp): +def test_calc_valid_data_file(test_mp): s = make_westeros(test_mp, solve=True) path = Path(__file__).parent / 'data' / 'westeros_macro_input.xlsx' c = macro.Calculate(s, path) c.read_data() + + +def test_calc_valid_data_dict(test_mp): + s = make_westeros(test_mp, solve=True) + path = Path(__file__).parent / 'data' / 'westeros_macro_input.xlsx' + data = pd.read_excel(path, sheet_name=None) + c = macro.Calculate(s, data) + c.read_data() + + +def test_calc_no_solution(test_mp): + s = make_westeros(test_mp) + path = Path(__file__).parent / 'data' / 'westeros_macro_input.xlsx' + pytest.raises(RuntimeError, macro.Calculate, s, path) + + +def test_calc_data_missing_par(test_mp): + s = make_westeros(test_mp, solve=True) + path = Path(__file__).parent / 'data' / 'westeros_macro_input.xlsx' + data = pd.read_excel(path, sheet_name=None) + data.pop('gdp_calibrate') + c = macro.Calculate(s, data) + pytest.raises(ValueError, c.read_data) + + +def test_calc_data_missing_column(test_mp): + s = make_westeros(test_mp, solve=True) + path = Path(__file__).parent / 'data' / 'westeros_macro_input.xlsx' + data = pd.read_excel(path, sheet_name=None) + # skip first data point + data['gdp_calibrate'] = data['gdp_calibrate'].drop('year', axis=1) + c = macro.Calculate(s, data) + pytest.raises(ValueError, c.read_data) + + +def test_calc_data_missing_datapoint(test_mp): + s = make_westeros(test_mp, solve=True) + path = Path(__file__).parent / 'data' / 'westeros_macro_input.xlsx' + data = pd.read_excel(path, sheet_name=None) + # skip first data point + data['gdp_calibrate'] = data['gdp_calibrate'][1:] + c = macro.Calculate(s, data) + pytest.raises(ValueError, c.read_data) From bf3b4c01e483290167dda857d52ccdd0f3333b6a Mon Sep 17 00:00:00 2001 From: Matthew Gidden Date: Wed, 24 Jul 2019 14:41:36 +0200 Subject: [PATCH 06/70] Reimplement cost_nodal_net, closes #221 --- message_ix/model/MESSAGE/model_solve.gms | 2 +- message_ix/model/MESSAGE/parameter_def.gms | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/message_ix/model/MESSAGE/model_solve.gms b/message_ix/model/MESSAGE/model_solve.gms index 0173e1be0..a7dae5af0 100644 --- a/message_ix/model/MESSAGE/model_solve.gms +++ b/message_ix/model/MESSAGE/model_solve.gms @@ -160,7 +160,7 @@ export_cost(node2, commodity, year) = trade_cost(node2, year) = SUM(commodity, import_cost(node2, commodity, year) - export_cost(node2, commodity, year)) ; * total energy system costs excluding taxes by node and time (CAVEAT: lacking regional corrections due to emission trading) -total_cost(node, year)$(NOT macro_base_period(year)) = ( +COST_NODAL_NET.L(node, year)$(NOT macro_base_period(year)) = ( COST_NODAL.L(node, year) + trade_cost(node, year) * subtract emission taxes applied at any higher nodal level (via map_node set) - sum((type_emission,emission,type_tec,type_year,node2)$( emission_scaling(type_emission,emission) diff --git a/message_ix/model/MESSAGE/parameter_def.gms b/message_ix/model/MESSAGE/parameter_def.gms index b7eb4a61d..116cfd6cb 100644 --- a/message_ix/model/MESSAGE/parameter_def.gms +++ b/message_ix/model/MESSAGE/parameter_def.gms @@ -733,7 +733,6 @@ Parameters *----------------------------------------------------------------------------------------------------------------------* Parameters - total_cost(node, year_all) total system costs net of trade costs and emissions taxes by node and year trade_cost(node, year_all) net of commodity import costs and commodity export revenues by node and year import_cost(node, commodity, year_all) import costs by commodity and node and year export_cost(node, commodity, year_all) export revenues by commodity and node and year From f17849a760fbdab4e4143741cdef87f288eb4777 Mon Sep 17 00:00:00 2001 From: Matthew Gidden Date: Thu, 25 Jul 2019 10:13:53 +0200 Subject: [PATCH 07/70] use fixtures instead of solving multiple scens --- message_ix/tests/test_macro.py | 38 +++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/message_ix/tests/test_macro.py b/message_ix/tests/test_macro.py index 097b7aef5..36dbde7d5 100644 --- a/message_ix/tests/test_macro.py +++ b/message_ix/tests/test_macro.py @@ -12,8 +12,18 @@ from message_ix.testing import make_westeros -def test_init(message_test_mp): - scen = Scenario(message_test_mp, **SCENARIO['dantzig']) +@pytest.fixture(scope='module') +def westeros_solved(test_mp): + return make_westeros(test_mp, solve=True) + + +@pytest.fixture(scope='module') +def westeros_not_solved(test_mp): + return make_westeros(test_mp, solve=False) + + +def test_init(test_mp): + scen = Scenario(test_mp, *msg_args) scen = scen.clone('foo', 'bar') scen.check_out() @@ -28,29 +38,29 @@ def test_init(message_test_mp): assert 'COST_ACCOUNTING_NODAL' in scen.equ_list() -def test_calc_valid_data_file(test_mp): - s = make_westeros(test_mp, solve=True) +def test_calc_valid_data_file(westeros_solved): + s = westeros_solved path = Path(__file__).parent / 'data' / 'westeros_macro_input.xlsx' c = macro.Calculate(s, path) c.read_data() -def test_calc_valid_data_dict(test_mp): - s = make_westeros(test_mp, solve=True) +def test_calc_valid_data_dict(westeros_solved): + s = westeros_solved path = Path(__file__).parent / 'data' / 'westeros_macro_input.xlsx' data = pd.read_excel(path, sheet_name=None) c = macro.Calculate(s, data) c.read_data() -def test_calc_no_solution(test_mp): - s = make_westeros(test_mp) +def test_calc_no_solution(westeros_not_solved): + s = westeros_not_solved path = Path(__file__).parent / 'data' / 'westeros_macro_input.xlsx' pytest.raises(RuntimeError, macro.Calculate, s, path) -def test_calc_data_missing_par(test_mp): - s = make_westeros(test_mp, solve=True) +def test_calc_data_missing_par(westeros_solved): + s = westeros_solved path = Path(__file__).parent / 'data' / 'westeros_macro_input.xlsx' data = pd.read_excel(path, sheet_name=None) data.pop('gdp_calibrate') @@ -58,8 +68,8 @@ def test_calc_data_missing_par(test_mp): pytest.raises(ValueError, c.read_data) -def test_calc_data_missing_column(test_mp): - s = make_westeros(test_mp, solve=True) +def test_calc_data_missing_column(westeros_solved): + s = westeros_solved path = Path(__file__).parent / 'data' / 'westeros_macro_input.xlsx' data = pd.read_excel(path, sheet_name=None) # skip first data point @@ -68,8 +78,8 @@ def test_calc_data_missing_column(test_mp): pytest.raises(ValueError, c.read_data) -def test_calc_data_missing_datapoint(test_mp): - s = make_westeros(test_mp, solve=True) +def test_calc_data_missing_datapoint(westeros_solved): + s = westeros_solved path = Path(__file__).parent / 'data' / 'westeros_macro_input.xlsx' data = pd.read_excel(path, sheet_name=None) # skip first data point From 445cb0aa42c5caa0e94c527bc84348e977c654eb Mon Sep 17 00:00:00 2001 From: Matthew Gidden Date: Thu, 25 Jul 2019 10:17:21 +0200 Subject: [PATCH 08/70] quiet warnings --- message_ix/tests/test_macro.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/message_ix/tests/test_macro.py b/message_ix/tests/test_macro.py index 36dbde7d5..cde55cad5 100644 --- a/message_ix/tests/test_macro.py +++ b/message_ix/tests/test_macro.py @@ -11,6 +11,11 @@ from message_ix import Scenario, macro from message_ix.testing import make_westeros +msg_args = ('canning problem (MESSAGE scheme)', 'standard') + +# tons of deprecation warnings come from reading excel (xlrd library), ignore +# them for now +pytestmark = pytest.mark.filterwarnings("ignore") @pytest.fixture(scope='module') def westeros_solved(test_mp): From 12dd446f2080563c79ac8c4b806588115f0cb15c Mon Sep 17 00:00:00 2001 From: Matthew Gidden Date: Thu, 25 Jul 2019 10:51:56 +0200 Subject: [PATCH 09/70] more test cleanup --- message_ix/tests/test_macro.py | 43 ++++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/message_ix/tests/test_macro.py b/message_ix/tests/test_macro.py index cde55cad5..c4c89eb97 100644 --- a/message_ix/tests/test_macro.py +++ b/message_ix/tests/test_macro.py @@ -11,12 +11,15 @@ from message_ix import Scenario, macro from message_ix.testing import make_westeros -msg_args = ('canning problem (MESSAGE scheme)', 'standard') - # tons of deprecation warnings come from reading excel (xlrd library), ignore # them for now pytestmark = pytest.mark.filterwarnings("ignore") +MSG_ARGS = ('canning problem (MESSAGE scheme)', 'standard') + +DATA_PATH = Path(__file__).parent / 'data' / 'westeros_macro_input.xlsx' + + @pytest.fixture(scope='module') def westeros_solved(test_mp): return make_westeros(test_mp, solve=True) @@ -28,7 +31,7 @@ def westeros_not_solved(test_mp): def test_init(test_mp): - scen = Scenario(test_mp, *msg_args) + scen = Scenario(test_mp, *MSG_ARGS) scen = scen.clone('foo', 'bar') scen.check_out() @@ -45,29 +48,25 @@ def test_init(test_mp): def test_calc_valid_data_file(westeros_solved): s = westeros_solved - path = Path(__file__).parent / 'data' / 'westeros_macro_input.xlsx' - c = macro.Calculate(s, path) + c = macro.Calculate(s, DATA_PATH) c.read_data() def test_calc_valid_data_dict(westeros_solved): s = westeros_solved - path = Path(__file__).parent / 'data' / 'westeros_macro_input.xlsx' - data = pd.read_excel(path, sheet_name=None) + data = pd.read_excel(DATA_PATH, sheet_name=None) c = macro.Calculate(s, data) c.read_data() def test_calc_no_solution(westeros_not_solved): s = westeros_not_solved - path = Path(__file__).parent / 'data' / 'westeros_macro_input.xlsx' - pytest.raises(RuntimeError, macro.Calculate, s, path) + pytest.raises(RuntimeError, macro.Calculate, s, DATA_PATH) def test_calc_data_missing_par(westeros_solved): s = westeros_solved - path = Path(__file__).parent / 'data' / 'westeros_macro_input.xlsx' - data = pd.read_excel(path, sheet_name=None) + data = pd.read_excel(DATA_PATH, sheet_name=None) data.pop('gdp_calibrate') c = macro.Calculate(s, data) pytest.raises(ValueError, c.read_data) @@ -75,8 +74,7 @@ def test_calc_data_missing_par(westeros_solved): def test_calc_data_missing_column(westeros_solved): s = westeros_solved - path = Path(__file__).parent / 'data' / 'westeros_macro_input.xlsx' - data = pd.read_excel(path, sheet_name=None) + data = pd.read_excel(DATA_PATH, sheet_name=None) # skip first data point data['gdp_calibrate'] = data['gdp_calibrate'].drop('year', axis=1) c = macro.Calculate(s, data) @@ -85,9 +83,24 @@ def test_calc_data_missing_column(westeros_solved): def test_calc_data_missing_datapoint(westeros_solved): s = westeros_solved - path = Path(__file__).parent / 'data' / 'westeros_macro_input.xlsx' - data = pd.read_excel(path, sheet_name=None) + data = pd.read_excel(DATA_PATH, sheet_name=None) # skip first data point data['gdp_calibrate'] = data['gdp_calibrate'][1:] c = macro.Calculate(s, data) pytest.raises(ValueError, c.read_data) + +# +# Regression tests: these tests were compiled upon moving from R to Python, +# values were confirmed correct at the time and thus are tested explicitly here +# + + +def test_calc_rho(westeros_solved): + s = westeros_solved + c = macro.Calculate(s, DATA_PATH) + c.read_data() + obs = c._rho().values + assert len(obs) == 1 + obs = obs[0] + exp = -4 + assert obs == exp From 1e98c9801e52ce36a5a84ec598676bd4e80cc81f Mon Sep 17 00:00:00 2001 From: Matthew Gidden Date: Thu, 25 Jul 2019 10:52:08 +0200 Subject: [PATCH 10/70] make gdp_calibrate have historical value --- message_ix/macro.py | 29 +++++++++++++++++++++++++-- tests/data/westeros_macro_input.xlsx | Bin 15319 -> 17086 bytes 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/message_ix/macro.py b/message_ix/macro.py index 3378f240b..b75e0e221 100644 --- a/message_ix/macro.py +++ b/message_ix/macro.py @@ -114,6 +114,8 @@ def validate(kind, values, df): for kind, values in checks: validate(kind, values, df) + return cols + class Calculate(object): @@ -148,5 +150,28 @@ def read_data(self): 'Missing required input data: {}'.format(par_diff)) for name in self.data: - _validate_data(name, self.data[name], - self.nodes, self.sectors, self.years) + idx = _validate_data(name, self.data[name], + self.nodes, self.sectors, self.years) + self.data[name] = self.data[name].set_index(idx)['value'] + + # special check for gdp_calibrate + check = self.data['gdp_calibrate'] + min_year_model = min(self.years) + min_year_data = min(check.index.get_level_values('year')) + if not min_year_data < min_year_model: + raise ValueError( + 'Must provide gdp_calibrate data prior to the modeling' + + ' period in order to calculate growth rates' + ) + + def derive_data(self): + self._rho() + self._k0() + + def _rho(self): + esub = self.data['esub'] + self.data['rho'] = (esub - 1) / esub + return self.data['rho'] + + def _k0(self): + pass diff --git a/tests/data/westeros_macro_input.xlsx b/tests/data/westeros_macro_input.xlsx index 6c966d6c133a0bfaaad4dad97bf2d3ee0b56c316..2e66f20b305f32a05924efec5a7bbbbdf76bd60d 100644 GIT binary patch literal 17086 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 literal 15319 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 Date: Thu, 25 Jul 2019 11:08:35 +0200 Subject: [PATCH 11/70] add base year check and k0 calc --- message_ix/macro.py | 12 ++++++++++-- message_ix/tests/test_macro.py | 13 ++++++++++++- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/message_ix/macro.py b/message_ix/macro.py index b75e0e221..5365fdb77 100644 --- a/message_ix/macro.py +++ b/message_ix/macro.py @@ -156,13 +156,16 @@ def read_data(self): # special check for gdp_calibrate check = self.data['gdp_calibrate'] + data_years = check.index.get_level_values('year') min_year_model = min(self.years) - min_year_data = min(check.index.get_level_values('year')) + min_year_data = min(data_years) if not min_year_data < min_year_model: raise ValueError( 'Must provide gdp_calibrate data prior to the modeling' + ' period in order to calculate growth rates' ) + # base year is most recent period PRIOR to the modeled period + self.base_year = max(data_years[data_years < min_year_model]) def derive_data(self): self._rho() @@ -174,4 +177,9 @@ def _rho(self): return self.data['rho'] def _k0(self): - pass + kgdp = self.data['kgdp'] + gdp = self.data['gdp_calibrate'] + # TODO: drop index level 'year' after?? + gdp0 = gdp.iloc[gdp.index.isin([self.base_year], level='year')] + self.data['k0'] = kgdp * gdp0 + return self.data['k0'] diff --git a/message_ix/tests/test_macro.py b/message_ix/tests/test_macro.py index c4c89eb97..d29bec1a9 100644 --- a/message_ix/tests/test_macro.py +++ b/message_ix/tests/test_macro.py @@ -99,8 +99,19 @@ def test_calc_rho(westeros_solved): s = westeros_solved c = macro.Calculate(s, DATA_PATH) c.read_data() - obs = c._rho().values + obs = c._rho() assert len(obs) == 1 obs = obs[0] exp = -4 assert obs == exp + + +def test_calc_k0(westeros_solved): + s = westeros_solved + c = macro.Calculate(s, DATA_PATH) + c.read_data() + obs = c._k0() + assert len(obs) == 1 + obs = obs[0] + exp = 15 + assert obs == exp From ac137ace7d003ac95208c5ffece9b8ca23ce82c5 Mon Sep 17 00:00:00 2001 From: Matthew Gidden Date: Thu, 25 Jul 2019 11:56:39 +0200 Subject: [PATCH 12/70] add price data and check --- message_ix/macro.py | 38 +++++++++++++++++++++++++-- message_ix/tests/test_macro.py | 24 +++++++++++++++++ tests/data/westeros_macro_input.xlsx | Bin 17086 -> 17111 bytes 3 files changed, 60 insertions(+), 2 deletions(-) diff --git a/message_ix/macro.py b/message_ix/macro.py index 5365fdb77..9016aa4e4 100644 --- a/message_ix/macro.py +++ b/message_ix/macro.py @@ -20,7 +20,7 @@ 'drate': ['node', ], 'esub': ['node', ], 'lotol': ['node', ], - 'p_ref': ['node', 'sector', ], + 'price_ref': ['node', 'sector', ], 'lakl': ['node', ], 'prfconst': ['node', 'sector', ], 'grow': ['node', 'year', ], @@ -58,7 +58,7 @@ } VERIFY_INPUT_DATA = [ - 'p_ref', 'lotol', 'esub', 'drate', 'depr', 'kpvs', 'kgdp', + 'price_ref', 'lotol', 'esub', 'drate', 'depr', 'kpvs', 'kgdp', 'gdp_calibrate', 'aeei', 'cost_ref', 'demand_ref', 'MERtoPPP', ] @@ -183,3 +183,37 @@ def _k0(self): gdp0 = gdp.iloc[gdp.index.isin([self.base_year], level='year')] self.data['k0'] = kgdp * gdp0 return self.data['k0'] + + def _total_cost(self): + # read from scenario + idx = ['node', 'year'] + model_cost = self.s.var('COST_NODAL_NET') + model_cost.rename(columns={'lvl': 'value'}, inplace=True) + model_cost = model_cost[idx + ['value']] + # get data provided in base year from data + cost_ref = self.data['cost_ref'].reset_index() + cost_ref['year'] = self.base_year + # combine into one value + total_cost = pd.concat([cost_ref, model_cost]).set_index(idx)['value'] + if total_cost.isnull().any(): + raise RuntimeError('NaN values found in total_cost calculation') + self.data['total_cost'] = total_cost + return total_cost + + def _price(self): + # read from scenario + idx = ['node', 'sector', 'year'] + model_price = self.s.var('PRICE_COMMODITY', + filters={'level': 'useful'}) + model_price.rename(columns={'lvl': 'value', 'commodity': 'sector'}, + inplace=True) + model_price = model_price[idx + ['value']] + # get data provided in base year from data + price_ref = self.data['price_ref'].reset_index() + price_ref['year'] = self.base_year + # combine into one value + price = pd.concat([price_ref, model_price]).set_index(idx)['value'] + if price.isnull().any(): + raise RuntimeError('NaN values found in price calculation') + self.data['price'] = price + return price diff --git a/message_ix/tests/test_macro.py b/message_ix/tests/test_macro.py index d29bec1a9..ec5f141c1 100644 --- a/message_ix/tests/test_macro.py +++ b/message_ix/tests/test_macro.py @@ -115,3 +115,27 @@ def test_calc_k0(westeros_solved): obs = obs[0] exp = 15 assert obs == exp + + +def test_calc_total_cost(westeros_solved): + s = westeros_solved + c = macro.Calculate(s, DATA_PATH) + c.read_data() + obs = c._total_cost() + # 4 values, 3 in model period, one in history + assert(len(obs) == 4) + obs = obs.values + exp = np.array([15, 17.477751, 22.143633, 28.189798]) + assert np.isclose(obs, exp).all() + + +def test_calc_price(westeros_solved): + s = westeros_solved + c = macro.Calculate(s, DATA_PATH) + c.read_data() + obs = c._price() + # 4 values, 3 in model period, one in history + assert(len(obs) == 4) + obs = obs.values + exp = np.array([195, 183.094376, 161.645111, 161.645111]) + assert np.isclose(obs, exp).all() diff --git a/tests/data/westeros_macro_input.xlsx b/tests/data/westeros_macro_input.xlsx index 2e66f20b305f32a05924efec5a7bbbbdf76bd60d..c4ff795c828a41423e095e38c757852f8839affc 100644 GIT binary patch delta 12549 zcmeHtRa6~Y+AZ$x!3nMbf(CaD4#6c5+}$@$ux#92gS)%CdvFO7g3HFakaYij(tY0U z!@Xnt%}b3@>)TbUR@G#FYtGpg2tFSOj-((1355j)2L}f>AXkY*2@WwJSN#?o@atFu zLH%-!S%IVk=*V|5V|jr-FnE#Z+@P81Q-tDSvQP=Kwap$u%$8bwLyQ5~a4v{`@2vM2 z=jH?DxNtp=OzFeU6>0^5qY*k0P z!sCp|gvIU_e0G&4?Kmh5RA7XynGh+3FXwu&J^T4`V-iC?g|&nwn)pM-iiRj8v6CCH z6=lC10Lt4S8?)lY{7gXO*L*j)eN{Qbp+YOuAytfYOATV} zI|O_m*qtQc8|Sy}e#|$f?*v#xzZ>^yg*I%N|6T%M)wfbmt*cP}PT7}MWEvB%(|owZ zBgq>+L%}Z|NK3f>?%pBo6jwn87A}lSu+j+}0}N~*0u1c0Z>u^ATJP6uI)kwV2b3pA zk=1W}ZL6N1SG@F%4`SM!6;vzC`+gV8Q7!{b+k~yw6D^t&lIhbZ?;lzt6=@pA=sDqg zTz^VC`&n8G=>a83C`HJ8rV50ub>W#7wI4nSf>xytv~*4zanDERH~yTL^^;l%00~m?z+{! zS=gcuy96zO=jESj;Fh2uqj;5D`;IK)p;u-k7@&=b8%!Wt-+PaPcDq;m9~Jc%v_^0d zZ#PWpt4}v^YrB)3gvWnou4QMfK7uW75!B{-_3Ye1ZND!a%&X~cj^9bwT)g0XS>HbI z0TjRIrKN`Xb9r;h;2~c=egia002`C5m1V2ydz&RLv{ujuw3c#89=kTuV$-zCIzdg? z14qOQ@i6i*I5BgnA?vdP7fI==kH=XBxB8LY8zU{8sRchD#xREj~f$EYBaKqTqK z$PkhM-Xt3?QpVt%lo;?V_}t9}IDm3VCMQU!-Qpb+&chK$)-}jrhM*sqTeZeoBy6uG zIy%41ZIfcn2TSA-VKFJZ62Z{g??wQxiC*OFYY$&X6$aN#GvnD>yRDcx!<-5rdPQ;% zmILdL-i#mv@R>p;BU-W+{Aj;!GgYER%`&_L$Xhk%7F}1UQhTQdxig(#e~dotMxsVN2b=Yw6 zQdr8t!4098NX#e-AHb6rl-PQyp6;QIL=2yepE>GHU$dy8kw87sM@;9Jk$aBmxNBf7 zKS7y8#UV~pZ|x@-g+NUZcli~nIR_zCQz8GhvZMyRT$7tB;j6p8I1n{&DP;4WG(-~= zn3UGh!)hw_uJLFlM{CP65L>Br7NzmfMKzMNJw?8NbKmn?5ujv_)%e6G4dZ1+K)4Ds zBHpsu+%DE7F06=Pn3mvQZq>YvUs}d zZvI`(ZW}tHyT*pll^3@r!V28lZl+w+nrddWtz!HF&v#)z!}qOjqc<6MTX@?^JG|cH zCxsDn4dLiHZtB384d-je-6!xDz5=|d6Gj{j?ocEapt^$mv_wiB(%v0kBS;1Z=xHInEwKK@@YQuvuo7}WJ zPCI&0Rrqye;74uZ`0a!9oq6bx@VscnEDDA}db~5TCrTdLhmUk%1X++~@wruXZl4X6 zRTPvR^6(?S(#Ad0L~k*FPyVD}5wWZ6L%w$O`i9WcXH?br7N1(WZ}_>$a%^zs zEMCf|eUZH9=;@9s_69-#HKb{#LS~<)AYy&kD~qkmD-FJ^%n$u;I4;2RaJ%qzg%Gw_ znKQeq3c0jE&g+!!LD{j%5Se^Ac})$Pl3><`aM?UL0tP@P_Dl=rV-~l^Him+^(-QoX zQb8O>1{&4R0hLo!Q+7Bu0hPpC@>pkEvwd$^2!BWqT;nB8Uq$EgOWlx8NdM@%t|mg} z^r_bS&$SF8jy{MNh4{FJ(863`k}6zF0xTVe$RVq-MJ~|TaBB=wRG>GHH;psk50ZMS z2jB-a)}eqa=~k|&^OT0G8&<*-WGBq|PeQLlr^p=_bEiFA@spl$HB}iic6zbQX|-j7 z5S%Fj;rj%Iq9$u8cZ=Jqnr`sD?}-Nq%4!5^C-g1MGpcS^R1?Nd2Ug@7q1~OB8~wPe zXQLU)rVdh;?hblaRy@Dd@o%7Y!|E!_oXZv~=K@;&v)R>V$=erPk#8*ee5R4(NkrW>73=FMx|JDah1*i<&vNUmv>ykRVZ4Z}Mi4wVO( zpYDrka+>t&X^xDCW@t*<*a3ppZ>I(~OKx3RVIp>>tO?}p6a$ximU-<54Ys2k;4yUC z7XWcn_wS`-dJ)_(FN{2+?it@OVcY2skE@Vy!QiA@MHe@J0W`g}E)X!FzZ!ad^-y|7 zv15I{Xc;vUOcEj;$ad3SO9Gk|znKq3S!zPcqqs~QbfA&9c?g9i0HeK% z8;IJ7(5D@R`=P6*q-T%FKM`h0pD=OmhJdDdYfM&7C>x!{uDo7R2AVen>kRZll~;bC)%(Lx-y?7sf%9aBR)d{cVQd9Ja*$a8=>y^*+Q&){d&ykJ}4E zW^Jdus+9v+2Vn~ZNyAeR=P>#{dLi2{c@gk-#iuJqDJ7YEIoawX0c3cvOs#CbnS0WV zEDb*Sa$Yiik*Y~#W~9wO3YM2Y_*J}q_sU&NPr{2#=} z?Zw%sA=~;)d`$W_5*Agsp$8AGlPCzdU&8@pA*C1IAz+$=KM}Vi$gk5BeAG<7ja_G~ zzNxtnU^9^DgB9r9>(uY5>zX>wbQF>7=dg%OB)kq|E1r`Ysq!x3)I>q&iYiT8Po`rN9n6Y|ZS*|+az@jpG?()#a|f%`JR-opNy^(fLJ@NX9x!YS#o zPIgdMKePP$jhq7x*#>6O>x^c)`ry8+y^o|;+R)A^BX-pHnD8+~3{??`jpNwV0MPeu zL$ITc?CodS@+hVfQz-7YF;`9?-j+ebV+?(UlFbEX2sSz>0)4_%sI5v(Mbb3eN z9UF^Jc?6Ug%q+9=&sp-(2s81#w27>Ul8xm9@AdHZzC+dsBAaVXA zEJjoHBMYKdczeN}9E~f?Z^?8E34lq1RT}6q&Xc&rwF2wS8I27*#(GkO=iKAp2#Uz9 z5fszd`v%Z2!4-if+dgYJc`QXaY_#f0P4|fO(nEPZsw5B9sK=&GA3or z5iTs?cVl^6j^=7Z@!VMIH=7)(&isGDgVp zo3?Mp*Sd=F*e?jP5T@!XJhfynsQtOX71(Yiej`sih= z+zRW&FTtU(S`^czI~@C*ulR(MSl)ZmBbk{?SAyli3CP%=Js@uB=~ZseCYr}M1pbVu zvaEQ*FU!yj*@b=lnB4KfLmD7b!@iRwi^Z991dY|>F>1E{jZVF5vME@7w7`yot*1HU zj=Clknp8yyn1oX{{d>(i^x9~i8w?e(O|))&q~ zpI&=z!81|v(YXt~5ycXU?@D;9$plxqY>L#! zSZo-s*Z>!Y6E)(Pup8i4nZjfy%xFZEIpWRhz6tx>hr5exE6Q%A{rwKWNiAS+WB+w& z|Jm|Z6-W}@1=ah$-H#+A&dCYlaoNxw!h0`^oX>@tfB2kcp890ElW<1OyuHkSfReHZ zvglm#y|V-*cP$`u#>u9cO|1?8uu(J9-j8VH%}{&7&5aQLwfUpuY0>G{)mC@eEM0&B)% z@;$AJtUmW-r$2o@wwSTL;~d36Xs*k89c%ue;-^b)pNayYN9O`o=@1++CVb_L@R!&m z`pGKY6SY>ld2J-O6%LsUVYW|EM{!PQANgu;4aKfT}te zo{Ub>RLYR(QQmgJ{wf>5fbcWzimbCTf2;*(bNt~nZKOc_^L1LMB0-7WD{^s%c4De- ztEZccCm02SXOJR(ypVH$rcU9%qfYk!?*IO`?Eg~zc!2gE5KONwD_DdZoiHy#RJays zN+ie@h{V=s{OD%oKjlTZ&JM9OicUhlb@%wXEQhrVD;3-HX5=RnRlE@0)SweJ@d~~K zeM#hgsk5Y2d$AwH&5Gx{R;k@?V6+k-r|;odk#)dgY5gaMKF20@#seb;$5egb zD(#71^TXPO-j_lAi_9#3j9QF6Et3?Gj?8P*Y@&BtT{#RA}|6Enn zW~GX0rNRdEz#HMF@s&ePy-S?NULasp#6b_8k^CjMQ&4K4FXf|(oS{Yb16@E~5;sg< zpxTd}A(CRydfBcRSw1JS+p#1Gd*|I(^Uw&feZ2j$wpF6?qFFA*dQ;Vrz#g38awtWQ`&A7!Z*m^vg0%4_j=DLC!$uH(!EKw)tmFS{? zDr~1aDmSfdcg-O9Cj8=}e8;Dd2J}>4dBqewi>^d;FzBStM|kkE9U51h>~AKCzMghi z$X6LZFDqx^yn-`C4WTXMN9o_eotnz_*#L}&Ow8~$EG%`kQ=k!;)#ZCWRVN{Xj?6P3 zYxDf7+oJnUZC9|ZEY`4i{nX&m9wAw(gg?Unuy*{aiZBxQV8f>0paz*Flf1jFbcz6A z|CtZZPV?M-)9yUP#2yxvDoS%xg9ZX;X1SPqu;4W(dK+!*QJ zme9UcxBaMN-5=tD+Vo~TKVSP~*hJFVsZpI(QCWF+VQ>3lMU&P)6$g6eiv_`Yw6e+S z_nYH_;e;w_vGis$#Po0a$TGW=Nq3jbc$Rl$u9qY)O336GG0fm|^C*e$mOp$Y)@2q@VCpQOP3rA^2SEbzFqDV=p~g(guG7MQ9$F1ri9ZT%%%I>S@=%037{hQbxGyK6r&$kQ793PuPeeGF=&J}zDqM- z6}-z84LTK1R&TdtW`orZ{YW*E#ahrS9&36GZr;a&lDh+CWPYkN1=bB1WX!EerP%70 zq!5J8K=12kiSA9fwmeo6>~P(;p3Aef-UAg^;6aI;+o!ls2AMWI4TJcVYw}Gv06;H( zoCsEfuR7ZY_-;mdL!ArHGo^El8f%FhN@OqlfWkU0j9;||-oGxW7qF-nWEi`mT$#`! zmKhlpN{|BC7gJP8QW-Bs!>~cf^2yAw#K%EK5HEKQM1o|?g6MBT8>S=8HO2X5YwSL3 zX|-N6`*LemZWrW6VO-ZRfZ(vDfKg?xEc=9Z;t%hl4E|QaSea(NvGJh z^e`9t=u1kz)pAmoh{9Xn9sq<4JrxIP3Q@#7Yv)kyT|J?#(hrnT?i{Q!qBjGmS}R6h zM2y>M@4)#qk}W&KlReROBdDeqdoM43oZ0hB=p|GDHz3MG$c9A+l`{ziH34Sih#6zt zW%6|-Yj+&8GvfQeP9j3zW>J_=`c52)R@KhL4U69I`#+_MN~Gu>p3YAgE<0$nZk|mU z@?9jhTr?25%W47P3e9ekTIdC0gfy#(1$0n1c$v2Ng+yZAfTYJPN0~i24dat&JGV|> z@{flmFX{>32NNQrOrqtDPXaC&36TxFJ12kYa3?>+=PX9%NM+=ZNoV9NYH{IrU2uoK z11S|IGA~V;Sx4rmZy0VcSO-^ETrz%biJ00}sx+ZR+Mvg>S4g|V(jyPb>kb?k2AO1| zAVE4%15Wp#yYtqT*NT9LL~K@NBKPF}98B1QL*GxHi-#k3 zPoEt-MAN14K!*&9cTuo-qhaC=O3ek5KtH6vo#r=&D3aXuip-WRGcwS7|BbE%47PEf zdNuW?j_)9PbQpF1N~PT@Lqb`(}lU4sxgp$%8r^(RQqm#fG+(FI^!2(`(wx zabjNcG!_)TY|i>EYv$Ct^2zV=FX&!8%k6A^M)xGbzoGltLXVkXdX_nX&+gza|9n3B z@X}jIEz=-WbUhJAYV+ZT7x{QM(5m;|#=m5e6${4#Y2 zBi+}ev^^NC^jN^RnURgTpO{N{`n=jjpIBw44|0u#C-F8`?@58<1Eipr8b*91Oz>D4OZtir_#wOW z>S?M!Ev!J(jnePkNVcfzQ))H_Iv7lA^dw8%SW z*XOr#zXPviC_D?02`?nbFoafehoGTtInGF2zYUG%EIqL5q?qD-KRx*<)8K7=DJoF) zVFjkEN%;v7>a5xB8UswZy@JL|zYe;=omUlt7dZg`$i5IZ$*)U!we}bDf#?tO0s3#u z2f1hS!3Aht*YVqYnEh=&P`0Lx?0lGaIbTXi>4oqT>12_Ryzd|!ew(8K>Uziv;}*G(CFj4=gGQeOu&0mm60!!ZamnTFzy4Q8@Q0;$*o&c74p5e zW=UJk58IHCDpcHh1kt?OuC=mtCScY&4H|z$fhX7(r44t|MQ*FPWD?C*3~ldSOmto{ zAf`Pv;G|Lspes(NS0E7m(#i?pTm7{mf@6%?S9(&@^{mC;?emt`rVF*k>6zlGO`NU& z38wV|KvD)VHa4#80k_YcT?I|JY#i~J+l+*=E)D@os|++AW_Om;vW$p2N;$*kejOw`KHf94_AgUI1n<;&N;8H`>dM z9N0hqVoX>W0k;*c2Mk8gg*u$j9H-ft=79}c z<5hxa`TaW^Y}7FoGpb(v-fB2#mF;L|JI$Ooz-GM9Q_7G!yxAJNqHON2W%9FuRJ_ zi2Qz{O+w50P#YxW&>rLb%X;b`uM zu7T|uj)NFMBdfF?zUhqbahS>TwR2_}lHORaj((`GAEvx+^7BGTa1BEqQ=)w1YY%=V z4j|CDu`hG;s@%F?trwZb&l7+FUC1`l8$m!$EE?5~esk+E_DC7ihz&B)(mgm`njt4{ zw9IE3v;ZE?7_NE*fVD_uw9wufDb0Nh@ZKX!?YCkJ61aTTov{-VL$a08PWyNxe>z@p zO?{Yg+fzOJ+1D)p>7)K>J`#iXP{+q1K>(|<{twLYO?1d1%bN9qi>nX1r1RC zrml0dtRjI6@22?lLvOJm?dlB=g5^T1J+`lISbuaVA89aTHXc~NZnH&Ucoz~|!Q@5f zncVCgVsG3UdH>BctWaD0$VCPkxT0(35clV4akVQgF)bbJnZV@;#A7+fwREe|^&eW1 zI(lXC+H)gU+;zNe-Y#E;d@8biR^f%VFtjaxjCcrp3+RD!8p(dCo$xdIe7A7gMi8rvfV~8Z=(X8v@S|JbhlEP3psJE4h8k(gC zA_0S9h2Jq}u=iKVP%*A+bk|kDfJ)(akE8^$vjpVM81|WVU*RTpRtqD=I@}uAHz&FS z*H6lQT8T;rb!o~#Kye-#?Pp_c{qA zwO)Qo5r@8mHnZWA{1bFXS<=&tj_J_AQByz;Xy~n|b$9!&U{&82(=w=C+dbD&Z&%0Z zS4@E!v;uBaFsDx%O z${3U;IL(hUwD*k$z#3niDb%OY$e~8lIA$a*BFHlME99DdNON?wAS|g6PUrkaR{2nd zaJ%JFJ-GcY4Ou{1IsyUIE-^^@Y}d!U*!7&A7o3s{l9bX0CFhzad8M3W7!ktvgw%#p zR>5YuJ;6Fj3h3q~?nQ#7Xxz@La8P5}s~q3Bx}koET5JF&&rx>xQ9!z`%C~LPWy!oa9Cxr_$bSTGV{W)<07CNJ%t(R9_XzYRnFMeF z3R(epwnC60WM5*;TG%HjD4k*MJ&?A-s|=$P#>c&-yiWIF7Zg6?iWuexV?tsznd+!> z?wHv**c$sl`}dN;n2+FAHkyLWjlo;-8(3NgTy<5KFSnymwhoX z4t_{n03!=y5<_zd2<>07iPB>7VG zo3T2xGtXjmpl)9?3~MPvjlJsw5<^pTB|=9!VGM2z3)=E{_$=OQ21n}`Q0P@(9Zh0v z+mYyEHEX%Vd6!)gy!+B~;7m|)e3JPE%t=`4JlFX>!! zK7p4*F&{77fZUDTOISMQ`1cSEzc}*Cx&I0H#(3~EzZ9496N2Z(4Dl1fzYPD1GsYbA zV?z`m#n_=?#}EjhlfDdePW&rOh4;_?>eo>N?YR)2UO@&N0t@V);rSc6-#L%pF^@lg zfE^PhK>h2Yf3=zatm(!5b@#)6-2JaU+nWs!0zi`1MPyp zAMCFx%&R{hEQ#t@A+!+2uho3n(8Z7l;XoA9{W>ozgz@Vk{@REBIk!@#UxgD)&@qWZ zNWY5z%5i_*O_%jo@!~(O`WM`R{drXdj$ehO!oTPL*KY;ukNJbWi%}GQP4e>guK5z* a8$W+g<h^J;FjPPJOp=_V8PwVZpfT#&$;$G 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 From 009e659f885790e8a45e1486202ba0ca8dd5a7b1 Mon Sep 17 00:00:00 2001 From: Matthew Gidden Date: Thu, 25 Jul 2019 12:00:44 +0200 Subject: [PATCH 13/70] add demand test --- message_ix/macro.py | 25 +++++++++++++++++++++++++ message_ix/tests/test_macro.py | 12 ++++++++++++ 2 files changed, 37 insertions(+) diff --git a/message_ix/macro.py b/message_ix/macro.py index 9016aa4e4..bbb67e3ef 100644 --- a/message_ix/macro.py +++ b/message_ix/macro.py @@ -3,6 +3,14 @@ import pandas as pd + +# +# TODOS: +# +# 1) all demands/prices assumed to be on USEFUL level, need to extend this +# to support others +# + MACRO_INIT = { 'sets': { 'sector': None, @@ -217,3 +225,20 @@ def _price(self): raise RuntimeError('NaN values found in price calculation') self.data['price'] = price return price + + def _demand(self): + # read from scenario + idx = ['node', 'sector', 'year'] + model_demand = self.s.var('DEMAND', filters={'level': 'useful'}) + model_demand.rename(columns={'lvl': 'value', 'commodity': 'sector'}, + inplace=True) + model_demand = model_demand[idx + ['value']] + # get data provided in base year from data + demand_ref = self.data['demand_ref'].reset_index() + demand_ref['year'] = self.base_year + # combine into one value + demand = pd.concat([demand_ref, model_demand]).set_index(idx)['value'] + if demand.isnull().any(): + raise RuntimeError('NaN values found in demand calculation') + self.data['demand'] = demand + return demand diff --git a/message_ix/tests/test_macro.py b/message_ix/tests/test_macro.py index ec5f141c1..21101255d 100644 --- a/message_ix/tests/test_macro.py +++ b/message_ix/tests/test_macro.py @@ -139,3 +139,15 @@ def test_calc_price(westeros_solved): obs = obs.values exp = np.array([195, 183.094376, 161.645111, 161.645111]) assert np.isclose(obs, exp).all() + + +def test_calc_demand(westeros_solved): + s = westeros_solved + c = macro.Calculate(s, DATA_PATH) + c.read_data() + obs = c._demand() + # 4 values, 3 in model period, one in history + assert(len(obs) == 4) + obs = obs.values + exp = np.array([90, 100, 150, 190]) + assert np.isclose(obs, exp).all() From 1b12e97836a9ab8aa9633a1c7f758df9ca76b527 Mon Sep 17 00:00:00 2001 From: Matthew Gidden Date: Thu, 25 Jul 2019 12:12:25 +0200 Subject: [PATCH 14/70] gdp0 added --- message_ix/macro.py | 34 +++++++++++++++++++++++++++++++--- message_ix/tests/test_macro.py | 18 ++++++++++++++++++ 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/message_ix/macro.py b/message_ix/macro.py index bbb67e3ef..94a415671 100644 --- a/message_ix/macro.py +++ b/message_ix/macro.py @@ -3,6 +3,7 @@ import pandas as pd +from functools import lru_cache # # TODOS: @@ -179,19 +180,28 @@ def derive_data(self): self._rho() self._k0() + @lru_cache() def _rho(self): esub = self.data['esub'] self.data['rho'] = (esub - 1) / esub return self.data['rho'] - def _k0(self): - kgdp = self.data['kgdp'] + @lru_cache() + def _gdp0(self): gdp = self.data['gdp_calibrate'] - # TODO: drop index level 'year' after?? gdp0 = gdp.iloc[gdp.index.isin([self.base_year], level='year')] + # get rid of year index + self.data['gdp0'] = gdp0.reset_index(level='year', drop=True) + return self.data['gdp0'] + + @lru_cache() + def _k0(self): + kgdp = self.data['kgdp'] + gdp0 = self._gdp0() self.data['k0'] = kgdp * gdp0 return self.data['k0'] + @lru_cache() def _total_cost(self): # read from scenario idx = ['node', 'year'] @@ -208,6 +218,7 @@ def _total_cost(self): self.data['total_cost'] = total_cost return total_cost + @lru_cache() def _price(self): # read from scenario idx = ['node', 'sector', 'year'] @@ -226,6 +237,7 @@ def _price(self): self.data['price'] = price return price + @lru_cache() def _demand(self): # read from scenario idx = ['node', 'sector', 'year'] @@ -242,3 +254,19 @@ def _demand(self): raise RuntimeError('NaN values found in demand calculation') self.data['demand'] = demand return demand + + @lru_cache() + def _bconst(self): + price_ref = self.data['price_ref'] + gdp = self.data['gdp_calibrate'] + # TODO: drop index level 'year' after?? + gdp0 = gdp.iloc[gdp.index.isin([self.base_year], level='year')] + demand_ref = self.data['demand_ref'] + rho = self._rho() + + print(price_ref) + print(gdp0) + print(demand_ref) + print(rho) + + # (p_ref[node, commodity]/1e3 * (gdp_calibrate[first_period, node] / demand_ref[node, commodity])**(rho[node] - 1)) diff --git a/message_ix/tests/test_macro.py b/message_ix/tests/test_macro.py index 21101255d..fe7a62310 100644 --- a/message_ix/tests/test_macro.py +++ b/message_ix/tests/test_macro.py @@ -106,6 +106,17 @@ def test_calc_rho(westeros_solved): assert obs == exp +def test_calc_gdp0(westeros_solved): + s = westeros_solved + c = macro.Calculate(s, DATA_PATH) + c.read_data() + obs = c._gdp0() + assert len(obs) == 1 + obs = obs[0] + exp = 5 + assert obs == exp + + def test_calc_k0(westeros_solved): s = westeros_solved c = macro.Calculate(s, DATA_PATH) @@ -151,3 +162,10 @@ def test_calc_demand(westeros_solved): obs = obs.values exp = np.array([90, 100, 150, 190]) assert np.isclose(obs, exp).all() + + +def test_calc_bconst(westeros_solved): + s = westeros_solved + c = macro.Calculate(s, DATA_PATH) + c.read_data() + obs = c._bconst() From dac763c1309f8453519bfd20bf6d867d7d184fdf Mon Sep 17 00:00:00 2001 From: Matthew Gidden Date: Thu, 25 Jul 2019 12:20:22 +0200 Subject: [PATCH 15/70] added bconst --- message_ix/macro.py | 17 +++++++---------- message_ix/tests/test_macro.py | 4 ++++ 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/message_ix/macro.py b/message_ix/macro.py index 94a415671..74ab41ae6 100644 --- a/message_ix/macro.py +++ b/message_ix/macro.py @@ -258,15 +258,12 @@ def _demand(self): @lru_cache() def _bconst(self): price_ref = self.data['price_ref'] - gdp = self.data['gdp_calibrate'] - # TODO: drop index level 'year' after?? - gdp0 = gdp.iloc[gdp.index.isin([self.base_year], level='year')] demand_ref = self.data['demand_ref'] rho = self._rho() - - print(price_ref) - print(gdp0) - print(demand_ref) - print(rho) - - # (p_ref[node, commodity]/1e3 * (gdp_calibrate[first_period, node] / demand_ref[node, commodity])**(rho[node] - 1)) + gdp0 = self._gdp0() + # TODO: is this calculation correct? + # in (value) ^ (rho - 1), should value = gdp0 / demand_ref + # or should value = price_ref * gdp0 / demand_ref / 1e3? + bconst = price_ref / 1e3 * (gdp0 / demand_ref) ** (rho - 1) + self.data['bconst'] = bconst + return self.data['bconst'] diff --git a/message_ix/tests/test_macro.py b/message_ix/tests/test_macro.py index fe7a62310..e856706fd 100644 --- a/message_ix/tests/test_macro.py +++ b/message_ix/tests/test_macro.py @@ -169,3 +169,7 @@ def test_calc_bconst(westeros_solved): c = macro.Calculate(s, DATA_PATH) c.read_data() obs = c._bconst() + assert len(obs) == 1 + obs = obs[0] + exp = 368465.76 + assert np.isclose(obs, exp) From 61e450d8717a472eb1f762626c840814ae192957 Mon Sep 17 00:00:00 2001 From: Matthew Gidden Date: Thu, 25 Jul 2019 14:40:04 +0200 Subject: [PATCH 16/70] update values to make them more inline with previous macro calculations --- message_ix/tests/test_macro.py | 6 +++--- tests/data/westeros_macro_input.xlsx | Bin 17111 -> 17107 bytes 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/message_ix/tests/test_macro.py b/message_ix/tests/test_macro.py index e856706fd..ae0b15d7e 100644 --- a/message_ix/tests/test_macro.py +++ b/message_ix/tests/test_macro.py @@ -113,7 +113,7 @@ def test_calc_gdp0(westeros_solved): obs = c._gdp0() assert len(obs) == 1 obs = obs[0] - exp = 5 + exp = 500 assert obs == exp @@ -124,7 +124,7 @@ def test_calc_k0(westeros_solved): obs = c._k0() assert len(obs) == 1 obs = obs[0] - exp = 15 + exp = 1500 assert obs == exp @@ -171,5 +171,5 @@ def test_calc_bconst(westeros_solved): obs = c._bconst() assert len(obs) == 1 obs = obs[0] - exp = 368465.76 + exp = 3.6846576e-05 assert np.isclose(obs, exp) diff --git a/tests/data/westeros_macro_input.xlsx b/tests/data/westeros_macro_input.xlsx index c4ff795c828a41423e095e38c757852f8839affc..36c687dcd55c04df186bda81d635e79c582bb160 100644 GIT binary patch delta 11949 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 JLc)(L{U6vrbdmr7 From 0739a0e02a1520e0f9ec6f2779902d3254dfda7e Mon Sep 17 00:00:00 2001 From: Matthew Gidden Date: Thu, 25 Jul 2019 14:48:30 +0200 Subject: [PATCH 17/70] added aconst --- message_ix/macro.py | 17 ++++++++++++++--- message_ix/tests/test_macro.py | 12 ++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/message_ix/macro.py b/message_ix/macro.py index 74ab41ae6..9be035d14 100644 --- a/message_ix/macro.py +++ b/message_ix/macro.py @@ -261,9 +261,20 @@ def _bconst(self): demand_ref = self.data['demand_ref'] rho = self._rho() gdp0 = self._gdp0() - # TODO: is this calculation correct? - # in (value) ^ (rho - 1), should value = gdp0 / demand_ref - # or should value = price_ref * gdp0 / demand_ref / 1e3? bconst = price_ref / 1e3 * (gdp0 / demand_ref) ** (rho - 1) self.data['bconst'] = bconst return self.data['bconst'] + + @lru_cache() + def _aconst(self): + bconst = self._bconst() + demand_ref = self.data['demand_ref'] + rho = self._rho() + gdp0 = self._gdp0() + k0 = self._k0() + kpvs = self.data['kpvs'] + # TODO: why name this partmp?? + partmp = (bconst * demand_ref ** rho).groupby(level='node').sum() + aconst = ((gdp0 / 1e3) ** rho - partmp) / (k0 / 1e3) ** (rho * kpvs) + self.data['aconst'] = aconst + return self.data['aconst'] diff --git a/message_ix/tests/test_macro.py b/message_ix/tests/test_macro.py index ae0b15d7e..054c8f177 100644 --- a/message_ix/tests/test_macro.py +++ b/message_ix/tests/test_macro.py @@ -173,3 +173,15 @@ def test_calc_bconst(westeros_solved): obs = obs[0] exp = 3.6846576e-05 assert np.isclose(obs, exp) + + +def test_calc_aconst(westeros_solved): + s = westeros_solved + c = macro.Calculate(s, DATA_PATH) + c.read_data() + obs = c._aconst() + + assert len(obs) == 1 + obs = obs[0] + exp = 26.027323 + assert np.isclose(obs, exp) From b417811b7cd6bdf156fe6c1980fbff718f69cd8a Mon Sep 17 00:00:00 2001 From: Matthew Gidden Date: Thu, 25 Jul 2019 15:43:42 +0200 Subject: [PATCH 18/70] add growth --- message_ix/macro.py | 67 +++++++++++++++++++++++----------- message_ix/tests/test_macro.py | 17 +++++++-- 2 files changed, 60 insertions(+), 24 deletions(-) diff --git a/message_ix/macro.py b/message_ix/macro.py index 9be035d14..d17b05760 100644 --- a/message_ix/macro.py +++ b/message_ix/macro.py @@ -72,26 +72,6 @@ ] -def init(s): - for key, values in MACRO_INIT['sets'].items(): - s.init_set(key, values) - for key, values in MACRO_INIT['pars'].items(): - s.init_par(key, values) - for key, values in MACRO_INIT['vars'].items(): - if not s.has_var(key): - try: - # TODO: this seems required because for some reason DEMAND (and - # perhaps others) seem to already be listed in the java code, - # but still needs to be initialized in the python code. However, - # you cannot init it with dimensions, only with the variable - # name. - s.init_var(key, values) - except: - s.init_var(key) - for key, values in MACRO_INIT['equs'].items(): - s.init_equ(key, values) - - def _validate_data(name, df, nodes, sectors, years): def validate(kind, values, df): if kind not in df: @@ -147,7 +127,7 @@ def __init__(self, s, data): if not s.has_solution(): raise RuntimeError('Scenario must have a solution to add MACRO') - demand = s.var('DEMAND') + demand = s.var('DEMAND', filters={'level': 'useful'}) self.nodes = demand['node'].unique() self.sectors = demand['commodity'].unique() self.years = demand['year'].unique() @@ -177,8 +157,27 @@ def read_data(self): self.base_year = max(data_years[data_years < min_year_model]) def derive_data(self): + # calculate all necessary derived data, adding to self.data this is + # done through method chaining, the bottom of which is aconst() + self._growth() self._rho() + self._gdp0() self._k0() + self._total_cost() + self._price() + self._demand() + self._bconst() + self._aconst() + + @lru_cache() + def _growth(self): + gdp = self.data['gdp_calibrate'] + diff = gdp.groupby(level='node').diff() + years = gdp.index.get_level_values('year') + dt = pd.Series(years, name='year', index=years).diff() + growth = (diff / gdp + 1) ** (1 / dt) - 1 + self.data['growth'] = growth.dropna() + return self.data['growth'] @lru_cache() def _rho(self): @@ -278,3 +277,29 @@ def _aconst(self): aconst = ((gdp0 / 1e3) ** rho - partmp) / (k0 / 1e3) ** (rho * kpvs) self.data['aconst'] = aconst return self.data['aconst'] + + +def init(s): + for key, values in MACRO_INIT['sets'].items(): + s.init_set(key, values) + for key, values in MACRO_INIT['pars'].items(): + s.init_par(key, values) + for key, values in MACRO_INIT['vars'].items(): + if not s.has_var(key): + try: + # TODO: this seems required because for some reason DEMAND (and + # perhaps others) seem to already be listed in the java code, + # but still needs to be initialized in the python code. However, + # you cannot init it with dimensions, only with the variable + # name. + s.init_var(key, values) + except: + s.init_var(key) + for key, values in MACRO_INIT['equs'].items(): + s.init_equ(key, values) + + +def add_model_data(s, data): + c = Calculate(s, data) + c.read_data() + c.derive_data() diff --git a/message_ix/tests/test_macro.py b/message_ix/tests/test_macro.py index 054c8f177..9111a4e34 100644 --- a/message_ix/tests/test_macro.py +++ b/message_ix/tests/test_macro.py @@ -95,6 +95,17 @@ def test_calc_data_missing_datapoint(westeros_solved): # +def test_calc_growth(westeros_solved): + s = westeros_solved + c = macro.Calculate(s, DATA_PATH) + c.read_data() + obs = c._growth() + assert len(obs) == 3 + obs = obs.values + exp = np.array([0.041380, 0.041380, 0.029186]) + assert np.isclose(obs, exp).all() + + def test_calc_rho(westeros_solved): s = westeros_solved c = macro.Calculate(s, DATA_PATH) @@ -134,7 +145,7 @@ def test_calc_total_cost(westeros_solved): c.read_data() obs = c._total_cost() # 4 values, 3 in model period, one in history - assert(len(obs) == 4) + assert len(obs) == 4 obs = obs.values exp = np.array([15, 17.477751, 22.143633, 28.189798]) assert np.isclose(obs, exp).all() @@ -146,7 +157,7 @@ def test_calc_price(westeros_solved): c.read_data() obs = c._price() # 4 values, 3 in model period, one in history - assert(len(obs) == 4) + assert len(obs) == 4 obs = obs.values exp = np.array([195, 183.094376, 161.645111, 161.645111]) assert np.isclose(obs, exp).all() @@ -158,7 +169,7 @@ def test_calc_demand(westeros_solved): c.read_data() obs = c._demand() # 4 values, 3 in model period, one in history - assert(len(obs) == 4) + assert len(obs) == 4 obs = obs.values exp = np.array([90, 100, 150, 190]) assert np.isclose(obs, exp).all() From b36e1b04cb8398401f32e455b3f7b150ffa54ec0 Mon Sep 17 00:00:00 2001 From: Matthew Gidden Date: Thu, 25 Jul 2019 15:56:37 +0200 Subject: [PATCH 19/70] added todos --- message_ix/macro.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/message_ix/macro.py b/message_ix/macro.py index d17b05760..350df9ab0 100644 --- a/message_ix/macro.py +++ b/message_ix/macro.py @@ -204,6 +204,8 @@ def _k0(self): def _total_cost(self): # read from scenario idx = ['node', 'year'] + # TODO: in the R code, this value is divided by 1000 + # do we need to do that here?!!? model_cost = self.s.var('COST_NODAL_NET') model_cost.rename(columns={'lvl': 'value'}, inplace=True) model_cost = model_cost[idx + ['value']] @@ -260,6 +262,7 @@ def _bconst(self): demand_ref = self.data['demand_ref'] rho = self._rho() gdp0 = self._gdp0() + # TODO: automatically get the units here!! bconst = price_ref / 1e3 * (gdp0 / demand_ref) ** (rho - 1) self.data['bconst'] = bconst return self.data['bconst'] @@ -274,6 +277,7 @@ def _aconst(self): kpvs = self.data['kpvs'] # TODO: why name this partmp?? partmp = (bconst * demand_ref ** rho).groupby(level='node').sum() + # TODO: automatically get the units here!! aconst = ((gdp0 / 1e3) ** rho - partmp) / (k0 / 1e3) ** (rho * kpvs) self.data['aconst'] = aconst return self.data['aconst'] From 245b3fc43f7f82e714eebd8afa256c13b4d2074b Mon Sep 17 00:00:00 2001 From: Matthew Gidden Date: Thu, 25 Jul 2019 16:41:14 +0200 Subject: [PATCH 20/70] initial steps to get parameters in --- message_ix/macro.py | 18 ++++++++++++--- message_ix/tests/test_macro.py | 41 +++++++++++++++++++++------------- 2 files changed, 40 insertions(+), 19 deletions(-) diff --git a/message_ix/macro.py b/message_ix/macro.py index 350df9ab0..b6c2b504e 100644 --- a/message_ix/macro.py +++ b/message_ix/macro.py @@ -22,6 +22,7 @@ 'price_MESSAGE': ['node', 'sector', 'year', ], 'cost_MESSAGE': ['node', 'year', ], 'gdp_calibrate': ['node', 'year', ], + 'historical_gdp': ['node', 'year', ], 'MERtoPPP': ['node', 'year', ], 'kgdp': ['node', ], 'kpvs': ['node', ], @@ -287,7 +288,10 @@ def init(s): for key, values in MACRO_INIT['sets'].items(): s.init_set(key, values) for key, values in MACRO_INIT['pars'].items(): - s.init_par(key, values) + try: + s.init_par(key, values) + except: + pass # already exists in the model, known for 'historical_gdp' for key, values in MACRO_INIT['vars'].items(): if not s.has_var(key): try: @@ -303,7 +307,15 @@ def init(s): s.init_equ(key, values) -def add_model_data(s, data): - c = Calculate(s, data) +def add_model_data(base, clone, data): + c = Calculate(base, data) c.read_data() c.derive_data() + + # TODO: we shouldn't have to have a for loop here + for s in c.sectors: + clone.add_set('sector', s) + + aeei = c.data['aeei'].reset_index() + aeei['unit'] = '-' + clone.add_par('aeei', aeei) diff --git a/message_ix/tests/test_macro.py b/message_ix/tests/test_macro.py index 9111a4e34..65c1d6d22 100644 --- a/message_ix/tests/test_macro.py +++ b/message_ix/tests/test_macro.py @@ -30,22 +30,6 @@ def westeros_not_solved(test_mp): return make_westeros(test_mp, solve=False) -def test_init(test_mp): - scen = Scenario(test_mp, *MSG_ARGS) - - scen = scen.clone('foo', 'bar') - scen.check_out() - macro.init(scen) - scen.commit('foo') - scen.solve() - - assert np.isclose(scen.var('OBJ')['lvl'], 153.675) - assert 'mapping_macro_sector' in scen.set_list() - assert 'aeei' in scen.par_list() - assert 'DEMAND' in scen.var_list() - assert 'COST_ACCOUNTING_NODAL' in scen.equ_list() - - def test_calc_valid_data_file(westeros_solved): s = westeros_solved c = macro.Calculate(s, DATA_PATH) @@ -196,3 +180,28 @@ def test_calc_aconst(westeros_solved): obs = obs[0] exp = 26.027323 assert np.isclose(obs, exp) + + +def test_init(test_mp): + scen = Scenario(test_mp, *MSG_ARGS) + + scen = scen.clone('foo', 'bar') + scen.check_out() + macro.init(scen) + scen.commit('foo') + scen.solve() + + assert np.isclose(scen.var('OBJ')['lvl'], 153.675) + assert 'mapping_macro_sector' in scen.set_list() + assert 'aeei' in scen.par_list() + assert 'DEMAND' in scen.var_list() + assert 'COST_ACCOUNTING_NODAL' in scen.equ_list() + + +def test_add_model_data(westeros_solved): + base = westeros_solved + clone = base.clone('foo', 'bar', keep_solution=False) + clone.check_out() + macro.init(clone) + macro.add_model_data(base, clone, DATA_PATH) + clone.commit('finished adding macro') From 4e9b83a143717f8fe64c2ef6a57440bf56ffdd85 Mon Sep 17 00:00:00 2001 From: Matthew Gidden Date: Thu, 25 Jul 2019 16:47:46 +0200 Subject: [PATCH 21/70] change dict structure to add units --- message_ix/macro.py | 62 +++++++++++++++++++++++++++++++-------------- 1 file changed, 43 insertions(+), 19 deletions(-) diff --git a/message_ix/macro.py b/message_ix/macro.py index b6c2b504e..1459fe92b 100644 --- a/message_ix/macro.py +++ b/message_ix/macro.py @@ -18,23 +18,40 @@ 'mapping_macro_sector': ['sector', 'commodity', 'level', ], }, 'pars': { - 'demand_MESSAGE': ['node', 'sector', 'year', ], - 'price_MESSAGE': ['node', 'sector', 'year', ], - 'cost_MESSAGE': ['node', 'year', ], - 'gdp_calibrate': ['node', 'year', ], - 'historical_gdp': ['node', 'year', ], - 'MERtoPPP': ['node', 'year', ], - 'kgdp': ['node', ], - 'kpvs': ['node', ], - 'depr': ['node', ], - 'drate': ['node', ], - 'esub': ['node', ], - 'lotol': ['node', ], - 'price_ref': ['node', 'sector', ], - 'lakl': ['node', ], - 'prfconst': ['node', 'sector', ], - 'grow': ['node', 'year', ], - 'aeei': ['node', 'sector', 'year', ], + 'demand_MESSAGE': {'idx': ['node', 'sector', 'year', ], + 'unit': ''}, + 'price_MESSAGE': {'idx': ['node', 'sector', 'year', ], + 'unit': ''}, + 'cost_MESSAGE': {'idx': ['node', 'year', ], + 'unit': ''}, + 'gdp_calibrate': {'idx': ['node', 'year', ], + 'unit': ''}, + 'historical_gdp': {'idx': ['node', 'year', ], + 'unit': ''}, + 'MERtoPPP': {'idx': ['node', 'year', ], + 'unit': ''}, + 'kgdp': {'idx': ['node', ], + 'unit': ''}, + 'kpvs': {'idx': ['node', ], + 'unit': ''}, + 'depr': {'idx': ['node', ], + 'unit': ''}, + 'drate': {'idx': ['node', ], + 'unit': ''}, + 'esub': {'idx': ['node', ], + 'unit': ''}, + 'lotol': {'idx': ['node', ], + 'unit': ''}, + 'price_ref': {'idx': ['node', 'sector', ], + 'unit': ''}, + 'lakl': {'idx': ['node', ], + 'unit': ''}, + 'prfconst': {'idx': ['node', 'sector', ], + 'unit': ''}, + 'grow': {'idx': ['node', 'year', ], + 'unit': ''}, + 'aeei': {'idx': ['node', 'sector', 'year', ], + 'unit': ''}, }, 'vars': { 'DEMAND': ['node', 'commodity', 'level', 'year', 'time', ], @@ -88,7 +105,8 @@ def validate(kind, values, df): if name in MACRO_DATA_FOR_DERIVATION: cols = MACRO_DATA_FOR_DERIVATION[name] else: - cols = MACRO_INIT['pars'][name] + cols = MACRO_INIT['pars'][name]['idx'] + # TODO: cols += ['unit'] ? col_diff = set(cols) - set(df.columns) if col_diff: msg = 'Missing expected columns for {}: {}' @@ -289,7 +307,7 @@ def init(s): s.init_set(key, values) for key, values in MACRO_INIT['pars'].items(): try: - s.init_par(key, values) + s.init_par(key, values['idx']) except: pass # already exists in the model, known for 'historical_gdp' for key, values in MACRO_INIT['vars'].items(): @@ -307,6 +325,12 @@ def init(s): s.init_equ(key, values) +ADD_PAR_UNITS = { + 'aeei': '=', + +} + + def add_model_data(base, clone, data): c = Calculate(base, data) c.read_data() From ef8d9e3f615e5b52728f558bf4f128f3a7e1a161 Mon Sep 17 00:00:00 2001 From: Matthew Gidden Date: Thu, 25 Jul 2019 16:57:07 +0200 Subject: [PATCH 22/70] add data_key to dict --- message_ix/macro.py | 127 ++++++++++++++++++++++----------- message_ix/tests/test_macro.py | 14 ++-- 2 files changed, 91 insertions(+), 50 deletions(-) diff --git a/message_ix/macro.py b/message_ix/macro.py index 1459fe92b..0e71a4eed 100644 --- a/message_ix/macro.py +++ b/message_ix/macro.py @@ -18,40 +18,81 @@ 'mapping_macro_sector': ['sector', 'commodity', 'level', ], }, 'pars': { - 'demand_MESSAGE': {'idx': ['node', 'sector', 'year', ], - 'unit': ''}, - 'price_MESSAGE': {'idx': ['node', 'sector', 'year', ], - 'unit': ''}, - 'cost_MESSAGE': {'idx': ['node', 'year', ], - 'unit': ''}, - 'gdp_calibrate': {'idx': ['node', 'year', ], - 'unit': ''}, - 'historical_gdp': {'idx': ['node', 'year', ], - 'unit': ''}, - 'MERtoPPP': {'idx': ['node', 'year', ], - 'unit': ''}, - 'kgdp': {'idx': ['node', ], - 'unit': ''}, - 'kpvs': {'idx': ['node', ], - 'unit': ''}, - 'depr': {'idx': ['node', ], - 'unit': ''}, - 'drate': {'idx': ['node', ], - 'unit': ''}, - 'esub': {'idx': ['node', ], - 'unit': ''}, - 'lotol': {'idx': ['node', ], - 'unit': ''}, - 'price_ref': {'idx': ['node', 'sector', ], - 'unit': ''}, - 'lakl': {'idx': ['node', ], - 'unit': ''}, - 'prfconst': {'idx': ['node', 'sector', ], - 'unit': ''}, - 'grow': {'idx': ['node', 'year', ], - 'unit': ''}, - 'aeei': {'idx': ['node', 'sector', 'year', ], - 'unit': ''}, + 'demand_MESSAGE': { + 'idx': ['node', 'sector', 'year', ], + 'unit': 'GWa', + 'data_key': 'demand', + }, + 'price_MESSAGE': { + 'idx': ['node', 'sector', 'year', ], + 'unit': 'USD/kWa', + 'data_key': 'price', + }, + 'cost_MESSAGE': { + 'idx': ['node', 'year', ], + 'unit': 'G$', + 'data_key': 'total_cost', + }, + 'gdp_calibrate': { + 'idx': ['node', 'year', ], + 'unit': 'T$', + }, + 'historical_gdp': { + 'idx': ['node', 'year', ], + 'unit': 'T$', + 'data_key': 'gdp0', + }, + 'MERtoPPP': { + 'idx': ['node', 'year', ], + 'unit': '-', + }, + 'kgdp': { + 'idx': ['node', ], + 'unit': '-', + }, + 'kpvs': { + 'idx': ['node', ], + 'unit': '-', + }, + 'depr': { + 'idx': ['node', ], + 'unit': '-', + }, + 'drate': { + 'idx': ['node', ], + 'unit': '-', + }, + 'esub': { + 'idx': ['node', ], + 'unit': '-', + }, + 'lotol': { + 'idx': ['node', ], + 'unit': '-', + }, + 'price_ref': { + 'idx': ['node', 'sector', ], + 'unit': 'USD/kWa', + }, + 'lakl': { + 'idx': ['node', ], + 'unit': '-', + 'data_key': 'aconst', + }, + 'prfconst': { + 'idx': ['node', 'sector', ], + 'unit': '-', + 'data_key': 'bconst', + }, + 'grow': { + 'idx': ['node', 'year', ], + 'unit': '-', + 'data_key': 'growth', + }, + 'aeei': { + 'idx': ['node', 'sector', 'year', ], + 'unit': '-', + }, }, 'vars': { 'DEMAND': ['node', 'commodity', 'level', 'year', 'time', ], @@ -325,12 +366,6 @@ def init(s): s.init_equ(key, values) -ADD_PAR_UNITS = { - 'aeei': '=', - -} - - def add_model_data(base, clone, data): c = Calculate(base, data) c.read_data() @@ -340,6 +375,12 @@ def add_model_data(base, clone, data): for s in c.sectors: clone.add_set('sector', s) - aeei = c.data['aeei'].reset_index() - aeei['unit'] = '-' - clone.add_par('aeei', aeei) + for name, values in MACRO_INIT['pars'].items(): + try: + key = values.get('data_key', name) + data = c.data[key].reset_index() + data['unit'] = values['unit'] + clone.add_par(name, data) + except Exception as e: + msg = 'Error in adding parameter {}\n'.format(name) + raise type(e)(msg + str(e)) diff --git a/message_ix/tests/test_macro.py b/message_ix/tests/test_macro.py index 65c1d6d22..89f2b6d8c 100644 --- a/message_ix/tests/test_macro.py +++ b/message_ix/tests/test_macro.py @@ -198,10 +198,10 @@ def test_init(test_mp): assert 'COST_ACCOUNTING_NODAL' in scen.equ_list() -def test_add_model_data(westeros_solved): - base = westeros_solved - clone = base.clone('foo', 'bar', keep_solution=False) - clone.check_out() - macro.init(clone) - macro.add_model_data(base, clone, DATA_PATH) - clone.commit('finished adding macro') +# def test_add_model_data(westeros_solved): +# base = westeros_solved +# clone = base.clone('foo', 'bar', keep_solution=False) +# clone.check_out() +# macro.init(clone) +# macro.add_model_data(base, clone, DATA_PATH) +# clone.commit('finished adding macro') From 3759263a6f8ca2e5c0dc65c1eb18a0cc43eaa879 Mon Sep 17 00:00:00 2001 From: Matthew Gidden Date: Thu, 25 Jul 2019 17:03:09 +0200 Subject: [PATCH 23/70] keep year in gdp0 index --- message_ix/macro.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/message_ix/macro.py b/message_ix/macro.py index 0e71a4eed..dbc9cf1e7 100644 --- a/message_ix/macro.py +++ b/message_ix/macro.py @@ -249,8 +249,7 @@ def _rho(self): def _gdp0(self): gdp = self.data['gdp_calibrate'] gdp0 = gdp.iloc[gdp.index.isin([self.base_year], level='year')] - # get rid of year index - self.data['gdp0'] = gdp0.reset_index(level='year', drop=True) + self.data['gdp0'] = gdp0 return self.data['gdp0'] @lru_cache() From 9f9a2dbf3052d2bd00ae374a1aa65122885a6ea0 Mon Sep 17 00:00:00 2001 From: Matthew Gidden Date: Thu, 25 Jul 2019 17:07:46 +0200 Subject: [PATCH 24/70] add test for adding macro data --- message_ix/macro.py | 1 + message_ix/tests/test_macro.py | 18 +++++++++++------- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/message_ix/macro.py b/message_ix/macro.py index dbc9cf1e7..432e5f335 100644 --- a/message_ix/macro.py +++ b/message_ix/macro.py @@ -236,6 +236,7 @@ def _growth(self): years = gdp.index.get_level_values('year') dt = pd.Series(years, name='year', index=years).diff() growth = (diff / gdp + 1) ** (1 / dt) - 1 + growth.name = 'value' self.data['growth'] = growth.dropna() return self.data['growth'] diff --git a/message_ix/tests/test_macro.py b/message_ix/tests/test_macro.py index 89f2b6d8c..7146bc8fc 100644 --- a/message_ix/tests/test_macro.py +++ b/message_ix/tests/test_macro.py @@ -198,10 +198,14 @@ def test_init(test_mp): assert 'COST_ACCOUNTING_NODAL' in scen.equ_list() -# def test_add_model_data(westeros_solved): -# base = westeros_solved -# clone = base.clone('foo', 'bar', keep_solution=False) -# clone.check_out() -# macro.init(clone) -# macro.add_model_data(base, clone, DATA_PATH) -# clone.commit('finished adding macro') +def test_add_model_data(westeros_solved): + base = westeros_solved + clone = base.clone('foo', 'bar', keep_solution=False) + clone.check_out() + macro.init(clone) + macro.add_model_data(base, clone, DATA_PATH) + clone.commit('finished adding macro') + clone.solve() + obs = clone.var('OBJ')['lvl'] + exp = base.var('OBJ')['lvl'] + assert np.isclose(obs, exp) From 34e107c96b9b17d9222512b61f3c95dfd830ed11 Mon Sep 17 00:00:00 2001 From: Matthew Gidden Date: Thu, 25 Jul 2019 17:11:00 +0200 Subject: [PATCH 25/70] rename that func --- message_ix/macro.py | 2 +- message_ix/tests/test_macro.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/message_ix/macro.py b/message_ix/macro.py index 432e5f335..49b6308f7 100644 --- a/message_ix/macro.py +++ b/message_ix/macro.py @@ -366,7 +366,7 @@ def init(s): s.init_equ(key, values) -def add_model_data(base, clone, data): +def add_macro_data(base, clone, data): c = Calculate(base, data) c.read_data() c.derive_data() diff --git a/message_ix/tests/test_macro.py b/message_ix/tests/test_macro.py index 7146bc8fc..591c9621f 100644 --- a/message_ix/tests/test_macro.py +++ b/message_ix/tests/test_macro.py @@ -198,12 +198,12 @@ def test_init(test_mp): assert 'COST_ACCOUNTING_NODAL' in scen.equ_list() -def test_add_model_data(westeros_solved): +def test_add_macro_data(westeros_solved): base = westeros_solved clone = base.clone('foo', 'bar', keep_solution=False) clone.check_out() macro.init(clone) - macro.add_model_data(base, clone, DATA_PATH) + macro.add_macro_data(base, clone, DATA_PATH) clone.commit('finished adding macro') clone.solve() obs = clone.var('OBJ')['lvl'] From a80306d6053fc75a783ba6682dbbf5a791ef77fb Mon Sep 17 00:00:00 2001 From: Matthew Gidden Date: Thu, 25 Jul 2019 17:18:56 +0200 Subject: [PATCH 26/70] starting calibration tests --- message_ix/macro.py | 11 ++++++++++- message_ix/tests/test_macro.py | 14 ++++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/message_ix/macro.py b/message_ix/macro.py index 49b6308f7..2ff01415e 100644 --- a/message_ix/macro.py +++ b/message_ix/macro.py @@ -366,7 +366,7 @@ def init(s): s.init_equ(key, values) -def add_macro_data(base, clone, data): +def add_model_data(base, clone, data): c = Calculate(base, data) c.read_data() c.derive_data() @@ -384,3 +384,12 @@ def add_macro_data(base, clone, data): except Exception as e: msg = 'Error in adding parameter {}\n'.format(name) raise type(e)(msg + str(e)) + + +def calibrate(s): + # run macro gams + # read aeei_calibrate and grow_calibrate + # add_par both + # check if calibration succeeded?? + s.solve(model='MACRO') + return s diff --git a/message_ix/tests/test_macro.py b/message_ix/tests/test_macro.py index 591c9621f..b9e8268a5 100644 --- a/message_ix/tests/test_macro.py +++ b/message_ix/tests/test_macro.py @@ -198,14 +198,24 @@ def test_init(test_mp): assert 'COST_ACCOUNTING_NODAL' in scen.equ_list() -def test_add_macro_data(westeros_solved): +def test_add_model_data(westeros_solved): base = westeros_solved clone = base.clone('foo', 'bar', keep_solution=False) clone.check_out() macro.init(clone) - macro.add_macro_data(base, clone, DATA_PATH) + macro.add_model_data(base, clone, DATA_PATH) clone.commit('finished adding macro') clone.solve() obs = clone.var('OBJ')['lvl'] exp = base.var('OBJ')['lvl'] assert np.isclose(obs, exp) + + +def test_calibrate(westeros_solved): + base = westeros_solved + clone = base.clone('foo', 'bar', keep_solution=False) + clone.check_out() + macro.init(clone) + macro.add_model_data(base, clone, DATA_PATH) + clone.commit('finished adding macro') + macro.calibrate(clone) From b8f1a027ef333a05df5f2ad521581ddec0379ded Mon Sep 17 00:00:00 2001 From: Matthew Gidden Date: Fri, 26 Jul 2019 11:10:07 +0200 Subject: [PATCH 27/70] I think this should work but it doesn't --- message_ix/macro.py | 36 ++++++++++++++++------ message_ix/model/MACRO/macro_data_load.gms | 3 +- message_ix/tests/test_macro.py | 5 ++- 3 files changed, 32 insertions(+), 12 deletions(-) diff --git a/message_ix/macro.py b/message_ix/macro.py index 2ff01415e..e255d11be 100644 --- a/message_ix/macro.py +++ b/message_ix/macro.py @@ -213,8 +213,10 @@ def read_data(self): 'Must provide gdp_calibrate data prior to the modeling' + ' period in order to calculate growth rates' ) - # base year is most recent period PRIOR to the modeled period - self.base_year = max(data_years[data_years < min_year_model]) + # init year is most recent period PRIOR to the modeled period + self.init_year = max(data_years[data_years < min_year_model]) + # base year is first model period + self.base_year = min_year_model def derive_data(self): # calculate all necessary derived data, adding to self.data this is @@ -249,7 +251,7 @@ def _rho(self): @lru_cache() def _gdp0(self): gdp = self.data['gdp_calibrate'] - gdp0 = gdp.iloc[gdp.index.isin([self.base_year], level='year')] + gdp0 = gdp.iloc[gdp.index.isin([self.init_year], level='year')] self.data['gdp0'] = gdp0 return self.data['gdp0'] @@ -269,9 +271,9 @@ def _total_cost(self): model_cost = self.s.var('COST_NODAL_NET') model_cost.rename(columns={'lvl': 'value'}, inplace=True) model_cost = model_cost[idx + ['value']] - # get data provided in base year from data + # get data provided in init year from data cost_ref = self.data['cost_ref'].reset_index() - cost_ref['year'] = self.base_year + cost_ref['year'] = self.init_year # combine into one value total_cost = pd.concat([cost_ref, model_cost]).set_index(idx)['value'] if total_cost.isnull().any(): @@ -288,9 +290,9 @@ def _price(self): model_price.rename(columns={'lvl': 'value', 'commodity': 'sector'}, inplace=True) model_price = model_price[idx + ['value']] - # get data provided in base year from data + # get data provided in init year from data price_ref = self.data['price_ref'].reset_index() - price_ref['year'] = self.base_year + price_ref['year'] = self.init_year # combine into one value price = pd.concat([price_ref, model_price]).set_index(idx)['value'] if price.isnull().any(): @@ -306,9 +308,9 @@ def _demand(self): model_demand.rename(columns={'lvl': 'value', 'commodity': 'sector'}, inplace=True) model_demand = model_demand[idx + ['value']] - # get data provided in base year from data + # get data provided in init year from data demand_ref = self.data['demand_ref'].reset_index() - demand_ref['year'] = self.base_year + demand_ref['year'] = self.init_year # combine into one value demand = pd.concat([demand_ref, model_demand]).set_index(idx)['value'] if demand.isnull().any(): @@ -365,16 +367,30 @@ def init(s): for key, values in MACRO_INIT['equs'].items(): s.init_equ(key, values) + try: + s.init_set("mapping_macro_sector", ("sector", "commodity", "level")) + except: # TODO: should this already exist? + pass # already exists + def add_model_data(base, clone, data): c = Calculate(base, data) c.read_data() c.derive_data() + # add sectoral set structure # TODO: we shouldn't have to have a for loop here for s in c.sectors: clone.add_set('sector', s) + clone.add_set("mapping_macro_sector", [s, s, "useful"]) + + # add temporal set structure + clone.add_set("type_year", "initializeyear_macro") + clone.add_set("cat_year", ["initializeyear_macro", c.init_year]) + clone.add_set("type_year", "baseyear_macro") + clone.add_set("cat_year", ["baseyear_macro", c.base_year]) + # add parameters for name, values in MACRO_INIT['pars'].items(): try: key = values.get('data_key', name) @@ -391,5 +407,5 @@ def calibrate(s): # read aeei_calibrate and grow_calibrate # add_par both # check if calibration succeeded?? - s.solve(model='MACRO') + s.solve(model='MACRO', var_list=["aeei_calibrate", "grow_calibrate"]) return s diff --git a/message_ix/model/MACRO/macro_data_load.gms b/message_ix/model/MACRO/macro_data_load.gms index 7252b9565..f3c750cb1 100755 --- a/message_ix/model/MACRO/macro_data_load.gms +++ b/message_ix/model/MACRO/macro_data_load.gms @@ -102,7 +102,8 @@ Parameters * parameters for spatially and temporally flexible formulation, and for myopic/rolling-horizon optimization duration_period_sum(year_all,year_all2) number of years between two periods ('year_all' must precede 'year_all2') interestrate(year_all) interest rate (to compute discount factor) - discountfactor(*) cumulative discount facor + df_period(year_all) cumulative discount factor over period duration + df_year(year_all) discount factor of the last year in the period ; *----------------------------------------------------------------------------------------------------------------------* diff --git a/message_ix/tests/test_macro.py b/message_ix/tests/test_macro.py index b9e8268a5..3fa68408e 100644 --- a/message_ix/tests/test_macro.py +++ b/message_ix/tests/test_macro.py @@ -213,9 +213,12 @@ def test_add_model_data(westeros_solved): def test_calibrate(westeros_solved): base = westeros_solved - clone = base.clone('foo', 'bar', keep_solution=False) + clone = base.clone(base.model, 'test macro calibration', + keep_solution=False) clone.check_out() macro.init(clone) macro.add_model_data(base, clone, DATA_PATH) clone.commit('finished adding macro') macro.calibrate(clone) + print(clone.var('UTILITY')) + print(clone.var('aeei_calibrate')) From cd191fe59601c4f304f3af82662cd6018d24945a Mon Sep 17 00:00:00 2001 From: Matthew Gidden Date: Fri, 26 Jul 2019 11:41:19 +0200 Subject: [PATCH 28/70] iteration now seems to work --- message_ix/macro.py | 55 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 48 insertions(+), 7 deletions(-) diff --git a/message_ix/macro.py b/message_ix/macro.py index e255d11be..a33342096 100644 --- a/message_ix/macro.py +++ b/message_ix/macro.py @@ -378,18 +378,23 @@ def add_model_data(base, clone, data): c.read_data() c.derive_data() - # add sectoral set structure - # TODO: we shouldn't have to have a for loop here - for s in c.sectors: - clone.add_set('sector', s) - clone.add_set("mapping_macro_sector", [s, s, "useful"]) - # add temporal set structure clone.add_set("type_year", "initializeyear_macro") clone.add_set("cat_year", ["initializeyear_macro", c.init_year]) clone.add_set("type_year", "baseyear_macro") clone.add_set("cat_year", ["baseyear_macro", c.base_year]) + # add nodal set structure + clone.add_set("type_node", "economy") + for n in c.nodes: + clone.add_set("cat_node", ["economy", n]) + + # add sectoral set structure + # TODO: we shouldn't have to have a for loop here + for s in c.sectors: + clone.add_set('sector', s) + clone.add_set("mapping_macro_sector", [s, s, "useful"]) + # add parameters for name, values in MACRO_INIT['pars'].items(): try: @@ -407,5 +412,41 @@ def calibrate(s): # read aeei_calibrate and grow_calibrate # add_par both # check if calibration succeeded?? - s.solve(model='MACRO', var_list=["aeei_calibrate", "grow_calibrate"]) + + # solve MACRO standalone to get calibrated values + var_list = ["UTILITY", "aeei_calibrate", "grow_calibrate"] + s.solve(model='MACRO', var_list=var_list) + util = s.var('UTILITY')['lvl'] + raw_aeei = s.var('aeei_calibrate') + aeei = (raw_aeei + .rename(columns={'lvl': 'value'}) + .drop('mrg', axis=1) + ) + aeei['unit'] = MACRO_INIT['pars']['aeei']['unit'] + raw_grow = s.var('grow_calibrate') + grow = (raw_grow + .rename(columns={'lvl': 'value'}) + .drop('mrg', axis=1) + ) + grow['unit'] = MACRO_INIT['pars']['grow']['unit'] + + # update parameters + s.remove_solution() + s.check_out() + print('FOO') + print(s.par('aeei')) + print(s.par('grow')) + s.add_par('aeei', aeei) + s.add_par('grow', grow) + print('BAR') + print(s.par('aeei')) + print(s.par('grow')) + s.commit('Updating MACRO values after calibration') + + # test to make sure number of iterations is 1 + test = s.clone(s.model, 'test to confirm MACRO converges') + test.solve(model='MACRO', var_list=var_list) + pd.testing.assert_frame_equal(raw_aeei, test.var('aeei_calibrate')) + pd.testing.assert_frame_equal(raw_grow, test.var('grow_calibrate')) + return s From 5297f807c519a637e067f895ba58fc985bcf7ee5 Mon Sep 17 00:00:00 2001 From: Matthew Gidden Date: Fri, 26 Jul 2019 11:59:55 +0200 Subject: [PATCH 29/70] keep track of iterations --- message_ix/macro.py | 26 ++++++++++++-------- message_ix/model/MACRO/macro_calibration.gms | 4 ++- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/message_ix/macro.py b/message_ix/macro.py index a33342096..d11c04190 100644 --- a/message_ix/macro.py +++ b/message_ix/macro.py @@ -372,6 +372,9 @@ def init(s): except: # TODO: should this already exist? pass # already exists + # keep track of number of iterations + s.init_var('NITER', None) + def add_model_data(base, clone, data): c = Calculate(base, data) @@ -414,9 +417,11 @@ def calibrate(s): # check if calibration succeeded?? # solve MACRO standalone to get calibrated values - var_list = ["UTILITY", "aeei_calibrate", "grow_calibrate"] - s.solve(model='MACRO', var_list=var_list) + var_list = ['UTILITY', 'NITER', 'aeei_calibrate', 'grow_calibrate'] + gams_args = ['LogOption=2'] # pass everything to log file + s.solve(model='MACRO', var_list=var_list, gams_args=gams_args) util = s.var('UTILITY')['lvl'] + niter = s.var('NITER')['lvl'] raw_aeei = s.var('aeei_calibrate') aeei = (raw_aeei .rename(columns={'lvl': 'value'}) @@ -433,19 +438,20 @@ def calibrate(s): # update parameters s.remove_solution() s.check_out() - print('FOO') - print(s.par('aeei')) - print(s.par('grow')) s.add_par('aeei', aeei) s.add_par('grow', grow) - print('BAR') - print(s.par('aeei')) - print(s.par('grow')) s.commit('Updating MACRO values after calibration') # test to make sure number of iterations is 1 - test = s.clone(s.model, 'test to confirm MACRO converges') - test.solve(model='MACRO', var_list=var_list) + test = s.clone(s.model, 'test to confirm MACRO converges',) + test.solve(model='MACRO', var_list=var_list, gams_args=gams_args) + + if test.var('NITER')['lvl'] > 1: + msg = 'Number of iterations after calibration > 1: {}' + raise RuntimeError(msg.format(test.var('NITER')['lvl'])) + + # TOO other checks? + print('FOO', niter, test.var('NITER')) pd.testing.assert_frame_equal(raw_aeei, test.var('aeei_calibrate')) pd.testing.assert_frame_equal(raw_grow, test.var('grow_calibrate')) diff --git a/message_ix/model/MACRO/macro_calibration.gms b/message_ix/model/MACRO/macro_calibration.gms index 14379b0e7..4bbee3b8a 100644 --- a/message_ix/model/MACRO/macro_calibration.gms +++ b/message_ix/model/MACRO/macro_calibration.gms @@ -15,6 +15,7 @@ Scalar Variables aeei_calibrate(node,sector,year_all) grow_calibrate(node,year_all) + NITER ; * ------------------------------------------------------------------------------ @@ -93,7 +94,8 @@ finite_time_corr(node_macro, year) = abs(DRATE(node_macro) - grow(node_macro, ye * export calibration results as reporting variables to GDX aeei_calibrate.L(node_macro,sector,year) = aeei(node_macro,sector,year) ; grow_calibrate.L(node_macro,year) = grow(node_macro,year) ; - +NITER.L = ctr ; + * write solution statistics status('MESSAGE_MACRO','modelstat') = 1 ; status('MESSAGE_MACRO','solvestat') = 1 ; From d5cf804842a696a1e87a3bd09eccdb94636ff281 Mon Sep 17 00:00:00 2001 From: Matthew Gidden Date: Fri, 26 Jul 2019 12:43:46 +0200 Subject: [PATCH 30/70] update fixture scope --- message_ix/tests/test_macro.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/message_ix/tests/test_macro.py b/message_ix/tests/test_macro.py index 3fa68408e..385ed2bfb 100644 --- a/message_ix/tests/test_macro.py +++ b/message_ix/tests/test_macro.py @@ -20,12 +20,12 @@ DATA_PATH = Path(__file__).parent / 'data' / 'westeros_macro_input.xlsx' -@pytest.fixture(scope='module') +@pytest.fixture(scope='function') def westeros_solved(test_mp): return make_westeros(test_mp, solve=True) -@pytest.fixture(scope='module') +@pytest.fixture(scope='function') def westeros_not_solved(test_mp): return make_westeros(test_mp, solve=False) From 9f2a2852113144fbb8fae04fdf40d10d7d6431e4 Mon Sep 17 00:00:00 2001 From: Matthew Gidden Date: Fri, 26 Jul 2019 12:44:22 +0200 Subject: [PATCH 31/70] add comment on scope --- message_ix/tests/test_macro.py | 1 + 1 file changed, 1 insertion(+) diff --git a/message_ix/tests/test_macro.py b/message_ix/tests/test_macro.py index 385ed2bfb..c1eaeb694 100644 --- a/message_ix/tests/test_macro.py +++ b/message_ix/tests/test_macro.py @@ -20,6 +20,7 @@ DATA_PATH = Path(__file__).parent / 'data' / 'westeros_macro_input.xlsx' +# TODO: what scope should these be? @pytest.fixture(scope='function') def westeros_solved(test_mp): return make_westeros(test_mp, solve=True) From d75baf11b437e7701bee2006d06d38325f6ba754 Mon Sep 17 00:00:00 2001 From: Matthew Gidden Date: Fri, 26 Jul 2019 13:07:02 +0200 Subject: [PATCH 32/70] calibration now tested appropriately --- message_ix/macro.py | 39 +++++++++++--------- message_ix/model/MACRO/macro_calibration.gms | 8 +++- message_ix/tests/test_macro.py | 16 +++++++- 3 files changed, 42 insertions(+), 21 deletions(-) diff --git a/message_ix/macro.py b/message_ix/macro.py index d11c04190..60eb10d85 100644 --- a/message_ix/macro.py +++ b/message_ix/macro.py @@ -5,6 +5,8 @@ from functools import lru_cache +from ixmp.utils import logger + # # TODOS: # @@ -373,7 +375,8 @@ def init(s): pass # already exists # keep track of number of iterations - s.init_var('NITER', None) + s.init_var('N_ITER', None) + s.init_var('MAX_ITER', None) def add_model_data(base, clone, data): @@ -416,12 +419,16 @@ def calibrate(s): # add_par both # check if calibration succeeded?? - # solve MACRO standalone to get calibrated values - var_list = ['UTILITY', 'NITER', 'aeei_calibrate', 'grow_calibrate'] + # solve MACRO standalone + var_list = ['N_ITER', 'MAX_ITER', 'aeei_calibrate', 'grow_calibrate'] gams_args = ['LogOption=2'] # pass everything to log file s.solve(model='MACRO', var_list=var_list, gams_args=gams_args) - util = s.var('UTILITY')['lvl'] - niter = s.var('NITER')['lvl'] + n_iter = s.var('N_ITER')['lvl'] + max_iter = s.var('MAX_ITER')['lvl'] + msg = 'MACRO converged after {} of a maximum of {} iterations' + logger().info(msg.format(n_iter, max_iter)) + + # get out calibrated values raw_aeei = s.var('aeei_calibrate') aeei = (raw_aeei .rename(columns={'lvl': 'value'}) @@ -435,24 +442,22 @@ def calibrate(s): ) grow['unit'] = MACRO_INIT['pars']['grow']['unit'] - # update parameters + # update calibrated value parameters s.remove_solution() s.check_out() s.add_par('aeei', aeei) s.add_par('grow', grow) s.commit('Updating MACRO values after calibration') - # test to make sure number of iterations is 1 - test = s.clone(s.model, 'test to confirm MACRO converges',) - test.solve(model='MACRO', var_list=var_list, gams_args=gams_args) - - if test.var('NITER')['lvl'] > 1: - msg = 'Number of iterations after calibration > 1: {}' - raise RuntimeError(msg.format(test.var('NITER')['lvl'])) + # # test to make sure number of iterations is 1 + # test = s.clone(s.model, 'test to confirm MACRO converges',) + # var_list = ['N_ITER', 'MAX_ITER'] + # test.solve(model='MESSAGE-MACRO', var_list=var_list, gams_args=gams_args) - # TOO other checks? - print('FOO', niter, test.var('NITER')) - pd.testing.assert_frame_equal(raw_aeei, test.var('aeei_calibrate')) - pd.testing.assert_frame_equal(raw_grow, test.var('grow_calibrate')) + # n_iter = s.var('N_ITER')['lvl'] + # max_iter = s.var('MAX_ITER')['lvl'] + # if test.var('N_ITER')['lvl'] > 1: + # msg = 'Number of iterations after calibration > 1: {}' + # raise RuntimeError(msg.format(test.var('NITER')['lvl'])) return s diff --git a/message_ix/model/MACRO/macro_calibration.gms b/message_ix/model/MACRO/macro_calibration.gms index 4bbee3b8a..5503c8b28 100644 --- a/message_ix/model/MACRO/macro_calibration.gms +++ b/message_ix/model/MACRO/macro_calibration.gms @@ -15,7 +15,8 @@ Scalar Variables aeei_calibrate(node,sector,year_all) grow_calibrate(node,year_all) - NITER + N_ITER + MAX_ITER ; * ------------------------------------------------------------------------------ @@ -94,7 +95,10 @@ finite_time_corr(node_macro, year) = abs(DRATE(node_macro) - grow(node_macro, ye * export calibration results as reporting variables to GDX aeei_calibrate.L(node_macro,sector,year) = aeei(node_macro,sector,year) ; grow_calibrate.L(node_macro,year) = grow(node_macro,year) ; -NITER.L = ctr ; + +* subtract one due to 1-based indexing +N_ITER.L = ctr - 1; +MAX_ITER.L = max_it ; * write solution statistics status('MESSAGE_MACRO','modelstat') = 1 ; diff --git a/message_ix/tests/test_macro.py b/message_ix/tests/test_macro.py index c1eaeb694..92a053b60 100644 --- a/message_ix/tests/test_macro.py +++ b/message_ix/tests/test_macro.py @@ -219,7 +219,19 @@ def test_calibrate(westeros_solved): clone.check_out() macro.init(clone) macro.add_model_data(base, clone, DATA_PATH) + + start_aeei = clone.par('aeei')['value'] + start_grow = clone.par('grow')['value'] + clone.commit('finished adding macro') macro.calibrate(clone) - print(clone.var('UTILITY')) - print(clone.var('aeei_calibrate')) + + end_aeei = clone.par('aeei')['value'] + end_grow = clone.par('grow')['value'] + + # calibration should have changed some/all of these values and none should + # be NaNs + assert not np.allclose(start_aeei, end_aeei) + assert not np.allclose(start_grow, end_grow) + assert not end_aeei.isnull().any() + assert not end_grow.isnull().any() From 8052e9d7ef095baabfa0fb283b8a983cf0a4bd59 Mon Sep 17 00:00:00 2001 From: Matthew Gidden Date: Fri, 26 Jul 2019 13:10:30 +0200 Subject: [PATCH 33/70] a bit more testing --- message_ix/tests/test_macro.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/message_ix/tests/test_macro.py b/message_ix/tests/test_macro.py index 92a053b60..d4c40c71e 100644 --- a/message_ix/tests/test_macro.py +++ b/message_ix/tests/test_macro.py @@ -231,7 +231,7 @@ def test_calibrate(westeros_solved): # calibration should have changed some/all of these values and none should # be NaNs - assert not np.allclose(start_aeei, end_aeei) - assert not np.allclose(start_grow, end_grow) + assert not np.allclose(start_aeei, end_aeei, rtol=1e-2) + assert not np.allclose(start_grow, end_grow, rtol=1e-2) assert not end_aeei.isnull().any() assert not end_grow.isnull().any() From 5c0ac089579326adae55837d9d662da116a350d8 Mon Sep 17 00:00:00 2001 From: Matthew Gidden Date: Fri, 26 Jul 2019 13:25:00 +0200 Subject: [PATCH 34/70] now check for number of iterations --- message_ix/macro.py | 30 ++++++++++++-------------- message_ix/model/MESSAGE-MACRO_run.gms | 15 +++++++++++++ message_ix/tests/test_macro.py | 2 +- 3 files changed, 30 insertions(+), 17 deletions(-) diff --git a/message_ix/macro.py b/message_ix/macro.py index 60eb10d85..29db4d09b 100644 --- a/message_ix/macro.py +++ b/message_ix/macro.py @@ -1,5 +1,6 @@ import collections import os +import warnings import pandas as pd @@ -413,12 +414,7 @@ def add_model_data(base, clone, data): raise type(e)(msg + str(e)) -def calibrate(s): - # run macro gams - # read aeei_calibrate and grow_calibrate - # add_par both - # check if calibration succeeded?? - +def calibrate(s, check_convergence=True): # solve MACRO standalone var_list = ['N_ITER', 'MAX_ITER', 'aeei_calibrate', 'grow_calibrate'] gams_args = ['LogOption=2'] # pass everything to log file @@ -449,15 +445,17 @@ def calibrate(s): s.add_par('grow', grow) s.commit('Updating MACRO values after calibration') - # # test to make sure number of iterations is 1 - # test = s.clone(s.model, 'test to confirm MACRO converges',) - # var_list = ['N_ITER', 'MAX_ITER'] - # test.solve(model='MESSAGE-MACRO', var_list=var_list, gams_args=gams_args) - - # n_iter = s.var('N_ITER')['lvl'] - # max_iter = s.var('MAX_ITER')['lvl'] - # if test.var('N_ITER')['lvl'] > 1: - # msg = 'Number of iterations after calibration > 1: {}' - # raise RuntimeError(msg.format(test.var('NITER')['lvl'])) + # test to make sure number of iterations is 1 + test = s.clone(s.model, 'test to confirm MACRO converges',) + var_list = ['N_ITER'] + test.solve(model='MESSAGE-MACRO', var_list=var_list, gams_args=gams_args) + + n_iter = test.var('N_ITER')['lvl'] + if n_iter > 1: + msg = 'Number of iterations after calibration > 1: {}' + if check_convergence: + raise RuntimeError(msg.format(n_iter)) + else: + warnings.warn(msg.format(n_iter)) return s diff --git a/message_ix/model/MESSAGE-MACRO_run.gms b/message_ix/model/MESSAGE-MACRO_run.gms index d518cbc87..9895cd039 100755 --- a/message_ix/model/MESSAGE-MACRO_run.gms +++ b/message_ix/model/MESSAGE-MACRO_run.gms @@ -91,6 +91,8 @@ Scalar max_adjustment maximum adjustment in current iteration convergence_status status of convergence (1 if successful) / 0 / scaling scaling factor to adjust step size when iteration oscillates / 1 / + max_it /%MAX_ITERATION%/ + ctr /0/ ; * declarations moved from solve files to avoid inclusion in loop @@ -108,6 +110,13 @@ Parameters report_iteration(iteration,*) ; +* variables to report back to user if needed +Variables + N_ITER + MAX_ITER +; + + price_init(node,sector,year_all) = 0 ; *----------------------------------------------------------------------------------------------------------------------* @@ -134,6 +143,8 @@ LOOP(iteration, put_utility 'log' /"+++ Starting iteration ", ORD(iteration):0:0, " of MESSAGEix-MACRO... +++ " ; +ctr = ctr + 1 ; + *----------------------------------------------------------------------------------------------------------------------* * solve MESSAGE model * *----------------------------------------------------------------------------------------------------------------------* @@ -274,6 +285,10 @@ $INCLUDE includes/aux_computation_time.gms * post-processing and export to gdx * *----------------------------------------------------------------------------------------------------------------------* +N_ITER.L = ctr ; +MAX_ITER.L = max_it ; + + $INCLUDE MESSAGE/reporting.gms * dump all input data, processed data and results to a gdx file (with additional comment as name extension if provided) diff --git a/message_ix/tests/test_macro.py b/message_ix/tests/test_macro.py index d4c40c71e..114581432 100644 --- a/message_ix/tests/test_macro.py +++ b/message_ix/tests/test_macro.py @@ -224,7 +224,7 @@ def test_calibrate(westeros_solved): start_grow = clone.par('grow')['value'] clone.commit('finished adding macro') - macro.calibrate(clone) + macro.calibrate(clone, check_convergence=True) end_aeei = clone.par('aeei')['value'] end_grow = clone.par('grow')['value'] From 1cf77c8f6a35f809c801d2113669a10fd003bf1f Mon Sep 17 00:00:00 2001 From: Matthew Gidden Date: Fri, 26 Jul 2019 13:35:44 +0200 Subject: [PATCH 35/70] finishing up initial pr --- message_ix/core.py | 12 ++++++++++++ message_ix/tests/test_macro.py | 9 ++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/message_ix/core.py b/message_ix/core.py index 3debfe8d9..7c316cf9a 100755 --- a/message_ix/core.py +++ b/message_ix/core.py @@ -6,6 +6,8 @@ from ixmp.utils import as_str_list, pd_read, pd_write, isscalar, logger import pandas as pd +from . import macro + class Scenario(ixmp.Scenario): """|MESSAGEix| Scenario. @@ -427,6 +429,16 @@ def solve(self, model='MESSAGE', solve_options={}, **kwargs): """ super().solve(model=model, solve_options=solve_options, **kwargs) + def add_macro(self, data, scenario=None, check_convergence=True): + scenario = scenario or '_'.join([self.scenario, 'macro']) + clone = self.clone(self.model, scenario, keep_solution=False) + clone.check_out() + macro.init(clone) + macro.add_model_data(self, clone, data) + clone.commit('finished adding macro') + macro.calibrate(clone, check_convergence=check_convergence) + return clone + def rename(self, name, mapping, keep=False): """Rename an element in a set diff --git a/message_ix/tests/test_macro.py b/message_ix/tests/test_macro.py index 114581432..f9d766f8f 100644 --- a/message_ix/tests/test_macro.py +++ b/message_ix/tests/test_macro.py @@ -219,11 +219,13 @@ def test_calibrate(westeros_solved): clone.check_out() macro.init(clone) macro.add_model_data(base, clone, DATA_PATH) + clone.commit('finished adding macro') start_aeei = clone.par('aeei')['value'] start_grow = clone.par('grow')['value'] - clone.commit('finished adding macro') + # TODO: this fails now because it takes 4 iterations to converge, don't know + # why, but the original macro-only iterations hit a max value (100) macro.calibrate(clone, check_convergence=True) end_aeei = clone.par('aeei')['value'] @@ -235,3 +237,8 @@ def test_calibrate(westeros_solved): assert not np.allclose(start_grow, end_grow, rtol=1e-2) assert not end_aeei.isnull().any() assert not end_grow.isnull().any() + + +def test_calibrate_roundtrip(westeros_solved): + # TODO: make this true when we address above issue + westeros_solved.add_macro(DATA_PATH, check_convergence=False) From 9e25787b156fcc3539543ca851de9370e1ff9bbd Mon Sep 17 00:00:00 2001 From: Matthew Gidden Date: Fri, 26 Jul 2019 14:33:14 +0200 Subject: [PATCH 36/70] add to release notes --- RELEASE_NOTES.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASE_NOTES.rst b/RELEASE_NOTES.rst index 50824f91a..5a113a285 100644 --- a/RELEASE_NOTES.rst +++ b/RELEASE_NOTES.rst @@ -6,6 +6,7 @@ All changes - `#313 `_: Include all tests in the message_ix package. - `#307 `_: adjust to deprecations in ixmp 2.0. +- `#223 `_: Add methods for parametrization and calibration of MACRO based on an existing MESSAGE Scenario. v2.0.0 (2020-01-14) From 2308f1e6c37cf634496a00c4aacae0389e548858 Mon Sep 17 00:00:00 2001 From: Matthew Gidden Date: Fri, 26 Jul 2019 14:45:07 +0200 Subject: [PATCH 37/70] stickler --- message_ix/macro.py | 14 +++++++------- message_ix/tests/test_macro.py | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/message_ix/macro.py b/message_ix/macro.py index 29db4d09b..f3e00aa6c 100644 --- a/message_ix/macro.py +++ b/message_ix/macro.py @@ -114,7 +114,6 @@ 'PHYSENE': ['node', 'sector', 'year', ], 'PRODENE': ['node', 'sector', 'year', ], 'NEWENE': ['node', 'sector', 'year', ], - 'EC': ['node', 'year', ], 'grow_calibrate': ['node', 'year', ], 'aeei_calibrate': ['node', 'sector', 'year', ], }, @@ -354,25 +353,26 @@ def init(s): for key, values in MACRO_INIT['pars'].items(): try: s.init_par(key, values['idx']) - except: + except: # noqa: ignore=E722 pass # already exists in the model, known for 'historical_gdp' for key, values in MACRO_INIT['vars'].items(): if not s.has_var(key): try: # TODO: this seems required because for some reason DEMAND (and # perhaps others) seem to already be listed in the java code, - # but still needs to be initialized in the python code. However, - # you cannot init it with dimensions, only with the variable - # name. + # but still needs to be initialized in the python code. + # However, you cannot init it with dimensions, only with the + # variable name. s.init_var(key, values) - except: + except: # noqa: ignore=E722 s.init_var(key) for key, values in MACRO_INIT['equs'].items(): s.init_equ(key, values) try: + # TODO: should this already exist? s.init_set("mapping_macro_sector", ("sector", "commodity", "level")) - except: # TODO: should this already exist? + except: # noqa: ignore=E722 pass # already exists # keep track of number of iterations diff --git a/message_ix/tests/test_macro.py b/message_ix/tests/test_macro.py index f9d766f8f..0fd4c5549 100644 --- a/message_ix/tests/test_macro.py +++ b/message_ix/tests/test_macro.py @@ -224,8 +224,8 @@ def test_calibrate(westeros_solved): start_aeei = clone.par('aeei')['value'] start_grow = clone.par('grow')['value'] - # TODO: this fails now because it takes 4 iterations to converge, don't know - # why, but the original macro-only iterations hit a max value (100) + # TODO: this fails now because it takes 4 iterations to converge, don't + # know why, but the original macro-only iterations hit a max value (100) macro.calibrate(clone, check_convergence=True) end_aeei = clone.par('aeei')['value'] From 65aa58b72912be0bb728855bdef0d869ba4406b2 Mon Sep 17 00:00:00 2001 From: Matthew Gidden Date: Fri, 26 Jul 2019 14:48:10 +0200 Subject: [PATCH 38/70] stickler --- message_ix/macro.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/message_ix/macro.py b/message_ix/macro.py index f3e00aa6c..fcc5d82bc 100644 --- a/message_ix/macro.py +++ b/message_ix/macro.py @@ -211,10 +211,9 @@ def read_data(self): min_year_model = min(self.years) min_year_data = min(data_years) if not min_year_data < min_year_model: - raise ValueError( - 'Must provide gdp_calibrate data prior to the modeling' + + msg = 'Must provide gdp_calibrate data prior to the modeling' + \ ' period in order to calculate growth rates' - ) + raise ValueError(msg) # init year is most recent period PRIOR to the modeled period self.init_year = max(data_years[data_years < min_year_model]) # base year is first model period From 08fac5f257c7b8994d51808860a2f7c14e4ee151 Mon Sep 17 00:00:00 2001 From: Matthew Gidden Date: Fri, 26 Jul 2019 14:54:44 +0200 Subject: [PATCH 39/70] final cleanup --- message_ix/macro.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/message_ix/macro.py b/message_ix/macro.py index fcc5d82bc..153e0d69a 100644 --- a/message_ix/macro.py +++ b/message_ix/macro.py @@ -424,14 +424,12 @@ def calibrate(s, check_convergence=True): logger().info(msg.format(n_iter, max_iter)) # get out calibrated values - raw_aeei = s.var('aeei_calibrate') - aeei = (raw_aeei + aeei = (s.var('aeei_calibrate') .rename(columns={'lvl': 'value'}) .drop('mrg', axis=1) ) aeei['unit'] = MACRO_INIT['pars']['aeei']['unit'] - raw_grow = s.var('grow_calibrate') - grow = (raw_grow + grow = (s.var('grow_calibrate') .rename(columns={'lvl': 'value'}) .drop('mrg', axis=1) ) @@ -445,16 +443,16 @@ def calibrate(s, check_convergence=True): s.commit('Updating MACRO values after calibration') # test to make sure number of iterations is 1 - test = s.clone(s.model, 'test to confirm MACRO converges',) + test = s.clone(s.model, 'test to confirm MACRO converges') var_list = ['N_ITER'] test.solve(model='MESSAGE-MACRO', var_list=var_list, gams_args=gams_args) n_iter = test.var('N_ITER')['lvl'] if n_iter > 1: - msg = 'Number of iterations after calibration > 1: {}' + msg = 'Number of iterations after calibration > 1: {}'.format(n_iter) if check_convergence: - raise RuntimeError(msg.format(n_iter)) + raise RuntimeError(msg) else: - warnings.warn(msg.format(n_iter)) + warnings.warn(msg) return s From 1b7e54a4cccd643d10b4dbf9a0a6ee2ed5d8ec04 Mon Sep 17 00:00:00 2001 From: Matthew Gidden Date: Thu, 1 Aug 2019 09:46:18 +0200 Subject: [PATCH 40/70] check for 0-prices --- message_ix/macro.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/message_ix/macro.py b/message_ix/macro.py index 153e0d69a..0c9870176 100644 --- a/message_ix/macro.py +++ b/message_ix/macro.py @@ -2,6 +2,7 @@ import os import warnings +import numpy as np import pandas as pd from functools import lru_cache @@ -288,9 +289,14 @@ def _price(self): idx = ['node', 'sector', 'year'] model_price = self.s.var('PRICE_COMMODITY', filters={'level': 'useful'}) + if np.isclose(model_price['lvl'], 0).any(): + # TODO: this needs a test.. + msg = '0-price found in MESSAGE variable PRICE_COMMODITY' + raise RuntimeError(msg) model_price.rename(columns={'lvl': 'value', 'commodity': 'sector'}, inplace=True) model_price = model_price[idx + ['value']] + # get data provided in init year from data price_ref = self.data['price_ref'].reset_index() price_ref['year'] = self.init_year From 3fffd84349b50e0da2e9f0fc31979a510591f201 Mon Sep 17 00:00:00 2001 From: Matthew Gidden Date: Thu, 8 Aug 2019 12:28:17 +0200 Subject: [PATCH 41/70] clean up and add 690 to grow and aeei params --- message_ix/macro.py | 16 +++++++++++----- tests/data/westeros_macro_input.xlsx | Bin 17107 -> 16608 bytes 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/message_ix/macro.py b/message_ix/macro.py index 0c9870176..6e69042b2 100644 --- a/message_ix/macro.py +++ b/message_ix/macro.py @@ -74,10 +74,6 @@ 'idx': ['node', ], 'unit': '-', }, - 'price_ref': { - 'idx': ['node', 'sector', ], - 'unit': 'USD/kWa', - }, 'lakl': { 'idx': ['node', ], 'unit': '-', @@ -126,6 +122,7 @@ MACRO_DATA_FOR_DERIVATION = { 'cost_ref': ['node', ], 'demand_ref': ['node', 'sector', ], + 'price_ref': ['node', 'sector', ], } VERIFY_INPUT_DATA = [ @@ -207,6 +204,8 @@ def read_data(self): self.data[name] = self.data[name].set_index(idx)['value'] # special check for gdp_calibrate + # TODO: this now needs to check that there are *TWO* periods in history + # for gdp_calibrate check = self.data['gdp_calibrate'] data_years = check.index.get_level_values('year') min_year_model = min(self.years) @@ -348,7 +347,8 @@ def _aconst(self): partmp = (bconst * demand_ref ** rho).groupby(level='node').sum() # TODO: automatically get the units here!! aconst = ((gdp0 / 1e3) ** rho - partmp) / (k0 / 1e3) ** (rho * kpvs) - self.data['aconst'] = aconst + # want the series to only have index of node + self.data['aconst'] = aconst.reset_index(level='year', drop=True) return self.data['aconst'] @@ -413,6 +413,12 @@ def add_model_data(base, clone, data): key = values.get('data_key', name) data = c.data[key].reset_index() data['unit'] = values['unit'] + # some data may have information prior to the MACRO initialization + # year which we need to remove in order to add it to the scenario + if 'year' in data: + data = data[data['year'] >= c.init_year] + print(name) + print(data) clone.add_par(name, data) except Exception as e: msg = 'Error in adding parameter {}\n'.format(name) diff --git a/tests/data/westeros_macro_input.xlsx b/tests/data/westeros_macro_input.xlsx index 36c687dcd55c04df186bda81d635e79c582bb160..c479126e4776b1d60db901de757b35a92182e0c9 100644 GIT binary patch literal 16608 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 ZeKO+UfZYKI2o3Pd53t_|aX$U)zW~>+4{-nh literal 17107 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 From 4996593ee0984f4027dc104a9a7a7dd4767351d8 Mon Sep 17 00:00:00 2001 From: Matthew Gidden Date: Thu, 8 Aug 2019 19:15:58 +0200 Subject: [PATCH 42/70] change dataload to include inityear in macro --- message_ix/model/MACRO/macro_data_load.gms | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/message_ix/model/MACRO/macro_data_load.gms b/message_ix/model/MACRO/macro_data_load.gms index f3c750cb1..87cba7178 100755 --- a/message_ix/model/MACRO/macro_data_load.gms +++ b/message_ix/model/MACRO/macro_data_load.gms @@ -154,7 +154,7 @@ DISPLAY node_macro ; $INCLUDE includes/period_parameter_assignment.gms year(year_all) = no ; -year(year_all)$( ORD(year_all) >= sum(year_all2$cat_year("baseyear_macro",year_all2), ORD(year_all2) ) ) = yes ; +year(year_all)$( ORD(year_all) >= sum(year_all2$cat_year("initializeyear_macro",year_all2), ORD(year_all2) ) ) = yes ; DISPLAY cat_year, macro_base_period, first_period, last_period, macro_horizon, year ; From d5930a5e5f3084bdd1645a661a136603c645efe1 Mon Sep 17 00:00:00 2001 From: Matthew Gidden Date: Thu, 8 Aug 2019 19:21:34 +0200 Subject: [PATCH 43/70] rm prints --- message_ix/macro.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/message_ix/macro.py b/message_ix/macro.py index 6e69042b2..f8fcbeeac 100644 --- a/message_ix/macro.py +++ b/message_ix/macro.py @@ -417,8 +417,6 @@ def add_model_data(base, clone, data): # year which we need to remove in order to add it to the scenario if 'year' in data: data = data[data['year'] >= c.init_year] - print(name) - print(data) clone.add_par(name, data) except Exception as e: msg = 'Error in adding parameter {}\n'.format(name) From eeb76266bfce0de9527fb729b7d15ba624a06f81 Mon Sep 17 00:00:00 2001 From: Matthew Gidden Date: Fri, 9 Aug 2019 08:17:01 +0200 Subject: [PATCH 44/70] fix data checking for gdp_calibrate --- message_ix/macro.py | 16 ++++++++-------- message_ix/tests/test_macro.py | 6 ++---- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/message_ix/macro.py b/message_ix/macro.py index f8fcbeeac..793560659 100644 --- a/message_ix/macro.py +++ b/message_ix/macro.py @@ -203,19 +203,19 @@ def read_data(self): self.nodes, self.sectors, self.years) self.data[name] = self.data[name].set_index(idx)['value'] - # special check for gdp_calibrate - # TODO: this now needs to check that there are *TWO* periods in history - # for gdp_calibrate + # special check for gdp_calibrate - it must have at minimum two years + # prior to the model horizon in order to compute growth rates in the + # historical period (MACRO's "initializeyear") check = self.data['gdp_calibrate'] data_years = check.index.get_level_values('year') min_year_model = min(self.years) - min_year_data = min(data_years) - if not min_year_data < min_year_model: - msg = 'Must provide gdp_calibrate data prior to the modeling' + \ - ' period in order to calculate growth rates' + data_years_before_model = data_years[data_years < min_year_model] + if len(data_years_before_model) < 2: + msg = 'Must provide two gdp_calibrate data points prior to the ' + \ + 'modeling period in order to calculate growth rates' raise ValueError(msg) # init year is most recent period PRIOR to the modeled period - self.init_year = max(data_years[data_years < min_year_model]) + self.init_year = max(data_years_before_model) # base year is first model period self.base_year = min_year_model diff --git a/message_ix/tests/test_macro.py b/message_ix/tests/test_macro.py index 0fd4c5549..0e4d7f153 100644 --- a/message_ix/tests/test_macro.py +++ b/message_ix/tests/test_macro.py @@ -224,8 +224,6 @@ def test_calibrate(westeros_solved): start_aeei = clone.par('aeei')['value'] start_grow = clone.par('grow')['value'] - # TODO: this fails now because it takes 4 iterations to converge, don't - # know why, but the original macro-only iterations hit a max value (100) macro.calibrate(clone, check_convergence=True) end_aeei = clone.par('aeei')['value'] @@ -240,5 +238,5 @@ def test_calibrate(westeros_solved): def test_calibrate_roundtrip(westeros_solved): - # TODO: make this true when we address above issue - westeros_solved.add_macro(DATA_PATH, check_convergence=False) + westeros_solved.add_macro(DATA_PATH, check_convergence=True) + print(westeros_solved.par('aeei')) From 6618121814f3aee402681d8b955518f9e66821b1 Mon Sep 17 00:00:00 2001 From: Matthew Gidden Date: Fri, 9 Aug 2019 08:20:16 +0200 Subject: [PATCH 45/70] further clean up and fixing growth test --- message_ix/macro.py | 3 +-- message_ix/tests/test_macro.py | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/message_ix/macro.py b/message_ix/macro.py index 793560659..1e127ed99 100644 --- a/message_ix/macro.py +++ b/message_ix/macro.py @@ -206,8 +206,7 @@ def read_data(self): # special check for gdp_calibrate - it must have at minimum two years # prior to the model horizon in order to compute growth rates in the # historical period (MACRO's "initializeyear") - check = self.data['gdp_calibrate'] - data_years = check.index.get_level_values('year') + data_years = self.data['gdp_calibrate'].index.get_level_values('year') min_year_model = min(self.years) data_years_before_model = data_years[data_years < min_year_model] if len(data_years_before_model) < 2: diff --git a/message_ix/tests/test_macro.py b/message_ix/tests/test_macro.py index 0e4d7f153..97f668b43 100644 --- a/message_ix/tests/test_macro.py +++ b/message_ix/tests/test_macro.py @@ -85,9 +85,9 @@ def test_calc_growth(westeros_solved): c = macro.Calculate(s, DATA_PATH) c.read_data() obs = c._growth() - assert len(obs) == 3 + assert len(obs) == 4 obs = obs.values - exp = np.array([0.041380, 0.041380, 0.029186]) + exp = np.array([0.0265836, 0.041380, 0.041380, 0.029186]) assert np.isclose(obs, exp).all() From a1a61be0933c64dab887c63d9d493fe63fb0d3ba Mon Sep 17 00:00:00 2001 From: Matthew Gidden Date: Fri, 9 Aug 2019 08:29:52 +0200 Subject: [PATCH 46/70] updated roundtrip test --- message_ix/tests/test_macro.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/message_ix/tests/test_macro.py b/message_ix/tests/test_macro.py index 97f668b43..059b7fb2f 100644 --- a/message_ix/tests/test_macro.py +++ b/message_ix/tests/test_macro.py @@ -238,5 +238,13 @@ def test_calibrate(westeros_solved): def test_calibrate_roundtrip(westeros_solved): - westeros_solved.add_macro(DATA_PATH, check_convergence=True) - print(westeros_solved.par('aeei')) + # this is a regression test with values observed on Aug 9, 2019 + with_macro = westeros_solved.add_macro(DATA_PATH, check_convergence=True) + aeei = with_macro.par('aeei')['value'].values + assert len(aeei) == 4 + exp = [0.02, 0.07171359, 0.03743102, 0.01990546] + assert np.allclose(aeei, exp) + grow = with_macro.par('grow')['value'].values + assert len(grow) == 4 + exp = [0.02658363, 0.06911822, 0.07950836, 0.02452974] + assert np.allclose(grow, exp) From b7d12e29e413789586853b04b60967ff82b2312c Mon Sep 17 00:00:00 2001 From: Matthew Gidden Date: Fri, 9 Aug 2019 08:33:54 +0200 Subject: [PATCH 47/70] stickler --- message_ix/macro.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/message_ix/macro.py b/message_ix/macro.py index 1e127ed99..9e5442890 100644 --- a/message_ix/macro.py +++ b/message_ix/macro.py @@ -210,8 +210,8 @@ def read_data(self): min_year_model = min(self.years) data_years_before_model = data_years[data_years < min_year_model] if len(data_years_before_model) < 2: - msg = 'Must provide two gdp_calibrate data points prior to the ' + \ - 'modeling period in order to calculate growth rates' + msg = 'Must provide two gdp_calibrate data points prior to the' + \ + ' modeling period in order to calculate growth rates' raise ValueError(msg) # init year is most recent period PRIOR to the modeled period self.init_year = max(data_years_before_model) From 026ca3a55fbdd186c64fd9e75a0c584d51be10e2 Mon Sep 17 00:00:00 2001 From: Matthew Gidden Date: Fri, 9 Aug 2019 09:14:06 +0200 Subject: [PATCH 48/70] remove extraneous try --- message_ix/macro.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/message_ix/macro.py b/message_ix/macro.py index 9e5442890..803a57834 100644 --- a/message_ix/macro.py +++ b/message_ix/macro.py @@ -373,12 +373,6 @@ def init(s): for key, values in MACRO_INIT['equs'].items(): s.init_equ(key, values) - try: - # TODO: should this already exist? - s.init_set("mapping_macro_sector", ("sector", "commodity", "level")) - except: # noqa: ignore=E722 - pass # already exists - # keep track of number of iterations s.init_var('N_ITER', None) s.init_var('MAX_ITER', None) From 3cf70cef30435ba1da460859d49678ea9972e366 Mon Sep 17 00:00:00 2001 From: Matthew Gidden Date: Fri, 9 Aug 2019 09:29:12 +0200 Subject: [PATCH 49/70] use sets instead in calc --- message_ix/macro.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/message_ix/macro.py b/message_ix/macro.py index 803a57834..af476221a 100644 --- a/message_ix/macro.py +++ b/message_ix/macro.py @@ -188,11 +188,18 @@ def __init__(self, s, data): raise RuntimeError('Scenario must have a solution to add MACRO') demand = s.var('DEMAND', filters={'level': 'useful'}) - self.nodes = demand['node'].unique() - self.sectors = demand['commodity'].unique() - self.years = demand['year'].unique() + self.nodes = set(demand['node'].unique()) + self.sectors = set(demand['commodity'].unique()) + self.years = set(demand['year'].unique()) def read_data(self): + # if 'config' in self.data: + # config = self.data['config'] + # rm_nodes = config.get('ignore_nodes', []) + # self.nodes = np.array(set(self.nodes) - set(rm_nodes)) + # rm_sectors = config.get('ignore_sectors', []) + # self.sectors = np.array(set(self.sectors) - set(rm_sectors)) + par_diff = set(VERIFY_INPUT_DATA) - set(self.data) if par_diff: raise ValueError( From 6117aacae44b09fd92b4fd729c0dc6a8548f87ec Mon Sep 17 00:00:00 2001 From: Matthew Gidden Date: Fri, 9 Aug 2019 09:38:28 +0200 Subject: [PATCH 50/70] add feature to remove nodes and sectors --- message_ix/macro.py | 15 +++++++++------ message_ix/tests/test_macro.py | 13 +++++++++++++ tests/data/westeros_macro_input.xlsx | Bin 16608 -> 18275 bytes 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/message_ix/macro.py b/message_ix/macro.py index af476221a..6798a73e0 100644 --- a/message_ix/macro.py +++ b/message_ix/macro.py @@ -193,12 +193,12 @@ def __init__(self, s, data): self.years = set(demand['year'].unique()) def read_data(self): - # if 'config' in self.data: - # config = self.data['config'] - # rm_nodes = config.get('ignore_nodes', []) - # self.nodes = np.array(set(self.nodes) - set(rm_nodes)) - # rm_sectors = config.get('ignore_sectors', []) - # self.sectors = np.array(set(self.sectors) - set(rm_sectors)) + if 'config' in self.data: + # users may remove certain nodes and sectors from the MACRO set + # definitions + config = self.data['config'] + self.nodes -= set(config.get('ignore_nodes', [])) + self.sectors -= set(config.get('ignore_sectors', [])) par_diff = set(VERIFY_INPUT_DATA) - set(self.data) if par_diff: @@ -206,6 +206,9 @@ def read_data(self): 'Missing required input data: {}'.format(par_diff)) for name in self.data: + # no need to validate configuration, it was processed above + if name == 'config': + continue idx = _validate_data(name, self.data[name], self.nodes, self.sectors, self.years) self.data[name] = self.data[name].set_index(idx)['value'] diff --git a/message_ix/tests/test_macro.py b/message_ix/tests/test_macro.py index 059b7fb2f..aaa854028 100644 --- a/message_ix/tests/test_macro.py +++ b/message_ix/tests/test_macro.py @@ -44,6 +44,19 @@ def test_calc_valid_data_dict(westeros_solved): c.read_data() +def test_config(westeros_solved): + s = westeros_solved + c = macro.Calculate(s, DATA_PATH) + c.nodes = set(list(c.nodes) + ['foo']) + c.sectors = set(list(c.sectors) + ['bar']) + + assert c.nodes == set(['Westeros', 'foo']) + assert c.sectors == set(['light', 'bar']) + c.read_data() + assert c.nodes == set(['Westeros']) + assert c.sectors == set(['light']) + + def test_calc_no_solution(westeros_not_solved): s = westeros_not_solved pytest.raises(RuntimeError, macro.Calculate, s, DATA_PATH) diff --git a/tests/data/westeros_macro_input.xlsx b/tests/data/westeros_macro_input.xlsx index c479126e4776b1d60db901de757b35a92182e0c9..72f7847841c307ee749d188250ea6a93b1cdf179 100644 GIT binary patch literal 18275 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
E541K?30lu#H9@4PM#x(hs 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 ZeKO+UfZYKI2o3Pd53t_|aX$U)zW~>+4{-nh From 76196cc2ec80f2c95f8de4191ae6b73246c3a5c3 Mon Sep 17 00:00:00 2001 From: Matthew Gidden Date: Mon, 12 Aug 2019 09:21:27 +0200 Subject: [PATCH 51/70] Add support for multi region/multi sector data reading --- message_ix/macro.py | 4 +- message_ix/tests/test_macro.py | 105 ++++++++++++++++++------ tests/data/multiregion_macro_input.xlsx | Bin 0 -> 11747 bytes 3 files changed, 83 insertions(+), 26 deletions(-) create mode 100644 tests/data/multiregion_macro_input.xlsx diff --git a/message_ix/macro.py b/message_ix/macro.py index 6798a73e0..75a6f7d05 100644 --- a/message_ix/macro.py +++ b/message_ix/macro.py @@ -245,8 +245,8 @@ def derive_data(self): def _growth(self): gdp = self.data['gdp_calibrate'] diff = gdp.groupby(level='node').diff() - years = gdp.index.get_level_values('year') - dt = pd.Series(years, name='year', index=years).diff() + years = sorted(gdp.index.get_level_values('year').unique()) + dt = pd.Series(years, index=pd.Index(years, name='year')).diff() growth = (diff / gdp + 1) ** (1 / dt) - 1 growth.name = 'value' self.data['growth'] = growth.dropna() diff --git a/message_ix/tests/test_macro.py b/message_ix/tests/test_macro.py index aaa854028..17d4d3dfc 100644 --- a/message_ix/tests/test_macro.py +++ b/message_ix/tests/test_macro.py @@ -17,7 +17,40 @@ MSG_ARGS = ('canning problem (MESSAGE scheme)', 'standard') -DATA_PATH = Path(__file__).parent / 'data' / 'westeros_macro_input.xlsx' +W_DATA_PATH = Path(__file__).parent / 'data' / 'westeros_macro_input.xlsx' +MR_DATA_PATH = Path(__file__).parent / 'data' / 'multiregion_macro_input.xlsx' + + +class MockScenario: + + def __init__(self): + self.data = pd.read_excel(MR_DATA_PATH, sheet_name=None) + for name, df in self.data.items(): + if 'year' in df: + df = df[df.year >= 2030] + self.data[name] = df + + def has_solution(self): + return True + + def var(self, name, **kwargs): + if name == 'DEMAND': + df = ( + self.data['aeei'] + .rename(columns={'sector': 'commodity'}) + ) + # must provide two data points before history + return df + if name in ['COST_NODAL_NET', 'PRICE_COMMODITY']: + df = ( + self.data['aeei'] + .rename(columns={ + 'sector': 'commodity', + 'value': 'lvl' + }) + ) + df['lvl'] = 1e3 + return df # TODO: what scope should these be? @@ -33,20 +66,25 @@ def westeros_not_solved(test_mp): def test_calc_valid_data_file(westeros_solved): s = westeros_solved - c = macro.Calculate(s, DATA_PATH) + c = macro.Calculate(s, W_DATA_PATH) c.read_data() def test_calc_valid_data_dict(westeros_solved): s = westeros_solved - data = pd.read_excel(DATA_PATH, sheet_name=None) + data = pd.read_excel(W_DATA_PATH, sheet_name=None) c = macro.Calculate(s, data) c.read_data() +def test_calc_no_solution(westeros_not_solved): + s = westeros_not_solved + pytest.raises(RuntimeError, macro.Calculate, s, W_DATA_PATH) + + def test_config(westeros_solved): s = westeros_solved - c = macro.Calculate(s, DATA_PATH) + c = macro.Calculate(s, W_DATA_PATH) c.nodes = set(list(c.nodes) + ['foo']) c.sectors = set(list(c.sectors) + ['bar']) @@ -57,14 +95,9 @@ def test_config(westeros_solved): assert c.sectors == set(['light']) -def test_calc_no_solution(westeros_not_solved): - s = westeros_not_solved - pytest.raises(RuntimeError, macro.Calculate, s, DATA_PATH) - - def test_calc_data_missing_par(westeros_solved): s = westeros_solved - data = pd.read_excel(DATA_PATH, sheet_name=None) + data = pd.read_excel(W_DATA_PATH, sheet_name=None) data.pop('gdp_calibrate') c = macro.Calculate(s, data) pytest.raises(ValueError, c.read_data) @@ -72,7 +105,7 @@ def test_calc_data_missing_par(westeros_solved): def test_calc_data_missing_column(westeros_solved): s = westeros_solved - data = pd.read_excel(DATA_PATH, sheet_name=None) + data = pd.read_excel(W_DATA_PATH, sheet_name=None) # skip first data point data['gdp_calibrate'] = data['gdp_calibrate'].drop('year', axis=1) c = macro.Calculate(s, data) @@ -81,12 +114,13 @@ def test_calc_data_missing_column(westeros_solved): def test_calc_data_missing_datapoint(westeros_solved): s = westeros_solved - data = pd.read_excel(DATA_PATH, sheet_name=None) + data = pd.read_excel(W_DATA_PATH, sheet_name=None) # skip first data point data['gdp_calibrate'] = data['gdp_calibrate'][1:] c = macro.Calculate(s, data) pytest.raises(ValueError, c.read_data) + # # Regression tests: these tests were compiled upon moving from R to Python, # values were confirmed correct at the time and thus are tested explicitly here @@ -95,7 +129,7 @@ def test_calc_data_missing_datapoint(westeros_solved): def test_calc_growth(westeros_solved): s = westeros_solved - c = macro.Calculate(s, DATA_PATH) + c = macro.Calculate(s, W_DATA_PATH) c.read_data() obs = c._growth() assert len(obs) == 4 @@ -106,7 +140,7 @@ def test_calc_growth(westeros_solved): def test_calc_rho(westeros_solved): s = westeros_solved - c = macro.Calculate(s, DATA_PATH) + c = macro.Calculate(s, W_DATA_PATH) c.read_data() obs = c._rho() assert len(obs) == 1 @@ -117,7 +151,7 @@ def test_calc_rho(westeros_solved): def test_calc_gdp0(westeros_solved): s = westeros_solved - c = macro.Calculate(s, DATA_PATH) + c = macro.Calculate(s, W_DATA_PATH) c.read_data() obs = c._gdp0() assert len(obs) == 1 @@ -128,7 +162,7 @@ def test_calc_gdp0(westeros_solved): def test_calc_k0(westeros_solved): s = westeros_solved - c = macro.Calculate(s, DATA_PATH) + c = macro.Calculate(s, W_DATA_PATH) c.read_data() obs = c._k0() assert len(obs) == 1 @@ -139,7 +173,7 @@ def test_calc_k0(westeros_solved): def test_calc_total_cost(westeros_solved): s = westeros_solved - c = macro.Calculate(s, DATA_PATH) + c = macro.Calculate(s, W_DATA_PATH) c.read_data() obs = c._total_cost() # 4 values, 3 in model period, one in history @@ -151,7 +185,7 @@ def test_calc_total_cost(westeros_solved): def test_calc_price(westeros_solved): s = westeros_solved - c = macro.Calculate(s, DATA_PATH) + c = macro.Calculate(s, W_DATA_PATH) c.read_data() obs = c._price() # 4 values, 3 in model period, one in history @@ -163,7 +197,7 @@ def test_calc_price(westeros_solved): def test_calc_demand(westeros_solved): s = westeros_solved - c = macro.Calculate(s, DATA_PATH) + c = macro.Calculate(s, W_DATA_PATH) c.read_data() obs = c._demand() # 4 values, 3 in model period, one in history @@ -175,7 +209,7 @@ def test_calc_demand(westeros_solved): def test_calc_bconst(westeros_solved): s = westeros_solved - c = macro.Calculate(s, DATA_PATH) + c = macro.Calculate(s, W_DATA_PATH) c.read_data() obs = c._bconst() assert len(obs) == 1 @@ -186,7 +220,7 @@ def test_calc_bconst(westeros_solved): def test_calc_aconst(westeros_solved): s = westeros_solved - c = macro.Calculate(s, DATA_PATH) + c = macro.Calculate(s, W_DATA_PATH) c.read_data() obs = c._aconst() @@ -217,7 +251,7 @@ def test_add_model_data(westeros_solved): clone = base.clone('foo', 'bar', keep_solution=False) clone.check_out() macro.init(clone) - macro.add_model_data(base, clone, DATA_PATH) + macro.add_model_data(base, clone, W_DATA_PATH) clone.commit('finished adding macro') clone.solve() obs = clone.var('OBJ')['lvl'] @@ -231,7 +265,7 @@ def test_calibrate(westeros_solved): keep_solution=False) clone.check_out() macro.init(clone) - macro.add_model_data(base, clone, DATA_PATH) + macro.add_model_data(base, clone, W_DATA_PATH) clone.commit('finished adding macro') start_aeei = clone.par('aeei')['value'] @@ -252,7 +286,8 @@ def test_calibrate(westeros_solved): def test_calibrate_roundtrip(westeros_solved): # this is a regression test with values observed on Aug 9, 2019 - with_macro = westeros_solved.add_macro(DATA_PATH, check_convergence=True) + with_macro = westeros_solved.add_macro( + W_DATA_PATH, check_convergence=True) aeei = with_macro.par('aeei')['value'].values assert len(aeei) == 4 exp = [0.02, 0.07171359, 0.03743102, 0.01990546] @@ -261,3 +296,25 @@ def test_calibrate_roundtrip(westeros_solved): assert len(grow) == 4 exp = [0.02658363, 0.06911822, 0.07950836, 0.02452974] assert np.allclose(grow, exp) + +# +# These are a series of tests to guarantee multiregion/multisector +# behavior is as expected. +# + + +def test_multiregion_valid_data(): + s = MockScenario() + c = macro.Calculate(s, MR_DATA_PATH) + c.read_data() + + +def test_multiregion_derive_data(): + s = MockScenario() + c = macro.Calculate(s, MR_DATA_PATH) + c.read_data() + c.derive_data() + obs = c.data['aconst'] + exp = pd.Series([3.74767687, 0.00285472], name='value', + index=pd.Index(['R11_AFR', 'R11_CPA'], name='node')) + pd.testing.assert_series_equal(obs, exp) diff --git a/tests/data/multiregion_macro_input.xlsx b/tests/data/multiregion_macro_input.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..390b8e791cb0248af6a4696e9232d30ce7a9ea35 GIT binary patch literal 11747 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 Date: Mon, 12 Aug 2019 09:28:40 +0200 Subject: [PATCH 52/70] added bconst test --- message_ix/tests/test_macro.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/message_ix/tests/test_macro.py b/message_ix/tests/test_macro.py index 17d4d3dfc..6ac5be4cb 100644 --- a/message_ix/tests/test_macro.py +++ b/message_ix/tests/test_macro.py @@ -314,7 +314,16 @@ def test_multiregion_derive_data(): c = macro.Calculate(s, MR_DATA_PATH) c.read_data() c.derive_data() + obs = c.data['aconst'] exp = pd.Series([3.74767687, 0.00285472], name='value', index=pd.Index(['R11_AFR', 'R11_CPA'], name='node')) pd.testing.assert_series_equal(obs, exp) + + obs = c.data['bconst'] + idx = pd.MultiIndex.from_product([['R11_AFR', 'R11_CPA'], + ['i_therm', 'rc_spec']], + names=['node', 'sector']) + exp = pd.Series([1.071971e-08, 1.487598e-11, 9.637483e-09, 6.955715e-13], + name='value', index=idx) + pd.testing.assert_series_equal(obs, exp) From 1bb9c5abbae8ec7554fc3ae8ee09134442e30c0a Mon Sep 17 00:00:00 2001 From: Matthew Gidden Date: Mon, 12 Aug 2019 14:35:54 +0200 Subject: [PATCH 53/70] add explicit test for ignoring nodes and sectors in multi-region tests --- message_ix/tests/test_macro.py | 31 ++++++++++++------------ tests/data/multiregion_macro_input.xlsx | Bin 11747 -> 18823 bytes 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/message_ix/tests/test_macro.py b/message_ix/tests/test_macro.py index 6ac5be4cb..79aac6fe7 100644 --- a/message_ix/tests/test_macro.py +++ b/message_ix/tests/test_macro.py @@ -34,23 +34,24 @@ def has_solution(self): return True def var(self, name, **kwargs): + df = self.data['aeei'] + # add extra commodity to be removed + extra_commod = df[df.sector == 'i_therm'] + extra_commod['sector'] = self.data['config']['ignore_sectors'][0] + # add extra region to be removed + extra_region = df[df.node == 'R11_AFR'] + extra_region['node'] = self.data['config']['ignore_nodes'][0] + df = pd.concat([df, extra_commod, extra_region]) + if name == 'DEMAND': - df = ( - self.data['aeei'] - .rename(columns={'sector': 'commodity'}) - ) - # must provide two data points before history - return df - if name in ['COST_NODAL_NET', 'PRICE_COMMODITY']: - df = ( - self.data['aeei'] - .rename(columns={ - 'sector': 'commodity', - 'value': 'lvl' - }) - ) + df = df.rename(columns={'sector': 'commodity'}) + elif name in ['COST_NODAL_NET', 'PRICE_COMMODITY']: + df = df.rename(columns={ + 'sector': 'commodity', + 'value': 'lvl' + }) df['lvl'] = 1e3 - return df + return df # TODO: what scope should these be? diff --git a/tests/data/multiregion_macro_input.xlsx b/tests/data/multiregion_macro_input.xlsx index 390b8e791cb0248af6a4696e9232d30ce7a9ea35..2e04a2b69edf9c6555ff93d25c8cb9cc365b3a8a 100644 GIT binary patch literal 18823 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 Mm#~#1^!o1q0UsG5!~g&Q literal 11747 zcmeHtbyyT$w>2R#bPe6zB@NQuAT8b94I(YwAl+Tk-3Uk{DBU2P(jk5$zVGWN*WZ2a zfA^mY4>Qm3uop9DoxRW6>ug0C$Y&T}U|_JI9}=+gs~72c;9y|9&%nU2LBDagW^%K0 zv~n^xF>!WcdVFGGbhojNjPJJ1V@3%+ehoK#)hFmHimK?sS?Y?MsAl7KjD(;M*cK0Q2) zAmTO@F$5zEgObYwL%kv2OaqC6cb zgO7=_gL(ulwu?R>Hb{6o4(eSFxm6tGUb!+~yoyDmeOOHO;(K_e#MvL#Hs!i_!l&v^ zZizDU!mSmqA@)MEX&BFqchkDa>CMOQ2?X>t?iJTQnyB98*AAMfKIOAIYUT34kz3vr z1T(qd`2L-dWwv+eIPek)otJU$nT@GmD=^)s7H=aOac9s)LmTC{{65h$r-t;Kb(tG< znJ->ao^oWJtZVAFb%kR=M_@r4$5YLm;T|=W27S&C8rr*APDNwym{_x}B9g~!lrXCD z3dg;Z5o-Qe*&@tl=XVEN^d{>EjN%`}#t11Bn}diwg7`;bSs#g2mQ(0uMrc2y%U!g% zt%gXOmp7Vv216d`>owhCQ^!#mXC3fHVX~&gn%BN|WBV$Tfa>gh1BsWSR<^9g>P>?0 zNHt20^zOjj?WG@@O%uI)0GjeGV(M6J+NQ5%nH87_z29e=ipX8lh~2i}Br9azm8{RHSP zza12I|02v~nVuf{HemHLy#Ic}C)FWyQJgF#@c>1>3(7SdDmW z_u0OjEQp>>GjQG)JRo}D*UR8-!xL`hhmh)K+C#m>;HYn284=pMLFSf5>dY%#A^xQ2 z=s)ySMS1l~u-T%rwu-%ZxBg8L^Y}84R3gitj!l5 z9=*Cw*4qbZyIZjih>Bd+8lY?1C`D6_hn8L#y;4gkUbDC~3($E}Q-#F_y>MLIu!5Qp zeh?=kdPdMhRP|$Zj_S`$%e`=9_?c-ikpGxzZ2!tMy8uw89jl8rM=NSH!hx#_sMx%f znyNd@^!F=9$s=c2=+uLF)hCVII(K^JH6FF`)$C8Xk=?mlIC2#V z<wrZbukVaZUPus<|_pHzcWLFD96rI@OK$h?x1 z8--ZuG(*ISLqdqXMTWr)`lug1R}F3fjRwWeIo(kyT4sT}-7~$&Cn^*>@r8aqFrmQV z{XI$cR9s@CNX;iTLc_Zn&H2ZTZiiPbV`S#ynxCv0l7sJx>i zwv}IPIcurwI#{oo9N)hlb>(R+`W!tsC~Ybba)E(*FJ@gBauT7h_T8GK$&v({eOV_2 zCV*l*OV2zi4Rhv8PnqRf@u|Nvf&3R}aSkoBVHx+WQVivITsWC6FXV(`gS*$(Rfl=p z6kcr5X)EP5STI8-)8c;SKIm72+j#Pw;D54{p`bKr3StN9pV?vm#ZI^FBRilvC_!ER zD#*9gO7#glD@=aC>V~&V2iG_4W&@|+%1Q=N6ELaKdCt701x=ITH7UgAO+* z-Mqj+pMb*QbIK#dLl-Fh5Q-R{oJ1Cz56NS+=1rGXP!@DhrNMY7{sSH_vpK@@6a;?F}55Q`G8x*q|W24%g5l?Lh^VLGz#NB(p7Ngo4;P)^?n)ZDPdK z1;SHBo;|NV5w<2ajyGv&3&g6FCSfnhsnYJa5)8$4T^WF0;7@&7BVts=ZIal|smNH% z2q)?!y@(Z!mHq(sCpZ6t%;EUI%=zESoc{qkod3tp{}wywvhHsBpt`pE&-#w*k)60l zeHVOuA4ct#>ccFkx?o|Qy6PKOsd&-6*LKYeGq@@GZMD}|yn0aH%ee(^8WJuv*fkW%8Ot}7NeqXthgU^Gsdh`u^J^arW#5syeT zG?40MoJwmFshgmYi!WM_xxJI-X+ zmh?0X`TT~fidqyJZv+WZ(uq__(FvrwrR!1)9T*=~HwHlG<`YjoeWce{1@H{Qz7L2s zB8#n?+zQ3p*#~f~hjMMSaU$Kw^&Y)P08vex*1DUnjYk`s3-8wEq}HWtCG*!N zM-E?g?@|w)ZKF%DYV#6Dd{CojLROg!Uq`~iJgeOc2-2SvX0~i=ML}lVk@w$->Z?=X zq5($XZeuC0mVZZdJA5Q7OXUD z4x8hWb%|1a-?4*-JE-dTl7K=t{FPo0T`u>Ug-(%Ud;k_f9`}hWqdhEx=9f>DFihjo zoB;d44|P2+^2lL+gn%{nMIU^)ZZ!wQM$rn2A!*SJya>#-9`x;tiVUPJF!k|;&6%k54}J5WU_6WtbYKL^@SAvmx?lO_DSe!ZNZB8Dz7n~5lIl!JyIAaK zfA^(eWF3=_;)yJ>wf%5w+xEKHJMnfS7A__c+KiI1e)s zy)NSRW4=QxbNP1cyL#3LSeM?A&h0_RFl596)$tH?G_%A|&j8ELKBv5SmWYYMTB7qp+)*rAYx^xpT!UaagnjdOhg2U)}a zT_P6|qAVEZxGFxUc^E0ztUb5i$U+W~RV^kqj!IGVuSr zCw`e9NpenUKfAE+K!hu7hV?0Q#8H(M~rd5Zl6xQCTVU_~MN>6Va4w&@|g+e6e7@02UrcOhhyuctH8Jyl|Yq-Tl% z!RET3<6g;-*`iCpPEvnzZi9;^6(uYFqyM!jg5`&7Sm=6#i1>8?^miC>{SUAx5v%@d z3M>$!k=X;;eo*9W{(-QP=8yo?7d?c5qWaLG5yYqvXgY#B805kjp~1-U`KB?muxMn& z)rjQc7@;s?UEeIw$Rz!J$+2Se;o^wH$X~#c`{9tY`{%<-T0%;pij7IRWssB0+7Ug+ zMychD`^32R88h^dZzW$&wIzQ}flb33ml?CF28{GG1)>2<8pAcVj9p``6Jqj;uw4_F zgJBMf7o%cCu$Be$LQuC`-UNh^t1c{a^0Jp13_kN&&?=l!9?Jq#Ahw)&K{q}&G<>0c zwLY^KvnaOQaZha?QZi!iYHz~4dA~TmwuNB?Wy7StE`s{=!l|rTyrqx04I3-Ti-|7# z`0hFTu|bt|<5*2o8lhWklUeJk)D)#Y4Ks~ujIAjTe()qJ0=q|{y)U@#;QuUfL5yE% zzko_y=YLb;epx;Va(3yzw)4j;{SE;&!30xa{ab0(#LFXJa5{amg*lrauWjtr@TRjD z`P$xZ<3x{QT)i3S<~Cr2&7(NcLFP)HE+?s@>ptEtYDM4@CY{grQ_}W-qv3nEC*xpx zuX>IxBSDd!`>Z`-3S3y}%3ZX_1iet&0*5G8h2%IpnM-`M%>rM4)dPT!nxj8xznV4J zJ)iXX`r}?B>dR*GHFrVfL?%yGfFcl4DV0CE*Wr~v%n$n!@ba*f`?ssLOb`SdytZn+7QZ*2GoNX8oHcd^| z6q}KtAvFl3(T$C(C#&==lxR5?iEoJHI;^dp@vG@=f#mlnXja7kZ4v~ zN)}`8FYW@=jr4^H9{k)TitV8nX~VWlF~6Buj#v#AdK6?1!^g)(9xJ`4q}bw7{vk1( z?2UYG-!ihi_vE;HpXr`D0NM-mAb$z@_i<-vXJ_?G6b{=iFr$cFeTVbA6OWb*v(Z4B z;1Gj6H^8u{K`?hPMlF=|&nMkJ=B>$bCberpGjl%bYnyHykQZZGZDVUO+*R!%ov24u zKBl|eUfUml3in>svNvHJJhk;(U$nv%Dk~<`4t~4%1KK+Z1#Od16mRLB# zaYV7j`K-3yn&>dGuseGyV=$#^e39o}t%3W8MY^Rd)N%fOX^oVPjA#=lxMsLJ8Y}TlU-`#{^VH0K8 zezS0jK8BwY#I_T*cPE*((Cm6H#sDNr6S=*IW}{1BJITRl`iogCg;^*AEUW`vjUGlo zYkF-6|BUwFQuzs64xaQDD-toyciSv1`0ox^yvc{IGC=3NU5kQKXFd&uNX{2NW9GW~ zv!YXU7dTFx=We|UoNQr*Ud#7Tf98K}F%kGWi0c#(?>`NGkd^ObZs2HQ{LwvRiE69+g%irG*vVyY|n{V+i`k*x?tO+ z(Uo`4X(VElw`$jD$ej6zrSV5wN_Axe1+{M2;uC83Bh{VGugdIe}eTA)}L$A_YeWu23ijxP#&Ow*2LJ(NYT;G-igV;-u~x$ zWTVfgwO*>jmYnASpe%iWlkvy(cT}X z;w80~j`7~r$4o#AV8wsZFkY3Tu2>oA8{WHHQjaCE(6hwi8|edAT-?r5i4_hM#bh>u zigr>$?@LKAw=`#qO1>OXAz@GDlEuR*9l~{oHJ$OL;wFghl$=GsMjL9K1%%bEmlym1 zG6Znn_Rf4EsnMB=V^*`8bWcW^Tr)l~I04K4!AL8mJ>GeRCYs zpaF?OJ4c~VrMEC}>BOIsNsFzcJ~Rf{u!^RWK{_#0-4TUMqEgRPU=$rUk-guu1C(|Q zBBl5`$I~|_cC<;g3U(v1`nFzF8BRXq91F9EneE4X8!Cqb0~qUc=sdjLGQjOZLK~0n zq{a|C&B=K;c0Uy^W}bV==TIXeP`98Dgn zjvEDSeUw1Z4OU(&!ypX_umRF$u*6UJrIsi9BqeISPt0c?kGmoHb@G^((Pb&$U0s#f zS9Uq>pokLK*>j33rqAY)_ZbxukOl%A= zhCr_rU%)2lD=ODDl5ed%LWZ5!>mhEM6mBCMH%Q;|$7sTJAvq`v4Q%du)C>$%eAj2O`Cp=hId7atcD% zSFU&{>>nkFt2wQvJGNhEP0{Pz^@^!NJ26uy$V#F0k%OhsFm94l9B*t_NkG)eI;znE zXfz?>)>d-`bS=BOfHJaD1{EDe7_|_z!LgIXOVX9HedS*4=%tz*Cg|B2Tt}0#Qt*?( z4VT0L4`iFxYV)fWYlW{+ASvQjbAZ#DGzMDlNR6f|rkXc9m3vf$H)-(@F@-k@&{100 zb98Tb^;NI-T>D{>xAtv%iEIxq(S0&$C(`DdBKYCOp6@Fu;{W<9MSs+>H zYw**rqH+)Wl70R-t9H{(c*}Ed_Ey?w@avjskvg7usTP%i*moeVUVzje;=iiDvxl|G zW7%(4SqTRzKA&Sf)Nel9wCCpGp677zK@AwT@UJ6r*<*;}BEpyTR;1QT;oayLMf65S zHq-1(J3eei$H#4sRG^Dvv9${*_PE&K=Ct}ha1$K!E3NzP221662$>!27~PP#G0QwG zIw3v7siQMTX7-jsQ@_$?akGQ2bSPq1V-cKj4aK83i#(H(rkqp{Ox6kLu z3-&ja$sSpP=YAe)Q@}=ZHZN%eAbxR)N}5VTh`N%DkqG3UWu@4}?@d7tjKWMuo7W(c zH4M70i2I>LM~@UAm@w08m^}gm-BrP#D}s5)hOD8RcM?mvYMaeFyv26cBm)aST@hh( zl6{qER6Igq2aaC8AbvOtYZ}XVnjQ3= zk|SFfo--$;+1Udf&*1%d4K}sXXo2^o?3)GEB8;2Dq#Y#!ptl`dSZ9d9V0h>{qiKF< z)b>iP*b#xvC8HK=J>8F2bGvwRyGe4h%pY&e*S$Y^!F7%PpcN|uc1m_JGVVqKnUeR8 z-6O$mOWp_xe&+Xoix!+UFXrcRP287QZk2G|S(e>-Z$#o|qm~y#d`=ffYWCd5GFV3+ z*de8^8k4uifJc!&J?d|JsjwWP^jcS`vv01;fBt~zLB0?af--qFg`3XZw_pu>i;rMlvQ&==K3 zI9}x`gSJj0ho}I65(*Z%Ek253Nn%riUajIYu|2)j3yHW>)0)wSaIkuA^32mSpEuV8 z+k5peEFu!3l0)$gU$rYWL&*7&&Cwbvcmo~^l7jMkqhjwzpjBU4pY~5HLH1+!q6Zze8`!FJk$#S zt}%T@1t3_?Anbx(cm_ z>Vt~1%k+>EU>&ZGz>frH3L)moZD&&aF+fH~tAmNzSf}oZUjKz>yX|<&*)aI*RkyMk zVE^)!yjlNCNZ`BSwpRA{TWY7cKl;&p@}_p(@qpe&es1qhDzv7SD%DSpAdVUKSLTjf z(|~P_U7YVuzSH5!HHM7~q2>*`12X!Y2vfL?Kg=PLwLzUs(}QDDzZ$Oe=T|CoWaz(Y z-hm`WW>jMnvP_Lby#$}>J5^Ln2Xv)9vvLb#&||bQsfjsf+X)rq8e)1N&)V=Y|BwANg^4VM&eu2BGS7AQu2+2S&u8>H zm?0C9yidQxuVlUOIK41?C+tiKfmD_bg9{=b zeD zn7e_*%+ES_lnG~J7bi^l5`EZKl-Ba-c?buRlrYB7(J2cFbd6Vn+I@?}>uw4n8t3R- zq(bp`2s?@?p|%s)>#XcFusN02rpJtX_NIoNB2lH1)UYG6^nkLaQ=Uv}d_4AkmZ@0I z>M&VpLir#)hb9%2-5BO5Pw?1DQKwh3bbCjJJEHwn$JG|vVsK&ul$&xR%~6YM8@7QP zpNpT%krLeH;ERMA){Q4B0dy;3tPvf$28Zld3PdyBmmg0svrg;y1$cjvEsaV!(?NnHXqZ|^$pI*vQSU%RO?ykD@vLY$u7UQgF+ zbCe`h>PPyL5SzwoH;R^kbX%3ey2IQ<6XM_q$f4JK zi&miBBh{rf$SWYP%*?-XW@;uVXjDHCVAz_Ig}80~O_kb3C^DkEr3Y7iSw8tC*i6Ef zyQ#%;%B1yZnj#{s8O)m^O(s#1etmKOkz=R;O$|mqQPv)C8lz2YL$t_6ZID^dKpV>te5E85uEaJChPrk zag(f+GPi<^xWV6loNR7XCVP(&B4he5o{f-J zPM_CHboVcm*pErrELl4&y}DCM>sf;|(VJ*0Ihix;AUJ^x;ZIdafJHl1E~D?f*+Ox) zwdgk~f0$#jMD?EzbJyEEDdBFd?wKkxakH#IAoKc!|KqY> zz+@neW}BQRn8fJO0Tbk^5tC%&1N5IV_*<6L{4VJDmjTIOkgW-(=V)T>#KidXu_CTp z9@P5@9(#*z-lf_~!sZmT66Y^bvhjD3+v3$n=6wh_Iu4ByM2NJ2iE=pDHXGg_<%`^l zRKCPVpfAJ4fEpfqJJvP)VBvsF3lII442IP{A0cha^t&vXbje_Qv?fsUBeZAib+${x z3%O*vD2yS6VGCiJ@z1*0!_a01DK^>rMy{y$C7cI9kfIQsoAlTX0MSWGL%R2crAe6{2#X7?CZ4x;ZT0E>Oe6 z-R?js&fjflRcA~pukoyf*`&;Ba5YoSi`xI4gpQ?SDomHx}2n@`i;ihWXFx~9knn=(wR?kS)1B*e{$&}ca0!(1yg zl$193n!WTs&tA$PyM9+qWet{&tl9ugHRl4jxgjyk8oGWhi;EqvoGhk4;@?9LX_SOv z3dF9>W!BV}6*rjnW3hs(1w_m#Cfn$bB=v-lJMUAFQ@I1;8R&&)s))TbMzlVR9R~>v zd9=|_t{6{4h;zC=2)t3%HvM+5$%}<&lH=_(y&_x~vHR_P@XDg-HzGwDa0m>@f1V@+ zE%oDNdCu# Date: Tue, 13 Aug 2019 15:44:20 +0200 Subject: [PATCH 54/70] fix and test issues seen by behnam --- message_ix/macro.py | 27 ++++++++++++++++++++++----- message_ix/tests/test_macro.py | 13 ++++++++++--- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/message_ix/macro.py b/message_ix/macro.py index 75a6f7d05..27ee18c16 100644 --- a/message_ix/macro.py +++ b/message_ix/macro.py @@ -228,6 +228,15 @@ def read_data(self): # base year is first model period self.base_year = min_year_model + def _clean_model_data(self, data): + if 'node' in data: + data = data[data['node'].isin(self.nodes)] + if 'commodity' in data: + data = data[data['commodity'].isin(self.sectors)] + if 'year' in data: + data = data[data['year'].isin(self.years)] + return data + def derive_data(self): # calculate all necessary derived data, adding to self.data this is # done through method chaining, the bottom of which is aconst() @@ -278,7 +287,7 @@ def _total_cost(self): idx = ['node', 'year'] # TODO: in the R code, this value is divided by 1000 # do we need to do that here?!!? - model_cost = self.s.var('COST_NODAL_NET') + model_cost = self._clean_model_data(self.s.var('COST_NODAL_NET')) model_cost.rename(columns={'lvl': 'value'}, inplace=True) model_cost = model_cost[idx + ['value']] # get data provided in init year from data @@ -295,8 +304,9 @@ def _total_cost(self): def _price(self): # read from scenario idx = ['node', 'sector', 'year'] - model_price = self.s.var('PRICE_COMMODITY', - filters={'level': 'useful'}) + model_price = self._clean_model_data( + self.s.var('PRICE_COMMODITY', filters={'level': 'useful'}) + ) if np.isclose(model_price['lvl'], 0).any(): # TODO: this needs a test.. msg = '0-price found in MESSAGE variable PRICE_COMMODITY' @@ -319,7 +329,9 @@ def _price(self): def _demand(self): # read from scenario idx = ['node', 'sector', 'year'] - model_demand = self.s.var('DEMAND', filters={'level': 'useful'}) + model_demand = self._clean_model_data( + self.s.var('DEMAND', filters={'level': 'useful'}) + ) model_demand.rename(columns={'lvl': 'value', 'commodity': 'sector'}, inplace=True) model_demand = model_demand[idx + ['value']] @@ -381,7 +393,12 @@ def init(s): except: # noqa: ignore=E722 s.init_var(key) for key, values in MACRO_INIT['equs'].items(): - s.init_equ(key, values) + try: + s.init_equ(key, values) + except: + # TODO: what to do with scenarios that already have structure? It + # seems that some do and some dont. + pass # keep track of number of iterations s.init_var('N_ITER', None) diff --git a/message_ix/tests/test_macro.py b/message_ix/tests/test_macro.py index 79aac6fe7..6a9d8d68c 100644 --- a/message_ix/tests/test_macro.py +++ b/message_ix/tests/test_macro.py @@ -316,14 +316,21 @@ def test_multiregion_derive_data(): c.read_data() c.derive_data() + nodes = ['R11_AFR', 'R11_CPA'] + sectors = ['i_therm', 'rc_spec'] + + # make sure no extraneous data is there + check = c.data['demand'].reset_index() + assert (check['node'].unique() == nodes).all() + assert (check['sector'].unique() == sectors).all() + obs = c.data['aconst'] exp = pd.Series([3.74767687, 0.00285472], name='value', - index=pd.Index(['R11_AFR', 'R11_CPA'], name='node')) + index=pd.Index(nodes, name='node')) pd.testing.assert_series_equal(obs, exp) obs = c.data['bconst'] - idx = pd.MultiIndex.from_product([['R11_AFR', 'R11_CPA'], - ['i_therm', 'rc_spec']], + idx = pd.MultiIndex.from_product([nodes, sectors], names=['node', 'sector']) exp = pd.Series([1.071971e-08, 1.487598e-11, 9.637483e-09, 6.955715e-13], name='value', index=idx) From 722e54799260f606966c1c1122918b00c09a94d5 Mon Sep 17 00:00:00 2001 From: Matthew Gidden Date: Tue, 13 Aug 2019 15:46:40 +0200 Subject: [PATCH 55/70] stickler --- message_ix/macro.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/message_ix/macro.py b/message_ix/macro.py index 27ee18c16..bcf0093ac 100644 --- a/message_ix/macro.py +++ b/message_ix/macro.py @@ -395,7 +395,7 @@ def init(s): for key, values in MACRO_INIT['equs'].items(): try: s.init_equ(key, values) - except: + except: # noqa: ignore=E722 # TODO: what to do with scenarios that already have structure? It # seems that some do and some dont. pass From d52bc9fe4d70e2223c85df4fa70c83823a93dc0a Mon Sep 17 00:00:00 2001 From: Matthew Gidden Date: Tue, 13 Aug 2019 17:02:34 +0200 Subject: [PATCH 56/70] add set ignore --- message_ix/macro.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/message_ix/macro.py b/message_ix/macro.py index bcf0093ac..8e851f7d9 100644 --- a/message_ix/macro.py +++ b/message_ix/macro.py @@ -375,7 +375,12 @@ def _aconst(self): def init(s): for key, values in MACRO_INIT['sets'].items(): - s.init_set(key, values) + try: + s.init_set(key, values) + except: # noqa: ignore=E722 + # TODO: what to do with scenarios that already have structure? It + # seems that some do and some dont. + pass for key, values in MACRO_INIT['pars'].items(): try: s.init_par(key, values['idx']) From fccded16cd1595c53ee4495c97af847b3bfc3b91 Mon Sep 17 00:00:00 2001 From: Matthew Gidden Date: Wed, 14 Aug 2019 11:47:28 +0200 Subject: [PATCH 57/70] final change required to match global model: divide model costs by 1e3 to get into expected units of T$ --- message_ix/macro.py | 5 +++-- message_ix/tests/test_macro.py | 6 +++--- tests/data/westeros_macro_input.xlsx | Bin 18275 -> 18327 bytes 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/message_ix/macro.py b/message_ix/macro.py index 8e851f7d9..faf468f61 100644 --- a/message_ix/macro.py +++ b/message_ix/macro.py @@ -285,11 +285,12 @@ def _k0(self): def _total_cost(self): # read from scenario idx = ['node', 'year'] - # TODO: in the R code, this value is divided by 1000 - # do we need to do that here?!!? model_cost = self._clean_model_data(self.s.var('COST_NODAL_NET')) model_cost.rename(columns={'lvl': 'value'}, inplace=True) model_cost = model_cost[idx + ['value']] + # TODO: in the R code, this value is divided by 1000 + # do we need to do that here?!!? + model_cost['value'] /= 1e3 # get data provided in init year from data cost_ref = self.data['cost_ref'].reset_index() cost_ref['year'] = self.init_year diff --git a/message_ix/tests/test_macro.py b/message_ix/tests/test_macro.py index 6a9d8d68c..64632f75e 100644 --- a/message_ix/tests/test_macro.py +++ b/message_ix/tests/test_macro.py @@ -180,7 +180,7 @@ def test_calc_total_cost(westeros_solved): # 4 values, 3 in model period, one in history assert len(obs) == 4 obs = obs.values - exp = np.array([15, 17.477751, 22.143633, 28.189798]) + exp = np.array([15, 17.477751, 22.143633, 28.189798]) / 1e3 assert np.isclose(obs, exp).all() @@ -291,11 +291,11 @@ def test_calibrate_roundtrip(westeros_solved): W_DATA_PATH, check_convergence=True) aeei = with_macro.par('aeei')['value'].values assert len(aeei) == 4 - exp = [0.02, 0.07171359, 0.03743102, 0.01990546] + exp = [0.02, 0.07173523, 0.03741514, 0.01990172] assert np.allclose(aeei, exp) grow = with_macro.par('grow')['value'].values assert len(grow) == 4 - exp = [0.02658363, 0.06911822, 0.07950836, 0.02452974] + exp = [0.02658363, 0.06910296, 0.07952086, 0.02452946] assert np.allclose(grow, exp) # diff --git a/tests/data/westeros_macro_input.xlsx b/tests/data/westeros_macro_input.xlsx index 72f7847841c307ee749d188250ea6a93b1cdf179..aa35b505527526ba0a58b64290a9f5faf8e5e115 100644 GIT binary patch delta 13998 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 literal 18275 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
E541K?30lu#H9@4PM#x(hs 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 Date: Wed, 21 Aug 2019 12:35:02 +0200 Subject: [PATCH 58/70] making tested scenarios defaults --- message_ix/macro.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/message_ix/macro.py b/message_ix/macro.py index faf468f61..c1f23934e 100644 --- a/message_ix/macro.py +++ b/message_ix/macro.py @@ -477,9 +477,11 @@ def calibrate(s, check_convergence=True): s.add_par('aeei', aeei) s.add_par('grow', grow) s.commit('Updating MACRO values after calibration') + s.set_as_default() # test to make sure number of iterations is 1 test = s.clone(s.model, 'test to confirm MACRO converges') + test.set_as_default() var_list = ['N_ITER'] test.solve(model='MESSAGE-MACRO', var_list=var_list, gams_args=gams_args) From f5a495fec7e89d8babbf6dc01bc207fb4b70e11d Mon Sep 17 00:00:00 2001 From: Matthew Gidden Date: Wed, 21 Aug 2019 12:37:21 +0200 Subject: [PATCH 59/70] now only run message-macro if check_convergence is True --- message_ix/macro.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/message_ix/macro.py b/message_ix/macro.py index c1f23934e..5fe0ca1e4 100644 --- a/message_ix/macro.py +++ b/message_ix/macro.py @@ -480,17 +480,16 @@ def calibrate(s, check_convergence=True): s.set_as_default() # test to make sure number of iterations is 1 - test = s.clone(s.model, 'test to confirm MACRO converges') - test.set_as_default() - var_list = ['N_ITER'] - test.solve(model='MESSAGE-MACRO', var_list=var_list, gams_args=gams_args) - - n_iter = test.var('N_ITER')['lvl'] - if n_iter > 1: - msg = 'Number of iterations after calibration > 1: {}'.format(n_iter) - if check_convergence: - raise RuntimeError(msg) - else: - warnings.warn(msg) + if check_convergence: + test = s.clone(s.model, 'test to confirm MACRO converges') + var_list = ['N_ITER'] + test.solve(model='MESSAGE-MACRO', + var_list=var_list, gams_args=gams_args) + test.set_as_default() + + n_iter = test.var('N_ITER')['lvl'] + if n_iter > 1: + msg = 'Number of iterations after calibration > 1: {}' + raise RuntimeError(msg.format(n_iter)) return s From 521aefdcb0657ab816d004b076af9631bd36fbda Mon Sep 17 00:00:00 2001 From: Matthew Gidden Date: Wed, 21 Aug 2019 12:37:59 +0200 Subject: [PATCH 60/70] stickler --- message_ix/macro.py | 1 - 1 file changed, 1 deletion(-) diff --git a/message_ix/macro.py b/message_ix/macro.py index 5fe0ca1e4..360171398 100644 --- a/message_ix/macro.py +++ b/message_ix/macro.py @@ -1,6 +1,5 @@ import collections import os -import warnings import numpy as np import pandas as pd From 664208a5f5514aede6a786353311b08fa00c2cc3 Mon Sep 17 00:00:00 2001 From: Unknown Date: Thu, 22 Aug 2019 14:51:50 +0200 Subject: [PATCH 61/70] changed depreciation rate in MACRO MESSAGE interest rate in utility discount factor --- message_ix/model/MACRO/macro_data_load.gms | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/message_ix/model/MACRO/macro_data_load.gms b/message_ix/model/MACRO/macro_data_load.gms index 87cba7178..e7e143505 100755 --- a/message_ix/model/MACRO/macro_data_load.gms +++ b/message_ix/model/MACRO/macro_data_load.gms @@ -231,7 +231,7 @@ LOOP(year_all $( ORD(year_all) > sum(year_all2$( macro_initial_period(year_all2) * new labor supply newlab(node_macro, year_all) = SUM(year_all2$( seq_period(year_all2,year_all) ), (labor(node_macro, year_all) - labor(node_macro, year_all2)*(1 - depr(node_macro))**duration_period(year_all))$((labor(node_macro, year_all) - labor(node_macro, year_all2)*(1 - depr(node_macro))**duration_period(year_all)) > 0)) + epsilon ; * calculation of utility discount factor based on discount rate (drate) - udf(node_macro, year_all) = SUM(year_all2$( seq_period(year_all2,year_all) ), udf(node_macro, year_all2) * (1 - (DRATE(node_macro) - grow(node_macro, year_all)))**duration_period(year_all)) ; + udf(node_macro, year_all) = SUM(year_all2$( seq_period(year_all2,year_all) ), udf(node_macro, year_all2) * (1 - (interestrate(year_all) - grow(node_macro, year_all)))**duration_period(year_all)) ; ); DISPLAY labor, newlab, udf; From 79446a7e7039cae322c33e77e561091d60faa1b2 Mon Sep 17 00:00:00 2001 From: Matthew Gidden Date: Thu, 22 Aug 2019 16:07:47 +0200 Subject: [PATCH 62/70] allow passing through of kwargs to message solve --- message_ix/core.py | 4 ++-- message_ix/macro.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/message_ix/core.py b/message_ix/core.py index 7c316cf9a..9c75c6db9 100755 --- a/message_ix/core.py +++ b/message_ix/core.py @@ -429,14 +429,14 @@ def solve(self, model='MESSAGE', solve_options={}, **kwargs): """ super().solve(model=model, solve_options=solve_options, **kwargs) - def add_macro(self, data, scenario=None, check_convergence=True): + def add_macro(self, data, scenario=None, check_convergence=True, **kwargs): scenario = scenario or '_'.join([self.scenario, 'macro']) clone = self.clone(self.model, scenario, keep_solution=False) clone.check_out() macro.init(clone) macro.add_model_data(self, clone, data) clone.commit('finished adding macro') - macro.calibrate(clone, check_convergence=check_convergence) + macro.calibrate(clone, check_convergence=check_convergence, **kwargs) return clone def rename(self, name, mapping, keep=False): diff --git a/message_ix/macro.py b/message_ix/macro.py index 360171398..76303603d 100644 --- a/message_ix/macro.py +++ b/message_ix/macro.py @@ -448,7 +448,7 @@ def add_model_data(base, clone, data): raise type(e)(msg + str(e)) -def calibrate(s, check_convergence=True): +def calibrate(s, check_convergence=True, **kwargs): # solve MACRO standalone var_list = ['N_ITER', 'MAX_ITER', 'aeei_calibrate', 'grow_calibrate'] gams_args = ['LogOption=2'] # pass everything to log file @@ -482,8 +482,8 @@ def calibrate(s, check_convergence=True): if check_convergence: test = s.clone(s.model, 'test to confirm MACRO converges') var_list = ['N_ITER'] - test.solve(model='MESSAGE-MACRO', - var_list=var_list, gams_args=gams_args) + kwargs['gams_args'] = kwargs.get('gams_args', gams_args) + test.solve(model='MESSAGE-MACRO', var_list=var_list, **kwargs) test.set_as_default() n_iter = test.var('N_ITER')['lvl'] From c0754d2ef4b8e6da4bdfe48fd384bc9094675293 Mon Sep 17 00:00:00 2001 From: Paul Natsuo Kishimoto Date: Thu, 7 Nov 2019 09:10:55 +0100 Subject: [PATCH 63/70] Add 'MACRO' model class --- message_ix/__init__.py | 4 +++- message_ix/models.py | 29 +++++++++++++++++++---------- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/message_ix/__init__.py b/message_ix/__init__.py index 7e7554b14..b59e5abdb 100644 --- a/message_ix/__init__.py +++ b/message_ix/__init__.py @@ -5,10 +5,11 @@ from ixmp.model import MODELS from .core import Scenario # noqa: F401 -from .models import MESSAGE, MESSAGE_MACRO +from .models import MACRO, MESSAGE, MESSAGE_MACRO __all__ = [ + 'MACRO', 'MESSAGE', 'MESSAGE_MACRO', 'MODELS', @@ -21,6 +22,7 @@ # Register models with ixmp core +MODELS['MACRO'] = MACRO MODELS['MESSAGE'] = MESSAGE MODELS['MESSAGE-MACRO'] = MESSAGE_MACRO diff --git a/message_ix/models.py b/message_ix/models.py index 3a8179dcf..5be33cc12 100644 --- a/message_ix/models.py +++ b/message_ix/models.py @@ -4,7 +4,7 @@ import re from ixmp import config -from ixmp.model.gams import GAMSModel +import ixmp.model.gams #: Solver options used by :meth:`message_ix.Scenario.solve`. @@ -21,7 +21,8 @@ def _template(*parts): return str(Path('{model_dir}', *parts)) -class MESSAGE(GAMSModel): +class GAMSModel(ixmp.model.gams.GAMSModel): + """Extended :class:`ixmp.model.gams.GAMSModel` for MESSAGE & MACRO.""" name = 'MESSAGE' #: Default model options. @@ -41,7 +42,7 @@ class MESSAGE(GAMSModel): # Disable the feature to put input/output GDX files, list files, etc. # in a temporary directory 'use_temp_dir': False, - }, GAMSModel.defaults) + }, ixmp.model.gams.GAMSModel.defaults) @classmethod def read_version(cls): @@ -70,9 +71,9 @@ def __init__(self, name=None, **model_options): def run(self, scenario): """Execute the model. - :class:`MESSAGE` creates a file named ``cplex.opt`` in the model - directory, containing the options in :obj:`DEFAULT_CPLEX_OPTIONS`, - or any overrides passed to :meth:`~message_ix.Scenario.solve`. + GAMSModel creates a file named ``cplex.opt`` in the model directory + containing the options in :obj:`DEFAULT_CPLEX_OPTIONS`, or any + overrides passed to :meth:`~message_ix.Scenario.solve`. """ # This is not safe against race conditions; if two runs are kicked off # simulatenously with the same dp.model_path, then they will try to @@ -97,11 +98,15 @@ def run(self, scenario): return result -class MESSAGE_MACRO(MESSAGE): - name = 'MESSAGE-MACRO' +class MESSAGE(GAMSModel): + name = 'MESSAGE' + - #: MESSAGE-MACRO uses the GAMS ``break;`` statement, and thus requires GAMS - #: 24.8.1 or later. +class MACRO(GAMSModel): + name = 'MACRO' + + #: MACRO uses the GAMS ``break;`` statement, and thus requires GAMS 24.8.1 + #: or later. GAMS_min_version = '24.8.1' def __init__(self, *args, **kwargs): @@ -114,6 +119,10 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) +class MESSAGE_MACRO(MACRO): + name = 'MESSAGE-MACRO' + + def gams_release(): """Return the GAMS release as a string, e.g. '24.7.4'.""" # TODO move this upstream to ixmp.model.gams From 3b3a63383e38c0441d753ea7097f94ccf5de763b Mon Sep 17 00:00:00 2001 From: Paul Natsuo Kishimoto Date: Tue, 3 Mar 2020 22:49:54 +0100 Subject: [PATCH 64/70] Override GAMSModel.initialize in MACRO class - macro.MACRO_INIT is split to MACRO_ITEMS, DATA_KEY, and UNITS; the former organized to be compatible with ixmp.GAMSModel.initialize_items. - macro.init() is removed. - A workaround is introduced for a bug in ixmp_source that prevents items being initialized, even if the 'correct' idx_sets are given. - Tests adjusted. --- message_ix/core.py | 10 +- message_ix/macro.py | 238 ++++++++++++--------------------- message_ix/models.py | 20 ++- message_ix/tests/test_macro.py | 15 +-- 4 files changed, 117 insertions(+), 166 deletions(-) diff --git a/message_ix/core.py b/message_ix/core.py index 9c75c6db9..aab47957a 100755 --- a/message_ix/core.py +++ b/message_ix/core.py @@ -6,8 +6,6 @@ from ixmp.utils import as_str_list, pd_read, pd_write, isscalar, logger import pandas as pd -from . import macro - class Scenario(ixmp.Scenario): """|MESSAGEix| Scenario. @@ -430,10 +428,16 @@ def solve(self, model='MESSAGE', solve_options={}, **kwargs): super().solve(model=model, solve_options=solve_options, **kwargs) def add_macro(self, data, scenario=None, check_convergence=True, **kwargs): + from . import macro + from .models import MACRO + scenario = scenario or '_'.join([self.scenario, 'macro']) clone = self.clone(self.model, scenario, keep_solution=False) clone.check_out() - macro.init(clone) + + # Add ixmp items: sets, parameters, variables, and equations + MACRO.initialize(clone) + macro.add_model_data(self, clone, data) clone.commit('finished adding macro') macro.calibrate(clone, check_convergence=check_convergence, **kwargs) diff --git a/message_ix/macro.py b/message_ix/macro.py index 76303603d..f10f1044a 100644 --- a/message_ix/macro.py +++ b/message_ix/macro.py @@ -15,108 +15,76 @@ # to support others # -MACRO_INIT = { - 'sets': { - 'sector': None, - 'mapping_macro_sector': ['sector', 'commodity', 'level', ], - }, - 'pars': { - 'demand_MESSAGE': { - 'idx': ['node', 'sector', 'year', ], - 'unit': 'GWa', - 'data_key': 'demand', - }, - 'price_MESSAGE': { - 'idx': ['node', 'sector', 'year', ], - 'unit': 'USD/kWa', - 'data_key': 'price', - }, - 'cost_MESSAGE': { - 'idx': ['node', 'year', ], - 'unit': 'G$', - 'data_key': 'total_cost', - }, - 'gdp_calibrate': { - 'idx': ['node', 'year', ], - 'unit': 'T$', - }, - 'historical_gdp': { - 'idx': ['node', 'year', ], - 'unit': 'T$', - 'data_key': 'gdp0', - }, - 'MERtoPPP': { - 'idx': ['node', 'year', ], - 'unit': '-', - }, - 'kgdp': { - 'idx': ['node', ], - 'unit': '-', - }, - 'kpvs': { - 'idx': ['node', ], - 'unit': '-', - }, - 'depr': { - 'idx': ['node', ], - 'unit': '-', - }, - 'drate': { - 'idx': ['node', ], - 'unit': '-', - }, - 'esub': { - 'idx': ['node', ], - 'unit': '-', - }, - 'lotol': { - 'idx': ['node', ], - 'unit': '-', - }, - 'lakl': { - 'idx': ['node', ], - 'unit': '-', - 'data_key': 'aconst', - }, - 'prfconst': { - 'idx': ['node', 'sector', ], - 'unit': '-', - 'data_key': 'bconst', - }, - 'grow': { - 'idx': ['node', 'year', ], - 'unit': '-', - 'data_key': 'growth', - }, - 'aeei': { - 'idx': ['node', 'sector', 'year', ], - 'unit': '-', - }, - }, - 'vars': { - 'DEMAND': ['node', 'commodity', 'level', 'year', 'time', ], - 'PRICE': ['node', 'commodity', 'level', 'year', 'time', ], - 'COST_NODAL': ['node', 'year', ], - 'COST_NODAL_NET': ['node', 'year', ], - 'GDP': ['node', 'year', ], - 'I': ['node', 'year', ], - 'C': ['node', 'year', ], - 'K': ['node', 'year', ], - 'KN': ['node', 'year', ], - 'Y': ['node', 'year', ], - 'YN': ['node', 'year', ], - 'EC': ['node', 'year', ], - 'UTILITY': None, - 'PHYSENE': ['node', 'sector', 'year', ], - 'PRODENE': ['node', 'sector', 'year', ], - 'NEWENE': ['node', 'sector', 'year', ], - 'grow_calibrate': ['node', 'year', ], - 'aeei_calibrate': ['node', 'sector', 'year', ], - }, - 'equs': { - 'COST_ACCOUNTING_NODAL': ['node', 'year', ], - }, -} +DATA_KEY = dict( + cost_MESSAGE='total_cost', + demand_MESSAGE='demand', + grow='growth', + historical_gdp='gdp0', + lakl='aconst', + prfconst='bconst', + price_MESSAGE='price', +) + +UNITS = dict( + cost_MESSAGE='G$', + demand_MESSAGE='GWa', + gdp_calibrate='T$', + historical_gdp='T$', + price_MESSAGE='USD/kWa', + # Used in calibrate() + aeei='-', + grow='-', +) + +#: ixmp items (sets, parameters, variables, and equations) in MACRO. +MACRO_ITEMS = dict( + sector=dict(ix_type='set'), + mapping_macro_sector=dict(ix_type='set', + idx_sets=['sector', 'commodity', 'level']), + + MERtoPPP=dict(ix_type='par', idx_sets=['node', 'year']), + aeei=dict(ix_type='par', idx_sets=['node', 'sector', 'year']), + cost_MESSAGE=dict(ix_type='par', idx_sets=['node', 'year']), + demand_MESSAGE=dict(ix_type='par', idx_sets=['node', 'sector', 'year']), + depr=dict(ix_type='par', idx_sets=['node']), + drate=dict(ix_type='par', idx_sets=['node']), + esub=dict(ix_type='par', idx_sets=['node']), + gdp_calibrate=dict(ix_type='par', idx_sets=['node', 'year']), + grow=dict(ix_type='par', idx_sets=['node', 'year']), + historical_gdp=dict(ix_type='par', idx_sets=['node', 'year']), + kgdp=dict(ix_type='par', idx_sets=['node']), + kpvs=dict(ix_type='par', idx_sets=['node']), + lakl=dict(ix_type='par', idx_sets=['node']), + lotol=dict(ix_type='par', idx_sets=['node']), + prfconst=dict(ix_type='par', idx_sets=['node', 'sector']), + price_MESSAGE=dict(ix_type='par', idx_sets=['node', 'sector', 'year']), + + C=dict(ix_type='var', idx_sets=['node', 'year']), + COST_NODAL_NET=dict(ix_type='var', idx_sets=['node', 'year']), + COST_NODAL=dict(ix_type='var', idx_sets=['node', 'year']), + DEMAND=dict(ix_type='var', + idx_sets=['node', 'commodity', 'level', 'year', 'time']), + EC=dict(ix_type='var', idx_sets=['node', 'year']), + GDP=dict(ix_type='var', idx_sets=['node', 'year']), + I=dict(ix_type='var', idx_sets=['node', 'year']), # noqa: E741 + K=dict(ix_type='var', idx_sets=['node', 'year']), + KN=dict(ix_type='var', idx_sets=['node', 'year']), + MAX_ITER=dict(ix_type='var', idx_sets=None), + N_ITER=dict(ix_type='var', idx_sets=None), + NEWENE=dict(ix_type='var', idx_sets=['node', 'sector', 'year']), + PHYSENE=dict(ix_type='var', idx_sets=['node', 'sector', 'year']), + PRICE=dict(ix_type='var', + idx_sets=['node', 'commodity', 'level', 'year', 'time']), + PRODENE=dict(ix_type='var', idx_sets=['node', 'sector', 'year']), + UTILITY=dict(ix_type='var', idx_sets=None), + Y=dict(ix_type='var', idx_sets=['node', 'year']), + YN=dict(ix_type='var', idx_sets=['node', 'year']), + aeei_calibrate=dict(ix_type='var', idx_sets=['node', 'sector', 'year']), + grow_calibrate=dict(ix_type='var', idx_sets=['node', 'year']), + + COST_ACCOUNTING_NODAL=dict(ix_type='equ', idx_sets=['node', 'year']), +) + MACRO_DATA_FOR_DERIVATION = { 'cost_ref': ['node', ], @@ -145,7 +113,7 @@ def validate(kind, values, df): if name in MACRO_DATA_FOR_DERIVATION: cols = MACRO_DATA_FOR_DERIVATION[name] else: - cols = MACRO_INIT['pars'][name]['idx'] + cols = MACRO_ITEMS[name]['idx_sets'] # TODO: cols += ['unit'] ? col_diff = set(cols) - set(df.columns) if col_diff: @@ -373,43 +341,6 @@ def _aconst(self): return self.data['aconst'] -def init(s): - for key, values in MACRO_INIT['sets'].items(): - try: - s.init_set(key, values) - except: # noqa: ignore=E722 - # TODO: what to do with scenarios that already have structure? It - # seems that some do and some dont. - pass - for key, values in MACRO_INIT['pars'].items(): - try: - s.init_par(key, values['idx']) - except: # noqa: ignore=E722 - pass # already exists in the model, known for 'historical_gdp' - for key, values in MACRO_INIT['vars'].items(): - if not s.has_var(key): - try: - # TODO: this seems required because for some reason DEMAND (and - # perhaps others) seem to already be listed in the java code, - # but still needs to be initialized in the python code. - # However, you cannot init it with dimensions, only with the - # variable name. - s.init_var(key, values) - except: # noqa: ignore=E722 - s.init_var(key) - for key, values in MACRO_INIT['equs'].items(): - try: - s.init_equ(key, values) - except: # noqa: ignore=E722 - # TODO: what to do with scenarios that already have structure? It - # seems that some do and some dont. - pass - - # keep track of number of iterations - s.init_var('N_ITER', None) - s.init_var('MAX_ITER', None) - - def add_model_data(base, clone, data): c = Calculate(base, data) c.read_data() @@ -433,11 +364,14 @@ def add_model_data(base, clone, data): clone.add_set("mapping_macro_sector", [s, s, "useful"]) # add parameters - for name, values in MACRO_INIT['pars'].items(): + for name, info in MACRO_ITEMS.items(): + if info['ix_type'] != 'par': + continue + try: - key = values.get('data_key', name) + key = DATA_KEY.get(name, name) data = c.data[key].reset_index() - data['unit'] = values['unit'] + data['unit'] = UNITS.get(name, '-') # some data may have information prior to the MACRO initialization # year which we need to remove in order to add it to the scenario if 'year' in data: @@ -459,16 +393,14 @@ def calibrate(s, check_convergence=True, **kwargs): logger().info(msg.format(n_iter, max_iter)) # get out calibrated values - aeei = (s.var('aeei_calibrate') - .rename(columns={'lvl': 'value'}) - .drop('mrg', axis=1) - ) - aeei['unit'] = MACRO_INIT['pars']['aeei']['unit'] - grow = (s.var('grow_calibrate') - .rename(columns={'lvl': 'value'}) - .drop('mrg', axis=1) - ) - grow['unit'] = MACRO_INIT['pars']['grow']['unit'] + aeei = s.var('aeei_calibrate') \ + .rename(columns={'lvl': 'value'}) \ + .drop('mrg', axis=1) \ + .assign(unit=UNITS['aeei']) + grow = s.var('grow_calibrate') \ + .rename(columns={'lvl': 'value'}) \ + .drop('mrg', axis=1) \ + .assign(unit=UNITS['grow']) # update calibrated value parameters s.remove_solution() diff --git a/message_ix/models.py b/message_ix/models.py index 5be33cc12..f13e5ea15 100644 --- a/message_ix/models.py +++ b/message_ix/models.py @@ -1,11 +1,13 @@ from collections import ChainMap -from copy import copy +from copy import copy, deepcopy from pathlib import Path import re from ixmp import config import ixmp.model.gams +from .macro import MACRO_ITEMS + #: Solver options used by :meth:`message_ix.Scenario.solve`. DEFAULT_CPLEX_OPTIONS = { @@ -118,6 +120,22 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + @classmethod + def initialize(cls, scenario, with_data=False): + """Initialize the model structure.""" + # NB some scenarios already have these items. This method simply adds + # any missing items. + + # FIXME the Java code under the JDBCBackend (ixmp_source) refuses to + # initialize these items with specified idx_sets—even if the + # sets are correct. + items = deepcopy(MACRO_ITEMS) + for name in 'C', 'COST_NODAL', 'COST_NODAL_NET', 'DEMAND', 'GDP', 'I': + items[name].pop('idx_sets') + + # Initialize the ixmp items + cls.initialize_items(scenario, items) + class MESSAGE_MACRO(MACRO): name = 'MESSAGE-MACRO' diff --git a/message_ix/tests/test_macro.py b/message_ix/tests/test_macro.py index 64632f75e..44fa185c8 100644 --- a/message_ix/tests/test_macro.py +++ b/message_ix/tests/test_macro.py @@ -1,14 +1,11 @@ -import pytest +from pathlib import Path import numpy as np import pandas as pd - -try: - from pathlib import Path -except ImportError: - from pathlib2 import Path +import pytest from message_ix import Scenario, macro +from message_ix.models import MACRO from message_ix.testing import make_westeros # tons of deprecation warnings come from reading excel (xlrd library), ignore @@ -236,7 +233,7 @@ def test_init(test_mp): scen = scen.clone('foo', 'bar') scen.check_out() - macro.init(scen) + MACRO.initialize(scen) scen.commit('foo') scen.solve() @@ -251,7 +248,7 @@ def test_add_model_data(westeros_solved): base = westeros_solved clone = base.clone('foo', 'bar', keep_solution=False) clone.check_out() - macro.init(clone) + MACRO.initialize(clone) macro.add_model_data(base, clone, W_DATA_PATH) clone.commit('finished adding macro') clone.solve() @@ -265,7 +262,7 @@ def test_calibrate(westeros_solved): clone = base.clone(base.model, 'test macro calibration', keep_solution=False) clone.check_out() - macro.init(clone) + MACRO.initialize(clone) macro.add_model_data(base, clone, W_DATA_PATH) clone.commit('finished adding macro') From 0c144872d11ba546fbd8573287af3607e37ee14f Mon Sep 17 00:00:00 2001 From: Unknown Date: Mon, 2 Mar 2020 22:08:58 +0100 Subject: [PATCH 65/70] reverted change from drate to interestrate in MACRO - reverted change from (constant) drate to (time-dependent) interestrate in MACRO - switched to lowercase spelling of drate --- message_ix/model/MACRO/macro_calibration.gms | 10 +++++----- message_ix/model/MACRO/macro_data_load.gms | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/message_ix/model/MACRO/macro_calibration.gms b/message_ix/model/MACRO/macro_calibration.gms index 5503c8b28..86649e117 100644 --- a/message_ix/model/MACRO/macro_calibration.gms +++ b/message_ix/model/MACRO/macro_calibration.gms @@ -15,8 +15,8 @@ Scalar Variables aeei_calibrate(node,sector,year_all) grow_calibrate(node,year_all) - N_ITER - MAX_ITER + N_ITER + MAX_ITER ; * ------------------------------------------------------------------------------ @@ -84,11 +84,11 @@ LOOP(year_all $( ORD(year_all) > sum(year_all2$( macro_initial_period(year_all2) * new labor supply newlab(node_macro, year_all) = SUM(year_all2$( seq_period(year_all2,year_all) ), (labor(node_macro, year_all) - labor(node_macro, year_all2)*(1 - depr(node_macro))**duration_period(year_all))$((labor(node_macro, year_all) - labor(node_macro, year_all2)*(1 - depr(node_macro))**duration_period(year_all)) > 0)) + epsilon ; * calculation of utility discount factor based on discount rate (drate) - udf(node_macro, year_all) = SUM(year_all2$( seq_period(year_all2,year_all) ), udf(node_macro, year_all2) * (1 - (DRATE(node_macro) - grow(node_macro, year_all)))**duration_period(year_all)) ; + udf(node_macro, year_all) = SUM(year_all2$( seq_period(year_all2,year_all) ), udf(node_macro, year_all2) * (1 - (drate(node_macro) - grow(node_macro, year_all)))**duration_period(year_all)) ; ); * recalcualte finite time horizon correction of utility function -finite_time_corr(node_macro, year) = abs(DRATE(node_macro) - grow(node_macro, year)) ; +finite_time_corr(node_macro, year) = abs(drate(node_macro) - grow(node_macro, year)) ; ) ; @@ -99,7 +99,7 @@ grow_calibrate.L(node_macro,year) = grow(node_macro,year) ; * subtract one due to 1-based indexing N_ITER.L = ctr - 1; MAX_ITER.L = max_it ; - + * write solution statistics status('MESSAGE_MACRO','modelstat') = 1 ; status('MESSAGE_MACRO','solvestat') = 1 ; diff --git a/message_ix/model/MACRO/macro_data_load.gms b/message_ix/model/MACRO/macro_data_load.gms index e7e143505..8cbb9eb9d 100755 --- a/message_ix/model/MACRO/macro_data_load.gms +++ b/message_ix/model/MACRO/macro_data_load.gms @@ -231,7 +231,7 @@ LOOP(year_all $( ORD(year_all) > sum(year_all2$( macro_initial_period(year_all2) * new labor supply newlab(node_macro, year_all) = SUM(year_all2$( seq_period(year_all2,year_all) ), (labor(node_macro, year_all) - labor(node_macro, year_all2)*(1 - depr(node_macro))**duration_period(year_all))$((labor(node_macro, year_all) - labor(node_macro, year_all2)*(1 - depr(node_macro))**duration_period(year_all)) > 0)) + epsilon ; * calculation of utility discount factor based on discount rate (drate) - udf(node_macro, year_all) = SUM(year_all2$( seq_period(year_all2,year_all) ), udf(node_macro, year_all2) * (1 - (interestrate(year_all) - grow(node_macro, year_all)))**duration_period(year_all)) ; + udf(node_macro, year_all) = SUM(year_all2$( seq_period(year_all2,year_all) ), udf(node_macro, year_all2) * (1 - (drate(node_macro) - grow(node_macro, year_all)))**duration_period(year_all)) ; ); DISPLAY labor, newlab, udf; @@ -263,4 +263,4 @@ DISPLAY ecst0, k0, i0, c0, y0 ; * simply taken. * ------------------------------------------------------------------------------ -finite_time_corr(node_macro, year) = abs(DRATE(node_macro) - grow(node_macro, year)) ; +finite_time_corr(node_macro, year) = abs(drate(node_macro) - grow(node_macro, year)) ; From 62d88347f56e653970de22ad3933a9de66a7b721 Mon Sep 17 00:00:00 2001 From: Paul Natsuo Kishimoto Date: Fri, 13 Mar 2020 11:06:57 +0100 Subject: [PATCH 66/70] Clean up rebase: relocate MACRO test data --- .../tests}/data/multiregion_macro_input.xlsx | Bin .../tests}/data/westeros_macro_input.xlsx | Bin 2 files changed, 0 insertions(+), 0 deletions(-) rename {tests => message_ix/tests}/data/multiregion_macro_input.xlsx (100%) rename {tests => message_ix/tests}/data/westeros_macro_input.xlsx (100%) diff --git a/tests/data/multiregion_macro_input.xlsx b/message_ix/tests/data/multiregion_macro_input.xlsx similarity index 100% rename from tests/data/multiregion_macro_input.xlsx rename to message_ix/tests/data/multiregion_macro_input.xlsx diff --git a/tests/data/westeros_macro_input.xlsx b/message_ix/tests/data/westeros_macro_input.xlsx similarity index 100% rename from tests/data/westeros_macro_input.xlsx rename to message_ix/tests/data/westeros_macro_input.xlsx From d29814cb414538a97a571454037de2f0a05471c1 Mon Sep 17 00:00:00 2001 From: Paul Natsuo Kishimoto Date: Fri, 13 Mar 2020 11:11:24 +0100 Subject: [PATCH 67/70] Handle any path-like argument to macro.Calculate() --- message_ix/macro.py | 38 +++++++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/message_ix/macro.py b/message_ix/macro.py index f10f1044a..cf224a757 100644 --- a/message_ix/macro.py +++ b/message_ix/macro.py @@ -1,5 +1,6 @@ import collections import os +from pathlib import Path import numpy as np import pandas as pd @@ -133,23 +134,34 @@ def validate(kind, values, df): return cols -class Calculate(object): +class Calculate: + """Perform and store MACRO calibration calculations. + s : .Scenario + *s* must have a solution. + data : dict (str -> pd.DataFrame) or os.PathLike + If :class:`.PathLike`, the path to an Excel file containing parameter + data, one per sheet. If :class:`dict`, a dictionary mapping parameter + names to data frames. + """ + # TODO add comments + # TODO add docstrings def __init__(self, s, data): - """ - s : solved message scenario - data : dict of parameter names to dataframes - """ - self.data = data self.s = s - good = isinstance(data, collections.Mapping) or os.path.exists(data) - if not good: - raise ValueError('Data argument is not a dictionary nor a file') - if not isinstance(data, collections.Mapping) and os.path.exists(data): - if not str(self.data).endswith('xlsx'): - raise ValueError('Must provide excel-based data file') - self.data = pd.read_excel(self.data, sheet_name=None) + if isinstance(data, collections.Mapping): + self.data = data + else: + # Handle a file path + try: + data_path = Path(data) + except ValueError: + raise ValueError(f'neither a dict nor a valid path: {data}') + + if not data_path.exists() or data_path.suffix != '.xlsx': + raise ValueError(f'not an Excel data file: {data_path}') + + self.data = pd.read_excel(data_path, sheet_name=None) if not s.has_solution(): raise RuntimeError('Scenario must have a solution to add MACRO') From e6f60ed363d5c8f8be5171fada6762ec50861545 Mon Sep 17 00:00:00 2001 From: Paul Natsuo Kishimoto Date: Fri, 13 Mar 2020 11:46:06 +0100 Subject: [PATCH 68/70] Use testing.SCENARIO in test_macro --- message_ix/macro.py | 6 +++++- message_ix/tests/test_macro.py | 18 ++++++++---------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/message_ix/macro.py b/message_ix/macro.py index cf224a757..a62d8f406 100644 --- a/message_ix/macro.py +++ b/message_ix/macro.py @@ -137,8 +137,12 @@ def validate(kind, values, df): class Calculate: """Perform and store MACRO calibration calculations. + The :class:`.Scenario` *s* must: + + - have a solution; + - have demand on the level 'useful'. + s : .Scenario - *s* must have a solution. data : dict (str -> pd.DataFrame) or os.PathLike If :class:`.PathLike`, the path to an Excel file containing parameter data, one per sheet. If :class:`dict`, a dictionary mapping parameter diff --git a/message_ix/tests/test_macro.py b/message_ix/tests/test_macro.py index 44fa185c8..83bb41fc7 100644 --- a/message_ix/tests/test_macro.py +++ b/message_ix/tests/test_macro.py @@ -6,13 +6,12 @@ from message_ix import Scenario, macro from message_ix.models import MACRO -from message_ix.testing import make_westeros +from message_ix.testing import SCENARIO, make_westeros # tons of deprecation warnings come from reading excel (xlrd library), ignore # them for now pytestmark = pytest.mark.filterwarnings("ignore") -MSG_ARGS = ('canning problem (MESSAGE scheme)', 'standard') W_DATA_PATH = Path(__file__).parent / 'data' / 'westeros_macro_input.xlsx' MR_DATA_PATH = Path(__file__).parent / 'data' / 'multiregion_macro_input.xlsx' @@ -51,15 +50,14 @@ def var(self, name, **kwargs): return df -# TODO: what scope should these be? -@pytest.fixture(scope='function') +@pytest.fixture(scope='class') def westeros_solved(test_mp): - return make_westeros(test_mp, solve=True) + yield make_westeros(test_mp, solve=True) -@pytest.fixture(scope='function') -def westeros_not_solved(test_mp): - return make_westeros(test_mp, solve=False) +@pytest.fixture(scope='class') +def westeros_not_solved(westeros_solved): + yield westeros_solved.clone(keep_solution=False) def test_calc_valid_data_file(westeros_solved): @@ -228,8 +226,8 @@ def test_calc_aconst(westeros_solved): assert np.isclose(obs, exp) -def test_init(test_mp): - scen = Scenario(test_mp, *MSG_ARGS) +def test_init(message_test_mp): + scen = Scenario(message_test_mp, **SCENARIO['dantzig']) scen = scen.clone('foo', 'bar') scen.check_out() From 4cbae0c6fed84a26298562c2389f7fa7163c36c3 Mon Sep 17 00:00:00 2001 From: Paul Natsuo Kishimoto Date: Fri, 13 Mar 2020 11:54:37 +0100 Subject: [PATCH 69/70] Remove unused 'import os' from macro --- message_ix/macro.py | 1 - 1 file changed, 1 deletion(-) diff --git a/message_ix/macro.py b/message_ix/macro.py index a62d8f406..e94484666 100644 --- a/message_ix/macro.py +++ b/message_ix/macro.py @@ -1,5 +1,4 @@ import collections -import os from pathlib import Path import numpy as np From 3a637646ea004df357ce540563482fd680af6244 Mon Sep 17 00:00:00 2001 From: Paul Natsuo Kishimoto Date: Sun, 15 Mar 2020 15:47:54 +0100 Subject: [PATCH 70/70] Add warning message to Scenario.add_macro --- message_ix/core.py | 19 ++++++++++++++++--- message_ix/macro.py | 31 ++++++++++++++++++++++--------- 2 files changed, 38 insertions(+), 12 deletions(-) diff --git a/message_ix/core.py b/message_ix/core.py index aab47957a..9eb0b5383 100755 --- a/message_ix/core.py +++ b/message_ix/core.py @@ -1,12 +1,21 @@ import collections from functools import lru_cache from itertools import product +import logging import ixmp from ixmp.utils import as_str_list, pd_read, pd_write, isscalar, logger import pandas as pd +log = logging.getLogger(__name__) + +# Also print warnings to stderr +_sh = logging.StreamHandler() +_sh.setLevel(level=logging.WARNING) +log.addHandler(_sh) + + class Scenario(ixmp.Scenario): """|MESSAGEix| Scenario. @@ -428,9 +437,13 @@ def solve(self, model='MESSAGE', solve_options={}, **kwargs): super().solve(model=model, solve_options=solve_options, **kwargs) def add_macro(self, data, scenario=None, check_convergence=True, **kwargs): - from . import macro + # TODO document + from .macro import EXPERIMENTAL, add_model_data, calibrate from .models import MACRO + # Display a warning + log.warning(EXPERIMENTAL) + scenario = scenario or '_'.join([self.scenario, 'macro']) clone = self.clone(self.model, scenario, keep_solution=False) clone.check_out() @@ -438,9 +451,9 @@ def add_macro(self, data, scenario=None, check_convergence=True, **kwargs): # Add ixmp items: sets, parameters, variables, and equations MACRO.initialize(clone) - macro.add_model_data(self, clone, data) + add_model_data(self, clone, data) clone.commit('finished adding macro') - macro.calibrate(clone, check_convergence=check_convergence, **kwargs) + calibrate(clone, check_convergence=check_convergence, **kwargs) return clone def rename(self, name, mapping, keep=False): diff --git a/message_ix/macro.py b/message_ix/macro.py index e94484666..0aadfc35b 100644 --- a/message_ix/macro.py +++ b/message_ix/macro.py @@ -1,19 +1,32 @@ import collections +from functools import lru_cache +import logging from pathlib import Path import numpy as np import pandas as pd -from functools import lru_cache -from ixmp.utils import logger +log = logging.getLogger(__name__) + + +EXPERIMENTAL = """ +======================= WARNING ======================= + +You are using *experimental*, incomplete features from +`message_ix.macro`—please exercise caution. Read more: +- https://github.com/iiasa/message_ix/issues/315 +- https://github.com/iiasa/message_ix/issues/317 +- https://github.com/iiasa/message_ix/issues/318 +- https://github.com/iiasa/message_ix/issues/319 +- https://github.com/iiasa/message_ix/issues/320 + +====================================================== +""" + -# -# TODOS: -# -# 1) all demands/prices assumed to be on USEFUL level, need to extend this -# to support others -# +# TODO all demands and prices are assumed to be on USEFUL level, need to extend +# this to support others DATA_KEY = dict( cost_MESSAGE='total_cost', @@ -405,7 +418,7 @@ def calibrate(s, check_convergence=True, **kwargs): n_iter = s.var('N_ITER')['lvl'] max_iter = s.var('MAX_ITER')['lvl'] msg = 'MACRO converged after {} of a maximum of {} iterations' - logger().info(msg.format(n_iter, max_iter)) + log.info(msg.format(n_iter, max_iter)) # get out calibrated values aeei = s.var('aeei_calibrate') \