def check_solution(scen: Scenario) -> None: """Perform several assertions about the solution of `scen`.""" # Reading "ACT" and "CAP" from the solution act = scen.var("ACT").set_index(["technology", "time"]) cap = scen.var("CAP").set_index(["technology"]) # 1) ACT is zero when capacity factor is zero cf = scen.par("capacity_factor").set_index(["technology", "time"]) cf_zero = cf.loc[cf["value"] == 0] for i in cf_zero.index: assert act.loc[i, "lvl"] == 0 # 2) CAP is correctly calculated based on ACT and capacity_factor for i in act.loc[act["lvl"] > 0].index: # Correct ACT based on duration of each time slice duration = float(scen.par("duration_time", {"time": i[1]})["value"]) act.loc[i, "duration-corrected"] = act.loc[i, "lvl"] / duration # Divide by (non-zero) capacity factor act.loc[i, "cf-corrected"] = act.loc[i, "duration-corrected"] / float( cf.loc[i, "value"] ) act = act.fillna(0).reset_index().set_index(["technology"]) # CAP = max("ACT" / "duration_time" / "capcity_factor") for i in cap.index: assert max(act.loc[i, "cf-corrected"]) == float(cap.loc[i, "lvl"])
def group_data(var: str, results: message_ix.Scenario) -> pd.DataFrame: # TODO: add as variable units = { 'ACT': 'GWa/a', 'CAP': 'GW', 'CAP_NEW': 'GW/a', 'EMISS': 'MtCO2/a' } historicals = { 'ACT': 'historical_activity', 'CAP_NEW': 'historical_new_capacity' } df = results.var(var) df = df.loc[df.lvl != 0] if var in historicals: df_hist = results.par(historicals[var]) df_hist = df_hist.rename(columns={'value': 'lvl'}) df_hist = df_hist.loc[df_hist.lvl != 0] df = df.append(df_hist, sort=False) # group Variable by technology and reshape to timeseries format if 'year_act' in df.columns: df = df[['node_loc', 'technology', 'year_act', 'lvl']] df = df.groupby(['node_loc', 'year_act', 'technology'], as_index=False).sum().copy() df = df.rename(columns={'node_loc': 'node', 'year_act': 'year'}) elif var == 'EMISS': df_hist = results.par('historical_emission') df_hist = df_hist.rename(columns={ 'type_emission': 'emission', 'type_year': 'year', 'value': 'lvl' }) df_hist = df_hist.loc[df_hist.lvl != 0] df = df.append(df_hist, sort=False) df['year'] = df.year.astype(int) df = df[['node', 'emission', 'year', 'lvl']] df = df.groupby(['node', 'year', 'emission'], as_index=False).sum().copy() else: df = df[['node_loc', 'technology', 'year_vtg', 'lvl']] df = df.groupby(['node_loc', 'year_vtg', 'technology'], as_index=False).sum().copy() df = df.rename(columns={'node_loc': 'node', 'year_vtg': 'year'}) df['unit'] = units[var] df['variable'] = var return df
def techs_same_lvl_com(scenario: message_ix.Scenario, level: str, commodity: str) -> List[str]: scenario.commodity = commodity output = scenario.par('output') _techs = output[(output.level == level) & (output.commodity == commodity)] _techs = _techs[~_techs.technology.str.contains('slack')] techs = sorted(list(set(_techs.technology.values))) return techs
def test_excel_read_write(test_mp): fname = 'test_excel_read_write.xlsx' scen1 = Scenario(test_mp, *msg_args) scen1.to_excel(fname) scen2 = Scenario(test_mp, model='foo', scenario='bar', version='new') scen2.read_excel(fname) exp = scen1.par('input') obs = scen2.par('input') pdt.assert_frame_equal(exp, obs) scen2.commit('foo') # must be checked in scen2.solve() assert np.isclose(scen2.var('OBJ')['lvl'], 153.675) os.remove(fname)
def test_excel_read_write(message_test_mp, tmp_path): # Path to temporary file tmp_path /= 'excel_read_write.xlsx' # Convert to string to ensure this can be handled fname = str(tmp_path) scen1 = Scenario(message_test_mp, **SCENARIO['dantzig']) scen1 = scen1.clone(keep_solution=False) scen1.check_out() scen1.init_set('new_set') scen1.add_set('new_set', 'member') scen1.init_par('new_par', idx_sets=['new_set']) scen1.add_par('new_par', 'member', 2, '-') scen1.commit('new set and parameter added.') # Writing to Excel without solving scen1.to_excel(fname) # Writing to Excel when scenario has a solution scen1.solve() scen1.to_excel(fname) scen2 = Scenario(message_test_mp, model='foo', scenario='bar', version='new') # Fails without init_items=True with pytest.raises(ValueError, match="no set 'new_set'"): scen2.read_excel(fname) # Succeeds with init_items=True scen2.read_excel(fname, init_items=True, commit_steps=True) exp = scen1.par('input') obs = scen2.par('input') pdt.assert_frame_equal(exp, obs) assert scen2.has_par('new_par') assert float(scen2.par('new_par')['value']) == 2 scen2.commit('foo') # must be checked in scen2.solve() assert np.isclose(scen2.var('OBJ')['lvl'], scen1.var('OBJ')['lvl'])
def test_excel_read_write(message_test_mp, tmp_path): # Path to temporary file tmp_path /= "excel_read_write.xlsx" # Convert to string to ensure this can be handled fname = str(tmp_path) scen1 = Scenario(message_test_mp, **SCENARIO["dantzig"]) scen1 = scen1.clone(keep_solution=False) scen1.check_out() scen1.init_set("new_set") scen1.add_set("new_set", "member") scen1.init_par("new_par", idx_sets=["new_set"]) scen1.add_par("new_par", "member", 2, "-") scen1.commit("new set and parameter added.") # Writing to Excel without solving scen1.to_excel(fname) # Writing to Excel when scenario has a solution scen1.solve() scen1.to_excel(fname) scen2 = Scenario(message_test_mp, model="foo", scenario="bar", version="new") # Fails without init_items=True with pytest.raises(ValueError, match="no set 'new_set'"): scen2.read_excel(fname) # Succeeds with init_items=True scen2.read_excel(fname, init_items=True, commit_steps=True) exp = scen1.par("input") obs = scen2.par("input") pdt.assert_frame_equal(exp, obs) assert scen2.has_par("new_par") assert float(scen2.par("new_par")["value"]) == 2 scen2.solve() assert np.isclose(scen2.var("OBJ")["lvl"], scen1.var("OBJ")["lvl"])
def test_rename_technology(test_mp): scen = Scenario(test_mp, *msg_args) assert scen.par('output')['technology'].isin(['canning_plant']).any() clone = scen.clone('foo', 'bar') clone.rename('technology', {'canning_plant': 'foo_bar'}) assert not clone.par('output')['technology'].isin(['canning_plant']).any() assert clone.par('output')['technology'].isin(['foo_bar']).any() clone.solve() assert np.isclose(clone.var('OBJ')['lvl'], 153.675)
def test_rename_technology_no_rm(test_mp): scen = Scenario(test_mp, *msg_args) assert scen.par('output')['technology'].isin(['canning_plant']).any() clone = scen.clone('foo', 'bar') # also test if already checked out clone.check_out() clone.rename('technology', {'canning_plant': 'foo_bar'}, keep=True) assert clone.par('output')['technology'].isin(['canning_plant']).any() assert clone.par('output')['technology'].isin(['foo_bar']).any()
def change_tech_costs(scenario: message_ix.Scenario, techs: Union[str, List[str]], costs: Costs) -> message_ix.Scenario: if isinstance(techs, str): techs = [techs] inv = scenario.par('inv_cost') if not inv.empty: inv.loc[inv['technology'].isin(techs), 'value'] = costs.inv_cost scenario.add_par('inv_cost', inv) fix = scenario.par('fix_cost') if not fix.empty: fix.loc[fix['technology'].isin(techs), 'value'] = costs.fix_cost scenario.add_par('fix_cost', fix) var = scenario.par('var_cost') if not var.empty: var.loc[var['technology'].isin(techs), 'value'] = costs.var_cost scenario.add_par('var_cost', var) return scenario
def check_solution(scen: Scenario) -> None: # Reading "ACT" greater than zero from the solution act = scen.var("ACT")[scen.var("ACT")["lvl"] > 0] # 1) Testing if linkage between "gas_ppl" with "gas_supply" technology is made assert "gas_supply" in set(act["technology"]) # 2) Testing if "ACT" of "gas_ppl" is correct (with respect to "duration_time_rel") # i.e., sum("ACT" of "gas_ppl") = sum("ACT" of "gas_supply") = sum("demand") assert (sum(act.loc[act["technology"] == "gas_ppl"]["lvl"]) == sum( act.loc[act["technology"] == "gas_supply"]["lvl"]) == sum( scen.par("demand")["value"])) # 3) Test if "CAP" of "gas_ppl" is correctly calculated based on "ACT" # i.e., "CAP" = max("ACT" / "duration_time") for h in act.loc[act["technology"] == "gas_ppl"]["time"]: act.loc[(act["time"] == h) & (act["technology"] == "gas_ppl"), "capacity-corrected", ] = act["lvl"] / float( scen.par("duration_time", {"time": h})["value"]) assert max(act.loc[act["technology"] == "gas_ppl", "capacity-corrected"]) == float( scen.var("CAP", {"technology": "gas_ppl"})["lvl"])
def test_add_horizon_repeat(test_mp, caplog): """add_horizon() does not handle scenarios with existing years.""" scen = Scenario(test_mp, **SCENARIO["dantzig"], version="new") # Create a base scenario scen.add_horizon([2010, 2020, 2030]) npt.assert_array_equal([10, 10, 10], scen.par("duration_period")["value"]) with pytest.raises( ValueError, match=r"Scenario has year=\[2010, 2020, 2030\] and related values", ): scen.add_horizon([2015, 2020, 2025], firstmodelyear=2010)
def test_rename_technology(test_mp): scen = Scenario(test_mp, *msg_args) scen.solve() assert scen.par('output')['technology'].isin(['canning_plant']).any() exp_obj = scen.var('OBJ')['lvl'] clone = scen.clone('foo', 'bar', keep_solution=False) clone.rename('technology', {'canning_plant': 'foo_bar'}) assert not clone.par('output')['technology'].isin(['canning_plant']).any() assert clone.par('output')['technology'].isin(['foo_bar']).any() clone.solve() obs_obj = clone.var('OBJ')['lvl'] assert obs_obj == exp_obj
def test_add_horizon(test_mp, args, kwargs, exp): scen = Scenario(test_mp, **SCENARIO['dantzig'], version='new') # Call completes successfully if isinstance(args[0], dict): with pytest.warns( DeprecationWarning, match=(r"dict\(\) argument to add_horizon\(\); use year= and " "firstmodelyear=")): scen.add_horizon(*args, **kwargs) else: scen.add_horizon(*args, **kwargs) # Sets and parameters have the expected contents npt.assert_array_equal(exp["year"], scen.set("year")) npt.assert_array_equal(exp["fmy"], scen.cat("year", "firstmodelyear")) npt.assert_array_equal(exp["dp"], scen.par("duration_period")["value"])
def test_addon_tec(test_mp): scen = Scenario(test_mp, *msg_args).clone(scenario='addon', keep_solution=False) add_addon(scen, costs=-1) scen.check_out() bda = scen.par('bound_activity_up', f) bda['value'] = bda['value'] / 2 scen.add_par('bound_activity_up', bda) scen.commit('changing output and bounds') scen.solve() exp = scen.var('ACT', f)['lvl'] obs = scen.var('ACT', g)['lvl'] assert np.isclose(exp, obs)
def test_addon_tec(message_test_mp): scen = Scenario(message_test_mp, **SCENARIO["dantzig"]).clone(scenario="addon", keep_solution=False) add_addon(scen, costs=-1) scen.check_out() bda = scen.par("bound_activity_up", f) bda["value"] = bda["value"] / 2 scen.add_par("bound_activity_up", bda) scen.commit("changing output and bounds") scen.solve() exp = scen.var("ACT", f)["lvl"] obs = scen.var("ACT", g)["lvl"] assert np.isclose(exp, obs)
def test_multi_db_run(tmpdir): # create a new instance of the transport problem and solve it mp1 = Platform(driver="hsqldb", path=tmpdir / "mp1") scen1 = make_dantzig(mp1, solve=True) mp2 = Platform(driver="hsqldb", path=tmpdir / "mp2") # add other unit to make sure that the mapping is correct during clone mp2.add_unit("wrong_unit") mp2.add_region("wrong_region", "country") # check that cloning across platforms must copy the full solution dest = dict(platform=mp2) pytest.raises(NotImplementedError, scen1.clone, keep_solution=False, **dest) pytest.raises(NotImplementedError, scen1.clone, shift_first_model_year=1964, **dest) # clone solved model across platforms (with default settings) scen1.clone(platform=mp2, keep_solution=True) # close the db to ensure that data and solution of the clone are saved mp2.close_db() del mp2 # reopen the connection to the second platform and reload scenario _mp2 = Platform(driver="hsqldb", path=tmpdir / "mp2") scen2 = Scenario(_mp2, **SCENARIO["dantzig"]) assert_multi_db(mp1, _mp2) # check that sets, variables and parameter were copied correctly npt.assert_array_equal(scen1.set("node"), scen2.set("node")) scen2.firstmodelyear == 1963 assert_frame_equal(scen1.par("var_cost"), scen2.par("var_cost")) assert np.isclose(scen2.var("OBJ")["lvl"], 153.675) assert_frame_equal(scen1.var("ACT"), scen2.var("ACT")) # check that custom unit, region and timeseries are migrated correctly assert_frame_equal(scen2.timeseries(iamc=True), TS_DF)
def test_multi_db_run(tmpdir): # create a new instance of the transport problem and solve it mp1 = Platform(tmpdir / 'mp1', dbtype='HSQLDB') scen1 = make_dantzig(mp1, solve=True) mp2 = Platform(tmpdir / 'mp2', dbtype='HSQLDB') # add other unit to make sure that the mapping is correct during clone mp2.add_unit('wrong_unit') mp2.add_region('wrong_region', 'country') # check that cloning across platforms must copy the full solution dest = dict(platform=mp2) pytest.raises(ValueError, scen1.clone, keep_solution=False, **dest) pytest.raises(ValueError, scen1.clone, shift_first_model_year=1964, **dest) # clone solved model across platforms (with default settings) scen1.clone(platform=mp2, keep_solution=True) # close the db to ensure that data and solution of the clone are saved mp2.close_db() del mp2 # reopen the connection to the second platform and reload scenario _mp2 = Platform(tmpdir / 'mp2', dbtype='HSQLDB') scen2 = Scenario(_mp2, **models['dantzig']) assert_multi_db(mp1, _mp2) # check that sets, variables and parameter were copied correctly npt.assert_array_equal(scen1.set('node'), scen2.set('node')) scen2.firstmodelyear == 1963 pdt.assert_frame_equal(scen1.par('var_cost'), scen2.par('var_cost')) assert np.isclose(scen2.var('OBJ')['lvl'], 153.675) pdt.assert_frame_equal(scen1.var('ACT'), scen2.var('ACT')) # check that custom unit, region and timeseries are migrated correctly pdt.assert_frame_equal(scen2.timeseries(iamc=True), TS_DF)
def storage_setup(test_mp, time_duration, comment): # First building a simple model and adding seasonality scen = Scenario(test_mp, 'no_storage', 'standard', version='new') model_setup(scen, [2020]) add_seasonality(scen, time_duration) fixed_share = {'a': 1, 'b': 1, 'c': 1, 'd': 1} year_to_time(scen, 'output', fixed_share) year_to_time(scen, 'var_cost', fixed_share) demand_share = {'a': 0.15, 'b': 0.2, 'c': 0.4, 'd': 0.25} year_to_time(scen, 'demand', demand_share) scen.commit('initialized test model') scen.solve(case='no_storage' + comment) # Second adding bound on the activity of the cheap technology scen.remove_solution() scen.check_out() for h in time_duration.keys(): scen.add_par('bound_activity_up', ['node', 'tec1', 2020, 'mode', h], 0.25, 'GWa') scen.commit('activity bounded') scen.solve(case='no_storage_bounded' + comment) cost_no_storage = scen.var('OBJ')['lvl'] act_no = scen.var('ACT', {'technology': 'tec2'})['lvl'].sum() # Third, adding storage technologies and their parameters scen.remove_solution() scen.check_out() time_order = {'a': 1, 'b': 2, 'c': 3, 'd': 4} add_storage_data(scen, time_order) scen.commit('storage added') scen.solve(case='with_storage' + comment) cost_with_storage = scen.var('OBJ')['lvl'] act_with = scen.var('ACT', {'technology': 'tec2'})['lvl'].sum() # I. Tests for functionality of storage # I.1. Contribution of storage to the system assert cost_with_storage < cost_no_storage # Or, activity of expensive technology should be lower with storage assert act_with < act_no # I.2. Activity of discharger should be always <= activity of charger act_pump = scen.var('ACT', {'technology': 'pump'})['lvl'] act_turb = scen.var('ACT', {'technology': 'turbine'})['lvl'] assert act_turb.sum() <= act_pump.sum() # I.3. Max activity of charger is lower than storage capacity max_pump = max(act_pump) cap_storage = float(scen.var('CAP', {'technology': 'dam'})['lvl']) assert max_pump <= cap_storage # I.4. Max activity of discharger is lower than storage capacity - losses max_turb = max(act_turb) loss = scen.par('storage_loss')['value'][0] assert max_turb <= cap_storage * (1 - loss) # II. Testing equations of storage (when added to ixmp variables) if scen.has_var('STORAGE'): # II.1. Equality: storage content in the beginning and end is equal storage_first = scen.var('STORAGE', {'time': 'a'})['lvl'] storage_last = scen.var('STORAGE', {'time': 'd'})['lvl'] assert storage_first == storage_last # II.2. Storage content should never exceed storage capacity assert max(scen.var('STORAGE')['lvl']) <= cap_storage # II.3. Commodity balance: charge - discharge - losses = 0 change = scen.var('STORAGE_CHG').set_index(['year_act', 'time'])['lvl'] loss = scen.par('storage_loss').set_index(['year', 'time'])['value'] assert sum(change[change > 0] * (1 - loss)) == -sum(change[change < 0]) # II.4. Energy balance: storage change + losses = storage content storage = scen.var('STORAGE').set_index(['year', 'time'])['lvl'] assert storage[(2020, 'b')] * (1 - loss[(2020, 'b')]) == -change[(2020, 'c')]
def model_generator( test_mp, comment, tec_time, demand_time, time_steps, com_dict, yr=2020, ): """ Generates a simple model with a few technologies, and a flexible number of time slices. Parameters ---------- comment : string Annotation for saving different scenarios and comparing their results. tec_time : dict A dictionary for mapping a technology to its input/output temporal levels. demand_time : dict A dictionary for mapping the total "demand" specified at a temporal level. time_steps : list of tuples Information about each time slice, packed in a tuple with three elements, including: "temporal_lvl", number of time slices, and the parent time slice. com_dict : dict A dictionary for specifying "input" and "output" commodities. yr : int, optional Model year. The default is 2020. """ # Building an empty scenario scen = Scenario(test_mp, "test_duration_time", comment, version="new") # Adding required sets scen.add_set("node", "fairyland") for c in com_dict.values(): scen.add_set("commodity", [x for x in list(c.values()) if x]) scen.add_set("level", "final") scen.add_set("year", yr) scen.add_set("type_year", yr) scen.add_set("technology", list(tec_time.keys())) scen.add_set("mode", "standard") # Adding "time" related info to the model: "lvl_temporal", "time", # "map_temporal_hierarchy", and "duration_time" map_time = {} for [tmp_lvl, number, parent] in time_steps: scen.add_set("lvl_temporal", tmp_lvl) if parent == "year": times = [tmp_lvl[0] + "-" + str(x + 1) for x in range(number)] else: times = [ p + "_" + tmp_lvl[0] + "-" + str(x + 1) for (p, x) in product(map_time[parent], range(number)) ] map_time[tmp_lvl] = times scen.add_set("time", times) # Adding "map_temporal_hierarchy" and "duration_time" for h in times: if parent == "year": p = "year" else: p = h.split("_" + tmp_lvl[0])[0] # Temporal hierarchy (order: temporal level, time, parent time) scen.add_set("map_temporal_hierarchy", [tmp_lvl, h, p]) # Duration time is relative to the duration of the parent temporal level dur_parent = float(scen.par("duration_time", {"time": p})["value"]) scen.add_par("duration_time", [h], dur_parent / number, "-") # Adding "demand" at a temporal level (total demand divided by the number of # time slices in that temporal level) for tmp_lvl, value in demand_time.items(): times = scen.set("map_temporal_hierarchy", {"lvl_temporal": tmp_lvl})["time"] for h in times: scen.add_par( "demand", ["fairyland", "electr", "final", yr, h], value / len(times), "GWa", ) # Adding "input" and "output" parameters of technologies for tec, [tmp_lvl_in, tmp_lvl_out] in tec_time.items(): times_in = scen.set("map_temporal_hierarchy", {"lvl_temporal": tmp_lvl_in})[ "time" ] times_out = scen.set("map_temporal_hierarchy", {"lvl_temporal": tmp_lvl_out})[ "time" ] # If technology is linking two different temporal levels if tmp_lvl_in != tmp_lvl_out: time_pairs = product(times_in, times_out) else: time_pairs = zip(times_in, times_out) # Configuring data for "time_origin" and "time" in "input" for (h_in, h_act) in time_pairs: # "input" inp = com_dict[tec]["input"] if inp: inp_spec = [yr, yr, "standard", "fairyland", inp, "final", h_act, h_in] scen.add_par("input", ["fairyland", tec] + inp_spec, 1, "-") # "output" for h in times_out: out = com_dict[tec]["output"] out_spec = [yr, yr, "standard", "fairyland", out, "final", h, h] scen.add_par("output", ["fairyland", tec] + out_spec, 1, "-") # Committing scen.commit("scenario was set up.") # Testing if the model solves in GAMS scen.solve(case=comment) # Testing if sum of "duration_time" is almost 1 for tmp_lvl in scen.set("lvl_temporal"): times = scen.set("map_temporal_hierarchy", {"lvl_temporal": tmp_lvl})[ "time" ].to_list() assert ( abs(sum(scen.par("duration_time", {"time": times})["value"]) - 1.0) < 1e-12 )
def storage_setup(test_mp, time_duration, comment): # First, building a simple model and adding seasonality scen = Scenario(test_mp, "no_storage", "standard", version="new") model_setup(scen, [2020]) add_seasonality(scen, time_duration) # Fixed share for parameters that don't change across timesteps fixed_share = {"a": 1, "b": 1, "c": 1, "d": 1} year_to_time(scen, "output", fixed_share) year_to_time(scen, "var_cost", fixed_share) # Variable share for parameters that are changing in each timestep # share of demand in each season from annual demand demand_share = {"a": 0.15, "b": 0.2, "c": 0.4, "d": 0.25} year_to_time(scen, "demand", demand_share) scen.commit("initialized a model with timesteps") scen.solve(case="no_storage" + comment) # Second, adding upper bound on activity of the cheap technology (wind_ppl) scen.remove_solution() scen.check_out() for h in time_duration.keys(): scen.add_par( "bound_activity_up", ["node", "wind_ppl", 2020, "mode", h], 0.25, "GWa" ) scen.commit("activity bounded") scen.solve(case="no_storage_bounded" + comment) cost_no_stor = scen.var("OBJ")["lvl"] act_no_stor = scen.var("ACT", {"technology": "gas_ppl"})["lvl"].sum() # Third, adding storage technologies but with no input to storage device scen.remove_solution() scen.check_out() # Chronological order of timesteps in the year time_order = {"a": 1, "b": 2, "c": 3, "d": 4} add_storage_data(scen, time_order) scen.commit("storage data added") scen.solve(case="with_storage_no_input" + comment) act = scen.var("ACT") # Forth, adding storage technologies and providing input to storage device scen.remove_solution() scen.check_out() # Adding a new technology "cooler" to provide input of "cooling" to dam scen.add_set("technology", "cooler") df = scen.par("output", {"technology": "turbine"}) df["technology"] = "cooler" df["commodity"] = "cooling" scen.add_par("output", df) # Changing input of dam from 1 to 1.2 to test commodity balance df = scen.par("input", {"technology": "dam"}) df["value"] = 1.2 scen.add_par("input", df) scen.commit("storage needs no separate input") scen.solve(case="with_storage_and_input" + comment) cost_with_stor = scen.var("OBJ")["lvl"] act_with_stor = scen.var("ACT", {"technology": "gas_ppl"})["lvl"].sum() # Fifth. Tests for the functionality of storage # 1. Check that "dam" is not active if no "input" commodity is defined assert "dam" not in act[act["lvl"] > 0]["technology"].tolist() # 2. Testing functionality of storage # Check the contribution of storage to the system cost assert cost_with_stor < cost_no_stor # Activity of expensive technology should be lower with storage assert act_with_stor < act_no_stor # 3. Activity of discharger <= activity of charger + initial content act_pump = scen.var("ACT", {"technology": "pump"})["lvl"] act_turb = scen.var("ACT", {"technology": "turbine"})["lvl"] initial_content = float(scen.par("storage_initial")["value"]) assert act_turb.sum() <= act_pump.sum() + initial_content # 4. Activity of input provider to storage = act of storage * storage input for ts in time_duration.keys(): act_cooler = scen.var("ACT", {"technology": "cooler", "time": ts})["lvl"] inp = scen.par("input", {"technology": "dam", "time": ts})["value"] act_stor = scen.var("ACT", {"technology": "dam", "time": ts})["lvl"] assert float(act_cooler) == float(inp) * float(act_stor) # 5. Max activity of charger <= max activity of storage max_pump = max(act_pump) act_storage = scen.var("ACT", {"technology": "dam"})["lvl"] max_stor = max(act_storage) assert max_pump <= max_stor # 6. Max activity of discharger <= max storage act - self discharge losses max_turb = max(act_turb) loss = scen.par("storage_self_discharge")["value"][0] assert max_turb <= max_stor * (1 - loss) # Sixth, testing equations of storage (when added to ixmp variables) if scen.has_var("STORAGE"): # 1. Equality: storage content in the beginning and end is related storage_first = scen.var("STORAGE", {"time": "a"})["lvl"] storage_last = scen.var("STORAGE", {"time": "d"})["lvl"] relation = scen.par("relation_storage", {"time_first": "d", "time_last": "a"})[ "value" ][0] assert storage_last >= storage_first * relation # 2. Storage content should never exceed storage activity assert max(scen.var("STORAGE")["lvl"]) <= max_stor # 3. Commodity balance: charge - discharge - losses = 0 change = scen.var("STORAGE_CHARGE").set_index(["year_act", "time"])["lvl"] loss = scen.par("storage_self_discharge").set_index(["year", "time"])["value"] assert sum(change[change > 0] * (1 - loss)) == -sum(change[change < 0]) # 4. Energy balance: storage change + losses = storage content storage = scen.var("STORAGE").set_index(["year", "time"])["lvl"] assert storage[(2020, "b")] * (1 - loss[(2020, "b")]) == -change[(2020, "c")]
def storage_setup(test_mp, time_duration, comment): # First, building a simple model and adding seasonality scen = Scenario(test_mp, 'no_storage', 'standard', version='new') model_setup(scen, [2020]) add_seasonality(scen, time_duration) # Fixed share for parameters that don't change across timesteps fixed_share = {'a': 1, 'b': 1, 'c': 1, 'd': 1} year_to_time(scen, 'output', fixed_share) year_to_time(scen, 'var_cost', fixed_share) # Variable share for parameters that are changing in each timestep # share of demand in each season from annual demand demand_share = {'a': 0.15, 'b': 0.2, 'c': 0.4, 'd': 0.25} year_to_time(scen, 'demand', demand_share) scen.commit('initialized a model with timesteps') scen.solve(case='no_storage' + comment) # Second, adding upper bound on activity of the cheap technology (wind_ppl) scen.remove_solution() scen.check_out() for h in time_duration.keys(): scen.add_par('bound_activity_up', ['node', 'wind_ppl', 2020, 'mode', h], 0.25, 'GWa') scen.commit('activity bounded') scen.solve(case='no_storage_bounded' + comment) cost_no_stor = scen.var('OBJ')['lvl'] act_no_stor = scen.var('ACT', {'technology': 'gas_ppl'})['lvl'].sum() # Third, adding storage technologies but with no input to storage device scen.remove_solution() scen.check_out() # Chronological order of timesteps in the year time_order = {'a': 1, 'b': 2, 'c': 3, 'd': 4} add_storage_data(scen, time_order) scen.commit('storage data added') scen.solve(case='with_storage_no_input' + comment) act = scen.var('ACT') # Forth, adding storage technologies and providing input to storage device scen.remove_solution() scen.check_out() # Adding a new technology "cooler" to provide input of "cooling" to dam scen.add_set('technology', 'cooler') df = scen.par('output', {'technology': 'turbine'}) df['technology'] = 'cooler' df['commodity'] = 'cooling' scen.add_par('output', df) # Changing input of dam from 1 to 1.2 to test commodity balance df = scen.par('input', {'technology': 'dam'}) df['value'] = 1.2 scen.add_par('input', df) scen.commit('storage needs no separate input') scen.solve(case='with_storage_and_input' + comment) cost_with_stor = scen.var('OBJ')['lvl'] act_with_stor = scen.var('ACT', {'technology': 'gas_ppl'})['lvl'].sum() # Fifth. Tests for the functionality of storage # 1. Check that "dam" is not active if no "input" commodity is defined assert 'dam' not in act[act['lvl'] > 0]['technology'].tolist() # 2. Testing functionality of storage # Check the contribution of storage to the system cost assert cost_with_stor < cost_no_stor # Activity of expensive technology should be lower with storage assert act_with_stor < act_no_stor # 3. Activity of discharger <= activity of charger + initial content act_pump = scen.var('ACT', {'technology': 'pump'})['lvl'] act_turb = scen.var('ACT', {'technology': 'turbine'})['lvl'] initial_content = float(scen.par('storage_initial')['value']) assert act_turb.sum() <= act_pump.sum() + initial_content # 4. Activity of input provider to storage = act of storage * storage input for ts in time_duration.keys(): act_cooler = scen.var('ACT', { 'technology': 'cooler', 'time': ts })['lvl'] inp = scen.par('input', {'technology': 'dam', 'time': ts})['value'] act_stor = scen.var('ACT', {'technology': 'dam', 'time': ts})['lvl'] assert float(act_cooler) == float(inp) * float(act_stor) # 5. Max activity of charger <= max activity of storage max_pump = max(act_pump) act_storage = scen.var('ACT', {'technology': 'dam'})['lvl'] max_stor = max(act_storage) assert max_pump <= max_stor # 6. Max activity of discharger <= max storage act - self discharge losses max_turb = max(act_turb) loss = scen.par('storage_self_discharge')['value'][0] assert max_turb <= max_stor * (1 - loss) # Sixth, testing equations of storage (when added to ixmp variables) if scen.has_var('STORAGE'): # 1. Equality: storage content in the beginning and end is related storage_first = scen.var('STORAGE', {'time': 'a'})['lvl'] storage_last = scen.var('STORAGE', {'time': 'd'})['lvl'] relation = scen.par('relation_storage', { 'time_first': 'd', 'time_last': 'a' })['value'][0] assert storage_last >= storage_first * relation # 2. Storage content should never exceed storage activity assert max(scen.var('STORAGE')['lvl']) <= max_stor # 3. Commodity balance: charge - discharge - losses = 0 change = scen.var('STORAGE_CHARGE').set_index(['year_act', 'time'])['lvl'] loss = scen.par('storage_self_discharge').set_index(['year', 'time'])['value'] assert sum(change[change > 0] * (1 - loss)) == -sum(change[change < 0]) # 4. Energy balance: storage change + losses = storage content storage = scen.var('STORAGE').set_index(['year', 'time'])['lvl'] assert storage[(2020, 'b')] * (1 - loss[(2020, 'b')]) == -change[(2020, 'c')]