Beispiel #1
0
def test_years_active(test_mp):
    test_mp.add_unit('year')
    scen = Scenario(test_mp, *msg_args, version='new')
    scen.add_set('node', 'foo')
    scen.add_set('technology', 'bar')

    # Periods of uneven length
    years = [1990, 1995, 2000, 2005, 2010, 2020, 2030]

    # First period length is immaterial
    duration = [1900, 5, 5, 5, 5, 10, 10]
    scen.add_horizon({'year': years, 'firstmodelyear': years[-1]})
    scen.add_par('duration_period',
                 pd.DataFrame(zip(years, duration), columns=['year', 'value']))

    # 'bar' built in period '1995' with 25-year lifetime:
    # - is constructed in 1991-01-01.
    # - by 1995-12-31, has operated 5 years.
    # - operates until 2015-12-31. This is within the period '2020'.
    scen.add_par('technical_lifetime', pd.DataFrame(dict(
        node_loc='foo',
        technology='bar',
        unit='year',
        value=25,
        year_vtg=years[1]), index=[0]))

    result = scen.years_active('foo', 'bar', years[1])

    # Correct return type
    assert isinstance(years, list)
    assert isinstance(years[0], int)

    # Years 1995 through 2020
    npt.assert_array_equal(result, years[1:-1])
Beispiel #2
0
def test_years_active_extend(test_mp):
    scen = Scenario(test_mp, *msg_multiyear_args)
    scen = scen.clone(keep_solution=False)
    scen.check_out()
    scen.add_set('year', ['2040', '2050'])
    scen.add_par('duration_period', '2040', 10, 'y')
    scen.add_par('duration_period', '2050', 10, 'y')
    df = scen.years_active('seattle', 'canning_plant', '2020')
    npt.assert_array_equal(df, [2020, 2030, 2040])
    scen.discard_changes()
def test_commodity_price_equality(test_mp):
    scen = Scenario(test_mp, "test_commodity_price", "equality", version="new")
    model_setup(scen, var_cost=-1)
    scen.commit("initialize test model with negative variable costs")

    # negative variable costs and supply >= demand causes an unbounded ray
    pytest.raises(CalledProcessError, scen.solve)

    # use the commodity-balance equality feature
    scen.check_out()
    scen.add_set("balance_equality", ["comm", "level"])
    scen.commit("set commodity-balance for `[comm, level]` as equality")
    scen.solve(case="price_commodity_equality")

    assert scen.var("OBJ")["lvl"] == -1
    assert scen.var("PRICE_COMMODITY")["lvl"][0] == -1
Beispiel #4
0
def test_commodity_price_equality(test_mp):
    scen = Scenario(test_mp, 'test_commodity_price', 'equality', version='new')
    model_setup(scen, var_cost=-1)
    scen.commit('initialize test model with negative variable costs')

    # negative variable costs and supply >= demand causes an unbounded ray
    pytest.raises(CalledProcessError, scen.solve)

    # use the commodity-balance equality feature
    scen.check_out()
    scen.add_set('balance_equality', ['comm', 'level'])
    scen.commit('set commodity-balance for `[comm, level]` as equality')
    scen.solve(case='price_commodity_equality')

    assert scen.var('OBJ')['lvl'] == -1
    assert scen.var('PRICE_COMMODITY')['lvl'][0] == -1
Beispiel #5
0
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'])
Beispiel #6
0
def test_years_active_extended2(test_mp):
    test_mp.add_unit("year")
    scen = Scenario(test_mp, **SCENARIO["dantzig"], version="new")
    scen.add_set("node", "foo")
    scen.add_set("technology", "bar")

    # Periods of uneven length
    years = [1990, 1995, 2000, 2005, 2010, 2020, 2030]

    # First period length is immaterial
    duration = [1900, 5, 5, 5, 5, 10, 10]
    scen.add_horizon(year=years, firstmodelyear=years[-1])
    scen.add_par(
        "duration_period", pd.DataFrame(zip(years, duration), columns=["year", "value"])
    )

    # 'bar' built in period '2020' with 10-year lifetime:
    # - is constructed in 2011-01-01.
    # - by 2020-12-31, has operated 10 years.
    # - operates until 2020-12-31. This is within the period '2020'.
    # The test ensures that the correct lifetime value is retrieved,
    # i.e. the lifetime for the vintage 2020.
    scen.add_par(
        "technical_lifetime",
        pd.DataFrame(
            dict(
                node_loc="foo",
                technology="bar",
                unit="year",
                value=[20, 20, 20, 20, 20, 10, 10],
                year_vtg=years,
            ),
        ),
    )

    result = scen.years_active("foo", "bar", years[-2])

    # Correct return type
    assert isinstance(result, list)
    assert isinstance(result[0], int)

    # Years 2020
    npt.assert_array_equal(result, years[-2])
Beispiel #7
0
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"])
Beispiel #8
0
def test_vintage_and_active_years_with_lifetime(test_mp):
    scen = Scenario(test_mp, *msg_args, version='new')
    years = ['2000', '2010', '2020']
    scen.add_horizon({'year': years,
                      'firstmodelyear': '2010'})
    scen.add_set('node', 'foo')
    scen.add_set('technology', 'bar')
    scen.add_par('duration_period', pd.DataFrame({
        'unit': '???',
        'value': 10,
        'year': years
    }))
    scen.add_par('technical_lifetime', pd.DataFrame({
        'node_loc': 'foo',
        'technology': 'bar',
        'unit': '???',
        'value': 20,
        'year_vtg': years,
    }))

    # part is before horizon
    obs = scen.vintage_and_active_years(ya_args=('foo', 'bar', '2000'))
    exp = pd.DataFrame({'year_vtg': (2000,),
                        'year_act': (2010,)})
    pdt.assert_frame_equal(exp, obs, check_like=True)  # ignore col order

    obs = scen.vintage_and_active_years(ya_args=('foo', 'bar', '2000'),
                                        in_horizon=False)
    exp = pd.DataFrame({'year_vtg': (2000, 2000),
                        'year_act': (2000, 2010)})
    pdt.assert_frame_equal(exp, obs, check_like=True)  # ignore col order

    # fully in horizon
    obs = scen.vintage_and_active_years(ya_args=('foo', 'bar', '2010'))
    exp = pd.DataFrame({'year_vtg': (2010, 2010),
                        'year_act': (2010, 2020)})
    pdt.assert_frame_equal(exp, obs, check_like=True)  # ignore col order

    # part after horizon
    obs = scen.vintage_and_active_years(ya_args=('foo', 'bar', '2020'))
    exp = pd.DataFrame({'year_vtg': (2020,),
                        'year_act': (2020,)})
    pdt.assert_frame_equal(exp, obs, check_like=True)  # ignore col order
Beispiel #9
0
def test_years_active(test_mp):
    test_mp.add_unit("year")
    scen = Scenario(test_mp, **SCENARIO["dantzig"], version="new")
    scen.add_set("node", "foo")
    scen.add_set("technology", "bar")

    # Periods of uneven length
    years = [1990, 1995, 2000, 2005, 2010, 2020, 2030]

    # First period length is immaterial
    duration = [1900, 5, 5, 5, 5, 10, 10]
    scen.add_horizon(year=years, firstmodelyear=years[-1])
    scen.add_par(
        "duration_period", pd.DataFrame(zip(years, duration), columns=["year", "value"])
    )

    # 'bar' built in period '1995' with 25-year lifetime:
    # - is constructed in 1991-01-01.
    # - by 1995-12-31, has operated 5 years.
    # - operates until 2015-12-31. This is within the period '2020'.
    scen.add_par(
        "technical_lifetime",
        pd.DataFrame(
            dict(
                node_loc="foo",
                technology="bar",
                unit="year",
                value=25,
                year_vtg=years[1],
            ),
            index=[0],
        ),
    )

    result = scen.years_active("foo", "bar", years[1])

    # Correct return type
    assert isinstance(result, list)
    assert isinstance(result[0], int)

    # Years 1995 through 2020
    npt.assert_array_equal(result, years[1:-1])
Beispiel #10
0
def test_years_active_extend3(test_mp):
    test_mp.add_unit("year")
    scen = Scenario(test_mp, **SCENARIO["dantzig"], version="new")
    scen.add_set("node", "foo")
    scen.add_set("technology", "bar")

    # Periods of uneven length
    years = [1990, 1995, 2000, 2005, 2010, 2020, 2030]

    scen.add_horizon(year=years, firstmodelyear=2010)

    scen.add_set("year", [1992])
    scen.add_par("duration_period", "1992", 2, "y")
    scen.add_par("duration_period", "1995", 3, "y")

    scen.add_par(
        "technical_lifetime",
        pd.DataFrame(
            dict(
                node_loc="foo",
                technology="bar",
                unit="year",
                value=[20],
                year_vtg=1990,
            ),
        ),
    )

    obs = scen.years_active("foo", "bar", 1990)

    assert obs == [1990, 1992, 1995, 2000, 2005]
Beispiel #11
0
def test_years_active_extend(test_mp):
    scen = Scenario(test_mp, *msg_multiyear_args)

    # Existing time horizon
    years = [2010, 2020, 2030]
    result = scen.years_active('seattle', 'canning_plant', years[1])
    npt.assert_array_equal(result, years[1:])

    # Add years to the scenario
    years.extend([2040, 2050])
    scen.check_out()
    scen.add_set('year', years[-2:])
    scen.add_par('duration_period', '2040', 10, 'y')
    scen.add_par('duration_period', '2050', 10, 'y')

    # technical_lifetime of seattle/canning_plant/2020 is 30 years.
    # - constructed in 2011-01-01.
    # - by 2020-12-31, has operated 10 years.
    # - operates until 2040-12-31.
    # - is NOT active within the period '2050' (2041-01-01 to 2050-12-31)
    result = scen.years_active('seattle', 'canning_plant', '2020')
    npt.assert_array_equal(result, years[1:-1])
Beispiel #12
0
def test_clone(tmpdir):
    # Two local platforms
    mp1 = ixmp.Platform(tmpdir / 'mp1', dbtype='HSQLDB')
    mp2 = ixmp.Platform(tmpdir / 'mp2', dbtype='HSQLDB')

    # A minimal scenario
    scen1 = Scenario(mp1, model='model', scenario='scenario', version='new')
    scen1.add_spatial_sets({'country': 'Austria'})
    scen1.add_set('technology', 'bar')
    scen1.add_horizon({'year': [2010, 2020]})
    scen1.commit('add minimal sets for testing')

    assert len(mp1.scenario_list(default=False)) == 1

    # Clone
    scen2 = scen1.clone(platform=mp2)

    # Return type of ixmp.Scenario.clone is message_ix.Scenario
    assert isinstance(scen2, Scenario)

    # Close and re-open both databases
    mp1.close_db()  # TODO this should be done automatically on del
    mp2.close_db()  # TODO this should be done automatically on del
    del mp1, mp2
    mp1 = ixmp.Platform(tmpdir / 'mp1', dbtype='HSQLDB')
    mp2 = ixmp.Platform(tmpdir / 'mp2', dbtype='HSQLDB')

    # Same scenarios present in each database
    assert all(
        mp1.scenario_list(default=False) == mp2.scenario_list(default=False))

    # Load both scenarios
    scen1 = Scenario(mp1, 'model', 'scenario')
    scen2 = Scenario(mp2, 'model', 'scenario')

    # Contents are identical
    assert all(scen1.set('node') == scen2.set('node'))
    assert all(scen1.set('year') == scen2.set('year'))
Beispiel #13
0
def test_clone(tmpdir):
    # Two local platforms
    mp1 = ixmp.Platform(driver="hsqldb", path=tmpdir / "mp1")
    mp2 = ixmp.Platform(driver="hsqldb", path=tmpdir / "mp2")

    # A minimal scenario
    scen1 = Scenario(mp1, model="model", scenario="scenario", version="new")
    scen1.add_spatial_sets({"country": "Austria"})
    scen1.add_set("technology", "bar")
    scen1.add_horizon(year=[2010, 2020])
    scen1.commit("add minimal sets for testing")

    assert len(mp1.scenario_list(default=False)) == 1

    # Clone
    scen2 = scen1.clone(platform=mp2)

    # Return type of ixmp.Scenario.clone is message_ix.Scenario
    assert isinstance(scen2, Scenario)

    # Close and re-open both databases
    mp1.close_db()  # TODO this should be done automatically on del
    mp2.close_db()  # TODO this should be done automatically on del
    del mp1, mp2
    mp1 = ixmp.Platform(driver="hsqldb", path=tmpdir / "mp1")
    mp2 = ixmp.Platform(driver="hsqldb", path=tmpdir / "mp2")

    # Same scenarios present in each database
    assert all(
        mp1.scenario_list(default=False) == mp2.scenario_list(default=False))

    # Load both scenarios
    scen1 = Scenario(mp1, "model", "scenario")
    scen2 = Scenario(mp2, "model", "scenario")

    # Contents are identical
    assert all(scen1.set("node") == scen2.set("node"))
    assert all(scen1.set("year") == scen2.set("year"))
Beispiel #14
0
def test_years_active_extend(message_test_mp):
    scen = Scenario(message_test_mp, **SCENARIO["dantzig multi-year"])

    # Existing time horizon
    years = [1963, 1964, 1965]
    result = scen.years_active("seattle", "canning_plant", years[1])
    npt.assert_array_equal(result, years[1:])

    # Add years to the scenario
    years.extend([1993, 1995])
    scen.check_out()
    scen.add_set("year", years[-2:])
    scen.add_par("duration_period", "1993", 28, "y")
    scen.add_par("duration_period", "1995", 2, "y")

    # technical_lifetime of seattle/canning_plant/1964 is 30 years.
    # - constructed in 1964-01-01.
    # - by 1964-12-31, has operated 1 year.
    # - by 1965-12-31, has operated 2 years.
    # - operates until 1993-12-31.
    # - is NOT active within the period '1995' (1994-01-01 to 1995-12-31)
    result = scen.years_active("seattle", "canning_plant", 1964)
    npt.assert_array_equal(result, years[1:-1])
Beispiel #15
0
def test_identify_nodes1(test_context):
    mp = test_context.get_platform()
    scenario = Scenario(mp,
                        model="identify_nodes",
                        scenario="identify_nodes",
                        version="new")
    scenario.add_set("technology", "t")
    scenario.add_set("year", 0)
    scenario.commit("")

    with scenario.transact():
        scenario.add_set("node", "R99_ZZZ")

    with pytest.raises(
            ValueError,
            match=re.escape(
                "Couldn't identify node codelist from ['R99_ZZZ', 'World']"),
    ):
        identify_nodes(scenario)
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")]
Beispiel #17
0
def base_scen_mp(test_mp):
    scen = Scenario(test_mp, 'model', 'standard', version='new')

    data = {2020: 1, 2030: 2, 2040: 3}

    years = sorted(list(set(data.keys())))
    scen.add_set('node', 'node')
    scen.add_set('commodity', 'comm')
    scen.add_set('level', 'level')
    scen.add_set('year', years)
    scen.add_set('technology', 'tec')
    scen.add_set('mode', 'mode')
    output_specs = ['node', 'comm', 'level', 'year', 'year']

    for (yr, value) in data.items():
        scen.add_par('demand', ['node', 'comm', 'level', yr, 'year'], 1, 'GWa')
        scen.add_par('technical_lifetime', ['node', 'tec', yr], 10, 'y')
        tec_specs = ['node', 'tec', yr, yr, 'mode']
        scen.add_par('output', tec_specs + output_specs, 1, '-')
        scen.add_par('var_cost', tec_specs + ['year'], value, 'USD/GWa')

    scen.commit('initialize test model')
    scen.solve(case='original_years')

    yield scen, test_mp
Beispiel #18
0
def base_scen_mp(test_mp):
    scen = Scenario(test_mp, "model", "standard", version="new")

    data = {2020: 1, 2030: 2, 2040: 3}

    years = sorted(list(set(data.keys())))
    scen.add_set("node", "node")
    scen.add_set("commodity", "comm")
    scen.add_set("level", "level")
    scen.add_set("year", years)
    scen.add_set("technology", "tec")
    scen.add_set("mode", "mode")
    output_specs = ["node", "comm", "level", "year", "year"]

    for (yr, value) in data.items():
        scen.add_par("demand", ["node", "comm", "level", yr, "year"], 1, "GWa")
        scen.add_par("technical_lifetime", ["node", "tec", yr], 10, "y")
        tec_specs = ["node", "tec", yr, yr, "mode"]
        scen.add_par("output", tec_specs + output_specs, 1, "-")
        scen.add_par("var_cost", tec_specs + ["year"], value, "USD/GWa")

    scen.commit("initialize test model")
    scen.solve(case="original_years", quiet=True)

    yield scen, test_mp
Beispiel #19
0
def test_vintage_and_active_years(test_mp):
    scen = Scenario(test_mp, **SCENARIO["dantzig"], version="new")

    years = [2000, 2010, 2020]
    scen.add_horizon(year=years, firstmodelyear=2010)
    obs = scen.vintage_and_active_years()
    exp = pd.DataFrame(
        {
            "year_vtg": (2000, 2000, 2010, 2010, 2020),
            "year_act": (2010, 2020, 2010, 2020, 2020),
        }
    )
    pdt.assert_frame_equal(exp, obs, check_like=True)  # ignore col order

    # Add a technology, its lifetime, and period durations
    scen.add_set("node", "foo")
    scen.add_set("technology", "bar")
    scen.add_par(
        "duration_period", pd.DataFrame({"unit": "???", "value": 10, "year": years})
    )
    scen.add_par(
        "technical_lifetime",
        pd.DataFrame(
            {
                "node_loc": "foo",
                "technology": "bar",
                "unit": "???",
                "value": 20,
                "year_vtg": years,
            }
        ),
    )

    # part is before horizon
    obs = scen.vintage_and_active_years(ya_args=("foo", "bar", "2000"))
    exp = pd.DataFrame({"year_vtg": (2000,), "year_act": (2010,)})
    pdt.assert_frame_equal(exp, obs, check_like=True)  # ignore col order

    obs = scen.vintage_and_active_years(
        ya_args=("foo", "bar", "2000"), in_horizon=False
    )
    exp = pd.DataFrame({"year_vtg": (2000, 2000), "year_act": (2000, 2010)})
    pdt.assert_frame_equal(exp, obs, check_like=True)  # ignore col order

    # fully in horizon
    obs = scen.vintage_and_active_years(ya_args=("foo", "bar", "2010"))
    exp = pd.DataFrame({"year_vtg": (2010, 2010), "year_act": (2010, 2020)})
    pdt.assert_frame_equal(exp, obs, check_like=True)  # ignore col order

    # part after horizon
    obs = scen.vintage_and_active_years(ya_args=("foo", "bar", "2020"))
    exp = pd.DataFrame({"year_vtg": (2020,), "year_act": (2020,)})
    pdt.assert_frame_equal(exp, obs, check_like=True)  # ignore col order

    # Advance the first model year
    scen.add_cat("year", "firstmodelyear", years[-1], is_unique=True)

    # Empty data frame: only 2000 and 2010 valid year_act for this node/tec;
    # but both are before the first model year
    obs = scen.vintage_and_active_years(
        ya_args=("foo", "bar", years[0]), in_horizon=True
    )
    pdt.assert_frame_equal(pd.DataFrame(columns=["year_vtg", "year_act"]), obs)

    # Exception is raised for incorrect arguments
    with pytest.raises(ValueError, match="3 arguments are required if using `ya_args`"):
        scen.vintage_and_active_years(ya_args=("foo", "bar"))
Beispiel #20
0
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
        )
Beispiel #21
0
def make_austria(mp, solve=False, quiet=True):
    """Return an :class:`message_ix.Scenario` for the Austrian energy system.

    This is the same model used in the ``austria.ipynb`` tutorial.

    Parameters
    ----------
    mp : ixmp.Platform
        Platform on which to create the scenario.
    solve : bool, optional
        If True, the scenario is solved.
    """
    mp.add_unit("USD/kW")
    mp.add_unit("MtCO2")
    mp.add_unit("tCO2/kWa")

    scen = Scenario(
        mp,
        version="new",
        **SCENARIO["austria"],
        annotation=
        "A stylized energy system model for illustration and testing",
    )

    # Structure

    year = dict(all=list(range(2010, 2041, 10)))
    scen.add_horizon(year=year["all"])
    year_df = scen.vintage_and_active_years()
    year["vtg"] = year_df["year_vtg"]
    year["act"] = year_df["year_act"]

    country = "Austria"
    scen.add_spatial_sets({"country": country})

    sets = dict(
        commodity=["electricity", "light", "other_electricity"],
        emission=["CO2"],
        level=["secondary", "final", "useful"],
        mode=["standard"],
    )

    sets["technology"] = AUSTRIA_TECH.index.to_list()
    plants = sets["technology"][:7]
    lights = sets["technology"][10:]

    for name, values in sets.items():
        scen.add_set(name, values)

    scen.add_cat("emission", "GHGs", "CO2")

    # Parameters

    name = "interestrate"
    scen.add_par(name, make_df(name, year=year["all"], value=0.05, unit="-"))

    common = dict(
        mode="standard",
        node_dest=country,
        node_loc=country,
        node_origin=country,
        node=country,
        time_dest="year",
        time_origin="year",
        time="year",
        year_act=year["act"],
        year_vtg=year["vtg"],
        year=year["all"],
    )

    gdp_profile = np.array([1.0, 1.21631, 1.4108, 1.63746])
    beta = 0.7
    demand_profile = gdp_profile**beta

    # From IEA statistics, in GW·h, converted to GW·a
    base_annual_demand = dict(other_electricity=55209.0 / 8760,
                              light=6134.0 / 8760)

    name = "demand"
    common.update(level="useful", unit="GWa")
    for c, base in base_annual_demand.items():
        scen.add_par(
            name,
            make_df(name, **common, commodity=c, value=base * demand_profile))
    common.pop("level")

    # input, output
    common.update(unit="-")
    for name, (tec, info) in product(("input", "output"),
                                     AUSTRIA_TECH.iterrows()):
        value = info[f"{name}_value"]
        if np.isnan(value):
            continue
        scen.add_par(
            name,
            make_df(
                name,
                **common,
                technology=tec,
                commodity=info[f"{name}_commodity"],
                level=info[f"{name}_level"],
                value=value,
            ),
        )

    data = AUSTRIA_PAR
    # Convert GW·h to GW·a
    data["activity"] = data["activity"] / 8760.0
    # Convert USD / MW·h to USD / GW·a
    data["var_cost"] = data["var_cost"] * 8760.0 / 1e3
    # Convert t / MW·h to t / kw·a
    data["emission_factor"] = data["emission_factor"] * 8760.0 / 1e3

    def _add():
        """Add using values from the calling scope."""
        scen.add_par(name, make_df(name, **common, technology=tec,
                                   value=value))

    name = "capacity_factor"
    for tec, value in data[name].dropna().items():
        _add()

    name = "technical_lifetime"
    common.update(year_vtg=year["all"], unit="y")
    for tec, value in data[name].dropna().items():
        _add()

    name = "growth_activity_up"
    common.update(year_act=year["all"][1:], unit="%")
    value = 0.05
    for tec in plants + lights:
        _add()

    name = "initial_activity_up"
    common.update(year_act=year["all"][1:], unit="%")
    value = 0.01 * base_annual_demand["light"] * demand_profile[1:]
    for tec in lights:
        _add()

    # bound_activity_lo, bound_activity_up
    common.update(year_act=year["all"][0], unit="GWa")
    for (tec, value), kind in product(data["activity"].dropna().items(),
                                      ("up", "lo")):
        name = f"bound_activity_{kind}"
        _add()

    name = "bound_activity_up"
    common.update(year_act=year["all"][1:])
    for tec in ("bio_ppl", "hydro_ppl", "import"):
        value = data.loc[tec, "activity"]
        _add()

    name = "bound_new_capacity_up"
    common.update(year_vtg=year["all"][0], unit="GW")
    for tec, value in (data["activity"] /
                       data["capacity_factor"]).dropna().items():
        _add()

    name = "inv_cost"
    common.update(dict(year_vtg=year["all"], unit="USD/kW"))
    for tec, value in data[name].dropna().items():
        _add()

    # fix_cost, var_cost
    common.update(
        dict(year_vtg=year["vtg"], year_act=year["act"], unit="USD/kWa"))
    for name in ("fix_cost", "var_cost"):
        for tec, value in data[name].dropna().items():
            _add()

    name = "emission_factor"
    common.update(year_vtg=year["vtg"],
                  year_act=year["act"],
                  unit="tCO2/kWa",
                  emission="CO2")
    for tec, value in data[name].dropna().items():
        _add()

    scen.commit("Initial commit for Austria model")
    scen.set_as_default()

    if solve:
        scen.solve(quiet=quiet)

    return scen
Beispiel #22
0
def make_westeros(mp, emissions=False, solve=False, quiet=True):
    """Return an :class:`message_ix.Scenario` for the Westeros model.

    This is the same model used in the ``westeros_baseline.ipynb`` tutorial.

    Parameters
    ----------
    mp : ixmp.Platform
        Platform on which to create the scenario.
    emissions : bool, optional
        If True, the ``emissions_factor`` parameter is also populated for CO2.
    solve : bool, optional
        If True, the scenario is solved.
    """
    mp.add_unit("USD/kW")
    mp.add_unit("tCO2/kWa")
    scen = Scenario(mp, version="new", **SCENARIO["westeros"])

    # Sets
    history = [690]
    model_horizon = [700, 710, 720]
    scen.add_horizon(year=history + model_horizon,
                     firstmodelyear=model_horizon[0])
    year_df = scen.vintage_and_active_years()
    vintage_years, act_years = year_df["year_vtg"], year_df["year_act"]

    country = "Westeros"
    scen.add_spatial_sets({"country": country})

    for name, values in (
        ("technology", ["coal_ppl", "wind_ppl", "grid", "bulb"]),
        ("mode", ["standard"]),
        ("level", ["secondary", "final", "useful"]),
        ("commodity", ["electricity", "light"]),
    ):
        scen.add_set(name, values)

    # Parameters — copy & paste from the tutorial notebook

    common = dict(
        mode="standard",
        node_dest=country,
        node_loc=country,
        node_origin=country,
        node=country,
        time_dest="year",
        time_origin="year",
        time="year",
        year_act=act_years,
        year_vtg=vintage_years,
        year=model_horizon,
    )

    gdp_profile = np.array([1.0, 1.5, 1.9])
    demand_per_year = 40 * 12 * 1000 / 8760
    scen.add_par(
        "demand",
        make_df(
            "demand",
            **common,
            commodity="light",
            level="useful",
            # FIXME should use demand_per_year; requires adjustments elsewhere.
            value=(100 * gdp_profile).round(),
            unit="GWa",
        ),
    )

    grid_efficiency = 0.9
    common.update(unit="-")

    for name, tec, c, l, value in [
        ("input", "bulb", "electricity", "final", 1.0),
        ("output", "bulb", "light", "useful", 1.0),
        ("input", "grid", "electricity", "secondary", 1.0),
        ("output", "grid", "electricity", "final", grid_efficiency),
        ("output", "coal_ppl", "electricity", "secondary", 1.0),
        ("output", "wind_ppl", "electricity", "secondary", 1.0),
    ]:
        scen.add_par(
            name,
            make_df(name,
                    **common,
                    technology=tec,
                    commodity=c,
                    level=l,
                    value=value),
        )

    # FIXME the value for wind_ppl should be 0.36; requires adjusting other tests.
    name = "capacity_factor"
    capacity_factor = dict(coal_ppl=1.0, wind_ppl=1.0, bulb=1.0)
    for tec, value in capacity_factor.items():
        scen.add_par(name, make_df(name, **common, technology=tec,
                                   value=value))

    name = "technical_lifetime"
    common.update(year_vtg=model_horizon, unit="y")
    for tec, value in dict(coal_ppl=20, wind_ppl=20, bulb=1).items():
        scen.add_par(name, make_df(name, **common, technology=tec,
                                   value=value))

    name = "growth_activity_up"
    common.update(year_act=model_horizon, unit="-")
    for tec in "coal_ppl", "wind_ppl":
        scen.add_par(name, make_df(name, **common, technology=tec, value=0.1))

    historic_demand = 0.85 * demand_per_year
    historic_generation = historic_demand / grid_efficiency
    coal_fraction = 0.6

    common.update(year_act=history, year_vtg=history, unit="GWa")
    for tec, value in (
        ("coal_ppl", coal_fraction * historic_generation),
        ("wind_ppl", (1 - coal_fraction) * historic_generation),
    ):
        name = "historical_activity"
        scen.add_par(name, make_df(name, **common, technology=tec,
                                   value=value))
        # 20 year lifetime
        name = "historical_new_capacity"
        scen.add_par(
            name,
            make_df(
                name,
                **common,
                technology=tec,
                value=value / (2 * 10 * capacity_factor[tec]),
            ),
        )

    name = "interestrate"
    scen.add_par(name, make_df(name, year=model_horizon, value=0.05, unit="-"))

    for name, tec, value in [
        ("inv_cost", "coal_ppl", 500),
        ("inv_cost", "wind_ppl", 1500),
        ("inv_cost", "bulb", 5),
        ("fix_cost", "coal_ppl", 30),
        ("fix_cost", "wind_ppl", 10),
        ("var_cost", "coal_ppl", 30),
        ("var_cost", "grid", 50),
    ]:
        common.update(
            dict(year_vtg=model_horizon, unit="USD/kW") if name ==
            "inv_cost" else dict(
                year_vtg=vintage_years, year_act=act_years, unit="USD/kWa"))
        scen.add_par(name, make_df(name, **common, technology=tec,
                                   value=value))

    scen.commit("basic model of Westerosi electrification")
    scen.set_as_default()

    if emissions:
        scen.check_out()

        # Introduce the emission species CO2 and the emission category GHG
        scen.add_set("emission", "CO2")
        scen.add_cat("emission", "GHG", "CO2")

        # we now add CO2 emissions to the coal powerplant
        name = "emission_factor"
        common.update(year_vtg=vintage_years,
                      year_act=act_years,
                      unit="tCO2/kWa")
        scen.add_par(
            name,
            make_df(name,
                    **common,
                    technology="coal_ppl",
                    emission="CO2",
                    value=100.0),
        )

        scen.commit("Added emissions sets/params to Westeros model.")

    if solve:
        scen.solve(quiet=quiet)

    return scen
Beispiel #23
0
def make_dantzig(mp, solve=False, multi_year=False, **solve_opts):
    """Return an :class:`message_ix.Scenario` for Dantzig's canning problem.

    Parameters
    ----------
    mp : ixmp.Platform
        Platform on which to create the scenario.
    solve : bool, optional
        If True, the scenario is solved.
    multi_year : bool, optional
        If True, the scenario has years 1963--1965 inclusive. Otherwise, the
        scenario has the single year 1963.
    """
    # add custom units and region for timeseries data
    mp.add_unit("USD/case")
    mp.add_unit("case")
    mp.add_region("DantzigLand", "country")

    # initialize a new (empty) instance of an `ixmp.Scenario`
    scen = Scenario(
        mp,
        model=SCENARIO["dantzig"]["model"],
        scenario="multi-year" if multi_year else "standard",
        annotation="Dantzig's canning problem as a MESSAGE-scheme Scenario",
        version="new",
    )

    # Sets
    # NB commit() is refused if technology and year are not given
    t = ["canning_plant", "transport_from_seattle", "transport_from_san-diego"]
    sets = {
        "technology": t,
        "node": "seattle san-diego new-york chicago topeka".split(),
        "mode": "production to_new-york to_chicago to_topeka".split(),
        "level": "supply consumption".split(),
        "commodity": ["cases"],
    }

    for name, values in sets.items():
        scen.add_set(name, values)

    scen.add_horizon(year=[1962, 1963], firstmodelyear=1963)

    # Parameters
    par = {}

    # Common values
    common = dict(
        commodity="cases",
        year=1963,
        year_vtg=1963,
        year_act=1963,
        time="year",
        time_dest="year",
        time_origin="year",
    )

    par["demand"] = make_df(
        "demand",
        **common,
        node=["new-york", "chicago", "topeka"],
        level="consumption",
        value=[325, 300, 275],
        unit="case",
    )
    par["bound_activity_up"] = make_df(
        "bound_activity_up",
        **common,
        node_loc=["seattle", "san-diego"],
        mode="production",
        technology="canning_plant",
        value=[350, 600],
        unit="case",
    )
    par["ref_activity"] = par["bound_activity_up"].copy()

    input = pd.DataFrame(
        [
            ["to_new-york", "seattle", "seattle", t[1]],
            ["to_chicago", "seattle", "seattle", t[1]],
            ["to_topeka", "seattle", "seattle", t[1]],
            ["to_new-york", "san-diego", "san-diego", t[2]],
            ["to_chicago", "san-diego", "san-diego", t[2]],
            ["to_topeka", "san-diego", "san-diego", t[2]],
        ],
        columns=["mode", "node_loc", "node_origin", "technology"],
    )
    par["input"] = make_df(
        "input",
        **input,
        **common,
        level="supply",
        value=1,
        unit="case",
    )

    output = pd.DataFrame(
        [
            ["supply", "production", "seattle", "seattle", t[0]],
            ["supply", "production", "san-diego", "san-diego", t[0]],
            ["consumption", "to_new-york", "new-york", "seattle", t[1]],
            ["consumption", "to_chicago", "chicago", "seattle", t[1]],
            ["consumption", "to_topeka", "topeka", "seattle", t[1]],
            ["consumption", "to_new-york", "new-york", "san-diego", t[2]],
            ["consumption", "to_chicago", "chicago", "san-diego", t[2]],
            ["consumption", "to_topeka", "topeka", "san-diego", t[2]],
        ],
        columns=["level", "mode", "node_dest", "node_loc", "technology"],
    )
    par["output"] = make_df("output", **output, **common, value=1, unit="case")

    # Variable cost: cost per kilometre × distance (neither parametrized
    # explicitly)
    var_cost = pd.DataFrame(
        [
            ["to_new-york", "seattle", "transport_from_seattle", 0.225],
            ["to_chicago", "seattle", "transport_from_seattle", 0.153],
            ["to_topeka", "seattle", "transport_from_seattle", 0.162],
            ["to_new-york", "san-diego", "transport_from_san-diego", 0.225],
            ["to_chicago", "san-diego", "transport_from_san-diego", 0.162],
            ["to_topeka", "san-diego", "transport_from_san-diego", 0.126],
        ],
        columns=["mode", "node_loc", "technology", "value"],
    )
    par["var_cost"] = make_df("var_cost",
                              **var_cost,
                              **common,
                              unit="USD/case")

    for name, value in par.items():
        scen.add_par(name, value)

    if multi_year:
        scen.add_set("year", [1964, 1965])
        scen.add_par("technical_lifetime", ["seattle", "canning_plant", 1964],
                     3, "y")

    if solve:
        # Always read one equation. Used by test_core.test_year_int.
        scen.init_equ("COMMODITY_BALANCE_GT",
                      ["node", "commodity", "level", "year", "time"])
        solve_opts["equ_list"] = solve_opts.get("equ_list",
                                                []) + ["COMMODITY_BALANCE_GT"]

    scen.commit("Created a MESSAGE-scheme version of the transport problem.")
    scen.set_as_default()

    if solve:
        solve_opts.setdefault("quiet", True)
        scen.solve(**solve_opts)

    scen.check_out(timeseries_only=True)
    scen.add_timeseries(HIST_DF, meta=True)
    scen.add_timeseries(INP_DF)
    scen.commit("Import Dantzig's transport problem for testing.")

    return scen
Beispiel #24
0
def test_vintage_and_active_years(test_mp):
    scen = Scenario(test_mp, *msg_args, version='new')

    years = [2000, 2010, 2020]
    scen.add_horizon({'year': years, 'firstmodelyear': 2010})
    obs = scen.vintage_and_active_years()
    exp = pd.DataFrame({'year_vtg': (2000, 2000, 2010, 2010, 2020),
                        'year_act': (2010, 2020, 2010, 2020, 2020)})
    pdt.assert_frame_equal(exp, obs, check_like=True)  # ignore col order

    # Add a technology, its lifetime, and period durations
    scen.add_set('node', 'foo')
    scen.add_set('technology', 'bar')
    scen.add_par('duration_period', pd.DataFrame({
        'unit': '???',
        'value': 10,
        'year': years
    }))
    scen.add_par('technical_lifetime', pd.DataFrame({
        'node_loc': 'foo',
        'technology': 'bar',
        'unit': '???',
        'value': 20,
        'year_vtg': years,
    }))

    # part is before horizon
    obs = scen.vintage_and_active_years(ya_args=('foo', 'bar', '2000'))
    exp = pd.DataFrame({'year_vtg': (2000,),
                        'year_act': (2010,)})
    pdt.assert_frame_equal(exp, obs, check_like=True)  # ignore col order

    obs = scen.vintage_and_active_years(ya_args=('foo', 'bar', '2000'),
                                        in_horizon=False)
    exp = pd.DataFrame({'year_vtg': (2000, 2000),
                        'year_act': (2000, 2010)})
    pdt.assert_frame_equal(exp, obs, check_like=True)  # ignore col order

    # fully in horizon
    obs = scen.vintage_and_active_years(ya_args=('foo', 'bar', '2010'))
    exp = pd.DataFrame({'year_vtg': (2010, 2010),
                        'year_act': (2010, 2020)})
    pdt.assert_frame_equal(exp, obs, check_like=True)  # ignore col order

    # part after horizon
    obs = scen.vintage_and_active_years(ya_args=('foo', 'bar', '2020'))
    exp = pd.DataFrame({'year_vtg': (2020,),
                        'year_act': (2020,)})
    pdt.assert_frame_equal(exp, obs, check_like=True)  # ignore col order

    # Advance the first model year
    scen.add_cat('year', 'firstmodelyear', years[-1], is_unique=True)

    # Empty data frame: only 2000 and 2010 valid year_act for this node/tec;
    # but both are before the first model year
    obs = scen.vintage_and_active_years(ya_args=('foo', 'bar', years[0]),
                                        in_horizon=True)
    pdt.assert_frame_equal(
        pd.DataFrame(columns=['year_vtg', 'year_act']),
        obs)

    # Exception is raised for incorrect arguments
    with pytest.raises(ValueError,
                       match='3 arguments are required if using `ya_args`'):
        scen.vintage_and_active_years(ya_args=('foo', 'bar'))
Beispiel #25
0
def make_westeros(mp, emissions=False, solve=False):
    """Return an :class:`message_ix.Scenario` for the Westeros model.

    This is the same model used in the ``westeros_baseline.ipynb`` tutorial.

    Parameters
    ----------
    mp : ixmp.Platform
        Platform on which to create the scenario.
    emissions : bool, optional
        If True, the ``emissions_factor`` parameter is also populated for CO2.
    solve : bool, optional
        If True, the scenario is solved.
    """
    scen = Scenario(mp, version='new', **SCENARIO['westeros'])

    # Sets

    history = [690]
    model_horizon = [700, 710, 720]
    scen.add_horizon({
        'year': history + model_horizon,
        'firstmodelyear': model_horizon[0]
    })

    country = 'Westeros'
    scen.add_spatial_sets({'country': country})

    sets = {
        'technology': 'coal_ppl wind_ppl grid bulb'.split(),
        'mode': ['standard'],
        'level': 'secondary final useful'.split(),
        'commodity': 'electricity light'.split(),
    }

    for name, values in sets.items():
        scen.add_set(name, values)

    # Parameters — copy & paste from the tutorial notebook

    gdp_profile = pd.Series([1., 1.5, 1.9],
                            index=pd.Index(model_horizon, name='Time'))
    demand_per_year = 40 * 12 * 1000 / 8760
    light_demand = pd.DataFrame({
        'node': country,
        'commodity': 'light',
        'level': 'useful',
        'year': model_horizon,
        'time': 'year',
        'value': (100 * gdp_profile).round(),
        'unit': 'GWa',
    })
    scen.add_par("demand", light_demand)

    year_df = scen.vintage_and_active_years()
    vintage_years, act_years = year_df['year_vtg'], year_df['year_act']

    base = {
        'node_loc': country,
        'year_vtg': vintage_years,
        'year_act': act_years,
        'mode': 'standard',
        'time': 'year',
        'unit': '-',
    }

    base_input = make_df(base, node_origin=country, time_origin='year')
    base_output = make_df(base, node_dest=country, time_dest='year')

    bulb_out = make_df(base_output,
                       technology='bulb',
                       commodity='light',
                       level='useful',
                       value=1.0)
    scen.add_par('output', bulb_out)

    bulb_in = make_df(base_input,
                      technology='bulb',
                      commodity='electricity',
                      level='final',
                      value=1.0)
    scen.add_par('input', bulb_in)

    grid_efficiency = 0.9
    grid_out = make_df(base_output,
                       technology='grid',
                       commodity='electricity',
                       level='final',
                       value=grid_efficiency)
    scen.add_par('output', grid_out)

    grid_in = make_df(base_input,
                      technology='grid',
                      commodity='electricity',
                      level='secondary',
                      value=1.0)
    scen.add_par('input', grid_in)

    coal_out = make_df(base_output,
                       technology='coal_ppl',
                       commodity='electricity',
                       level='secondary',
                       value=1.)
    scen.add_par('output', coal_out)

    wind_out = make_df(base_output,
                       technology='wind_ppl',
                       commodity='electricity',
                       level='secondary',
                       value=1.)
    scen.add_par('output', wind_out)

    base_capacity_factor = {
        'node_loc': country,
        'year_vtg': vintage_years,
        'year_act': act_years,
        'time': 'year',
        'unit': '-',
    }

    capacity_factor = {
        'coal_ppl': 1,
        'wind_ppl': 1,
        'bulb': 1,
    }

    for tec, val in capacity_factor.items():
        df = make_df(base_capacity_factor, technology=tec, value=val)
        scen.add_par('capacity_factor', df)

    base_technical_lifetime = {
        'node_loc': country,
        'year_vtg': model_horizon,
        'unit': 'y',
    }

    lifetime = {
        'coal_ppl': 20,
        'wind_ppl': 20,
        'bulb': 1,
    }

    for tec, val in lifetime.items():
        df = make_df(base_technical_lifetime, technology=tec, value=val)
        scen.add_par('technical_lifetime', df)

    base_growth = {
        'node_loc': country,
        'year_act': model_horizon,
        'time': 'year',
        'unit': '-',
    }

    growth_technologies = [
        "coal_ppl",
        "wind_ppl",
    ]

    for tec in growth_technologies:
        df = make_df(base_growth, technology=tec, value=0.1)
        scen.add_par('growth_activity_up', df)

    historic_demand = 0.85 * demand_per_year
    historic_generation = historic_demand / grid_efficiency
    coal_fraction = 0.6

    base_capacity = {
        'node_loc': country,
        'year_vtg': history,
        'unit': 'GWa',
    }

    base_activity = {
        'node_loc': country,
        'year_act': history,
        'mode': 'standard',
        'time': 'year',
        'unit': 'GWa',
    }

    old_activity = {
        'coal_ppl': coal_fraction * historic_generation,
        'wind_ppl': (1 - coal_fraction) * historic_generation,
    }

    for tec, val in old_activity.items():
        df = make_df(base_activity, technology=tec, value=val)
        scen.add_par('historical_activity', df)

    act_to_cap = {
        # 20 year lifetime
        'coal_ppl': 1 / 10 / capacity_factor['coal_ppl'] / 2,
        'wind_ppl': 1 / 10 / capacity_factor['wind_ppl'] / 2,
    }

    for tec in act_to_cap:
        value = old_activity[tec] * act_to_cap[tec]
        df = make_df(base_capacity, technology=tec, value=value)
        scen.add_par('historical_new_capacity', df)

    rate = [0.05] * len(model_horizon)
    unit = ['-'] * len(model_horizon)
    scen.add_par("interestrate", model_horizon, rate, unit)

    base_inv_cost = {
        'node_loc': country,
        'year_vtg': model_horizon,
        'unit': 'USD/GWa',
    }

    # in $ / kW
    costs = {
        'coal_ppl': 500,
        'wind_ppl': 1500,
        'bulb': 5,
    }

    for tec, val in costs.items():
        df = make_df(base_inv_cost, technology=tec, value=val)
        scen.add_par('inv_cost', df)

    base_fix_cost = {
        'node_loc': country,
        'year_vtg': vintage_years,
        'year_act': act_years,
        'unit': 'USD/GWa',
    }

    # in $ / kW
    costs = {
        'coal_ppl': 30,
        'wind_ppl': 10,
    }

    for tec, val in costs.items():
        df = make_df(base_fix_cost, technology=tec, value=val)
        scen.add_par('fix_cost', df)

    base_var_cost = {
        'node_loc': country,
        'year_vtg': vintage_years,
        'year_act': act_years,
        'mode': 'standard',
        'time': 'year',
        'unit': 'USD/GWa',
    }

    # in $ / MWh
    costs = {
        'coal_ppl': 30,
        'grid': 50,
    }

    for tec, val in costs.items():
        df = make_df(base_var_cost, technology=tec, value=val)
        scen.add_par('var_cost', df)

    scen.commit('basic model of Westerosi electrification')
    scen.set_as_default()

    if emissions:
        scen.check_out()

        # Introduce the emission species CO2 and the emission category GHG
        scen.add_set('emission', 'CO2')
        scen.add_cat('emission', 'GHG', 'CO2')

        # we now add CO2 emissions to the coal powerplant
        base_emission_factor = {
            'node_loc': country,
            'year_vtg': vintage_years,
            'year_act': act_years,
            'mode': 'standard',
            'unit': 'USD/GWa',
        }

        emission_factor = make_df(base_emission_factor,
                                  technology='coal_ppl',
                                  emission='CO2',
                                  value=100.)
        scen.add_par('emission_factor', emission_factor)

        scen.commit('Added emissions sets/params to Westeros model.')

    if solve:
        scen.solve()

    return scen
Beispiel #26
0
def make_subannual(
    request,
    tec_dict,
    time_steps,
    demand,
    time_relative=[],
    com_dict={"gas_ppl": {
        "input": "fuel",
        "output": "electr"
    }},
    capacity={"gas_ppl": {
        "inv_cost": 0.1,
        "technical_lifetime": 5
    }},
    capacity_factor={},
    var_cost={},
):
    """Return an :class:`message_ix.Scenario` with subannual time resolution.

    The scenario contains a simple model with two technologies, and a number of time
    slices.

    Parameters
    ----------
    request :
        The pytest ``request`` fixture.
    tec_dict : dict
        A dictionary for a technology and required info for time-related parameters.
        (e.g., ``tec_dict = {"gas_ppl": {"time_origin": ["summer"], "time": ["summer"],
        "time_dest": ["summer"]}``)
    time_steps : list of tuples
        Information about each time slice, packed in a tuple with four elements,
        including: time slice name, duration relative to "year", "temporal_lvl",
        and parent time slice (e.g., ``time_steps = [("summer", 1, "season", "year")]``)
    demand : dict
        A dictionary for information of "demand" in each time slice.
        (e.g., 11demand = {"summer": 2.5}``)
    time_relative: list of str, optional
        List of parent "time" slices, for which a relative duration time is maintained.
        This will be used to specify parameter "duration_time_rel" for these "time"s.
    com_dict : dict, optional
        A dictionary for specifying "input" and "output" commodities.
        (e.g., ``com_dict = {"gas_ppl": {"input": "fuel", "output": "electr"}}``)
    capacity : dict, optional
        Data for "inv_cost" and "technical_lifetime" per technology.
    capacity_factor : dict, optional
        "capacity_factor" with technology as key and "time"/"value" pairs as value.
    var_cost : dict, optional
        "var_cost" with technology as key and "time"/"value" pairs as value.
    """
    # Get the `test_mp` fixture for the requesting test function
    mp = request.getfixturevalue("test_mp")

    # Build an empty scenario
    scen = Scenario(mp, request.node.name, scenario="test", version="new")

    # Add required sets
    scen.add_set("node", "node")
    for c in com_dict.values():
        scen.add_set("commodity", [x for x in list(c.values()) if x])

    # Fixed values
    y = 2020
    unit = "GWa"

    scen.add_set("level", "level")
    scen.add_set("year", y)
    scen.add_set("type_year", y)
    scen.add_set("mode", "mode")

    scen.add_set("technology", list(tec_dict.keys()))

    # Add "time" and "duration_time" to the model
    for (h, dur, tmp_lvl, parent) in time_steps:
        scen.add_set("time", h)
        scen.add_set("time", parent)
        scen.add_set("lvl_temporal", tmp_lvl)
        scen.add_set("map_temporal_hierarchy", [tmp_lvl, h, parent])
        scen.add_par("duration_time", [h], dur, "-")

    scen.add_set("time_relative", time_relative)

    # Common dimensions for parameter data
    common = dict(
        node="node",
        node_loc="node",
        mode="mode",
        level="level",
        year=y,
        year_vtg=y,
        year_act=y,
    )

    # Define demand; unpack (key, value) pairs into individual pd.DataFrame rows
    df = make_df(
        "demand",
        **common,
        commodity="electr",
        time=demand.keys(),
        value=demand.values(),
        unit=unit,
    )
    scen.add_par("demand", df)

    # Add "input" and "output" parameters of technologies
    common.update(value=1.0, unit="-")
    base_output = make_df("output", **common, node_dest="node")
    base_input = make_df("input", **common, node_origin="node")
    for tec, times in tec_dict.items():
        c = com_dict[tec]
        for h1, h2 in zip(times["time"], times.get("time_dest", [])):
            scen.add_par(
                "output",
                base_output.assign(technology=tec,
                                   commodity=c["output"],
                                   time=h1,
                                   time_dest=h2),
            )
        for h1, h2 in zip(times["time"], times.get("time_origin", [])):
            scen.add_par(
                "input",
                base_input.assign(technology=tec,
                                  commodity=c["input"],
                                  time=h1,
                                  time_origin=h2),
            )

    # Add capacity related parameters
    for year, tec in product([y], capacity.keys()):
        for parname, val in capacity[tec].items():
            scen.add_par(parname, ["node", tec, year], val, "-")

    common.pop("value")

    # Add capacity factor and variable cost data, both optional
    for name, arg in [("capacity_factor", capacity_factor),
                      ("var_cost", var_cost)]:
        for tec, data in arg.items():
            df = make_df(name,
                         **common,
                         technology=tec,
                         time=data.keys(),
                         value=data.values())
            scen.add_par(name, df)

    scen.commit(
        f"Scenario with subannual time resolution for {request.node.name}")
    scen.solve()

    return scen
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')]
Beispiel #28
0
def make_dantzig(mp, solve=False, multi_year=False, **solve_opts):
    """Return an :class:`message_ix.Scenario` for Dantzig's canning problem.

    Parameters
    ----------
    mp : ixmp.Platform
        Platform on which to create the scenario.
    solve : bool, optional
        If True, the scenario is solved.
    multi_year : bool, optional
        If True, the scenario has years 1963--1965 inclusive. Otherwise, the
        scenario has the single year 1963.
    """
    # add custom units and region for timeseries data
    mp.add_unit('USD/case')
    mp.add_unit('case')
    mp.add_region('DantzigLand', 'country')

    # initialize a new (empty) instance of an `ixmp.Scenario`
    scen = Scenario(
        mp,
        model=SCENARIO['dantzig']['model'],
        scenario='multi-year' if multi_year else 'standard',
        annotation="Dantzig's canning problem as a MESSAGE-scheme Scenario",
        version='new')

    # Sets
    # NB commit() is refused if technology and year are not given
    t = ['canning_plant', 'transport_from_seattle', 'transport_from_san-diego']
    sets = {
        'technology': t,
        'node': 'seattle san-diego new-york chicago topeka'.split(),
        'mode': 'production to_new-york to_chicago to_topeka'.split(),
        'level': 'supply consumption'.split(),
        'commodity': ['cases'],
    }

    for name, values in sets.items():
        scen.add_set(name, values)

    scen.add_horizon({'year': [1962, 1963], 'firstmodelyear': 1963})

    # Parameters
    par = {}

    demand = {
        'node': 'new-york chicago topeka'.split(),
        'value': [325, 300, 275]
    }
    par['demand'] = make_df(pd.DataFrame.from_dict(demand),
                            commodity='cases',
                            level='consumption',
                            time='year',
                            unit='case',
                            year=1963)

    b_a_u = {'node_loc': ['seattle', 'san-diego'], 'value': [350, 600]}
    par['bound_activity_up'] = make_df(pd.DataFrame.from_dict(b_a_u),
                                       mode='production',
                                       technology='canning_plant',
                                       time='year',
                                       unit='case',
                                       year_act=1963)
    par['ref_activity'] = par['bound_activity_up'].copy()

    input = pd.DataFrame(
        [
            ['to_new-york', 'seattle', 'seattle', t[1]],
            ['to_chicago', 'seattle', 'seattle', t[1]],
            ['to_topeka', 'seattle', 'seattle', t[1]],
            ['to_new-york', 'san-diego', 'san-diego', t[2]],
            ['to_chicago', 'san-diego', 'san-diego', t[2]],
            ['to_topeka', 'san-diego', 'san-diego', t[2]],
        ],
        columns=['mode', 'node_loc', 'node_origin', 'technology'])
    par['input'] = make_df(input,
                           commodity='cases',
                           level='supply',
                           time='year',
                           time_origin='year',
                           unit='case',
                           value=1,
                           year_act=1963,
                           year_vtg=1963)

    output = pd.DataFrame(
        [
            ['supply', 'production', 'seattle', 'seattle', t[0]],
            ['supply', 'production', 'san-diego', 'san-diego', t[0]],
            ['consumption', 'to_new-york', 'new-york', 'seattle', t[1]],
            ['consumption', 'to_chicago', 'chicago', 'seattle', t[1]],
            ['consumption', 'to_topeka', 'topeka', 'seattle', t[1]],
            ['consumption', 'to_new-york', 'new-york', 'san-diego', t[2]],
            ['consumption', 'to_chicago', 'chicago', 'san-diego', t[2]],
            ['consumption', 'to_topeka', 'topeka', 'san-diego', t[2]],
        ],
        columns=['level', 'mode', 'node_dest', 'node_loc', 'technology'])
    par['output'] = make_df(output,
                            commodity='cases',
                            time='year',
                            time_dest='year',
                            unit='case',
                            value=1,
                            year_act=1963,
                            year_vtg=1963)

    # Variable cost: cost per kilometre × distance (neither parametrized
    # explicitly)
    var_cost = pd.DataFrame(
        [
            ['to_new-york', 'seattle', 'transport_from_seattle', 0.225],
            ['to_chicago', 'seattle', 'transport_from_seattle', 0.153],
            ['to_topeka', 'seattle', 'transport_from_seattle', 0.162],
            ['to_new-york', 'san-diego', 'transport_from_san-diego', 0.225],
            ['to_chicago', 'san-diego', 'transport_from_san-diego', 0.162],
            ['to_topeka', 'san-diego', 'transport_from_san-diego', 0.126],
        ],
        columns=['mode', 'node_loc', 'technology', 'value'])
    par['var_cost'] = make_df(var_cost,
                              time='year',
                              unit='USD/case',
                              year_act=1963,
                              year_vtg=1963)

    for name, value in par.items():
        scen.add_par(name, value)

    if multi_year:
        scen.add_set('year', [1964, 1965])
        scen.add_par('technical_lifetime', ['seattle', 'canning_plant', 1964],
                     3, 'y')

    if solve:
        # Always read one equation. Used by test_core.test_year_int.
        scen.init_equ('COMMODITY_BALANCE_GT',
                      ['node', 'commodity', 'level', 'year', 'time'])
        solve_opts['equ_list'] = solve_opts.get('equ_list', []) \
            + ['COMMODITY_BALANCE_GT']

    scen.commit('Created a MESSAGE-scheme version of the transport problem.')
    scen.set_as_default()

    if solve:
        scen.solve(**solve_opts)

    scen.check_out(timeseries_only=True)
    scen.add_timeseries(HIST_DF, meta=True)
    scen.add_timeseries(INP_DF)
    scen.commit("Import Dantzig's transport problem for testing.")

    return scen
Beispiel #29
0
def apply_spec(
    scenario: Scenario,
    spec: Mapping[str, ScenarioInfo],
    data: Callable = None,
    **options,
):
    """Apply `spec` to `scenario`.

    Parameters
    ----------
    spec
        A 'specification': :class:`dict` with 'require', 'remove', and 'add' keys and
        :class:`.ScenarioInfo` objects as values.
    data : callable, optional
        Function to add data to `scenario`. `data` can either manipulate the scenario
        directly, or return a :class:`dict` compatible with :func:`.add_par_data`.

    Other parameters
    ----------------
    dry_run : bool
        Don't modify `scenario`; only show what would be done. Default :obj:`False`.
        Exceptions will still be raised if the elements from ``spec['required']`` are
        missing; this serves as a check that the scenario has the required features for
        applying the spec.
    fast : bool
        Do not remove existing parameter data; increases speed on large scenarios.
    quiet : bool
        Only show log messages at level ``ERROR`` and higher. If :obj:`False` (default),
        show log messages at level ``DEBUG`` and higher.
    message : str
        Commit message.

    See also
    --------
    .add_par_data
    .strip_par_data
    .Code
    .ScenarioInfo
    """
    dry_run = options.get("dry_run", False)

    log.setLevel(logging.ERROR if options.get("quiet", False) else logging.DEBUG)

    if not dry_run:
        try:
            scenario.remove_solution()
        except ValueError:
            pass
        maybe_check_out(scenario)

    dump: Dict[str, pd.DataFrame] = {}  # Removed data

    for set_name in scenario.set_list():
        # Check whether this set is mentioned at all in the spec
        if 0 == sum(map(lambda info: len(info.set[set_name]), spec.values())):
            # Not mentioned; don't do anything
            continue

        log.info(f"Set {repr(set_name)}")

        # Base contents of the set
        base_set = scenario.set(set_name)
        # Unpack a multi-dimensional/indexed set to a list of tuples
        base = (
            list(base_set.itertuples(index=False))
            if isinstance(base_set, pd.DataFrame)
            else base_set.tolist()
        )

        log.info(f"  {len(base)} elements")
        # log.debug(', '.join(map(repr, base)))  # All elements; verbose

        # Check for required elements
        require = spec["require"].set[set_name]
        log.info(f"  Check {len(require)} required elements")

        # Raise an exception about the first missing element
        missing = list(filter(lambda e: e not in base, require))
        if len(missing):
            log.error(f"  {len(missing)} elements not found: {repr(missing)}")
            raise ValueError

        # Remove elements and associated parameter values
        remove = spec["remove"].set[set_name]
        for element in remove:
            msg = f"{repr(element)} and associated parameter elements"

            if options.get("fast", False):
                log.info(f"  Skip removing {msg} (fast=True)")
                continue

            log.info(f"  Remove {msg}")
            strip_par_data(scenario, set_name, element, dry_run=dry_run, dump=dump)

        # Add elements
        add = [] if dry_run else spec["add"].set[set_name]
        for element in add:
            scenario.add_set(
                set_name,
                element.id if isinstance(element, Code) else element,
            )

        if len(add):
            log.info(f"  Add {len(add)} element(s)")
            log.debug("  " + ellipsize(add))

        log.info("  ---")

    N_removed = sum(len(d) for d in dump.values())
    log.info(f"{N_removed} parameter elements removed")

    # Add units to the Platform before adding data
    for unit in spec["add"].set["unit"]:
        unit = unit if isinstance(unit, Code) else Code(id=unit, name=unit)
        log.info(f"Add unit {repr(unit)}")
        scenario.platform.add_unit(unit.id, comment=str(unit.name))

    # Add data
    if callable(data):
        result = data(scenario, dry_run=dry_run)
        if result:
            # `data` function returned some data; use add_par_data()
            add_par_data(scenario, result, dry_run=dry_run)

    # Finalize
    log.info("Commit results.")
    maybe_commit(
        scenario,
        condition=not dry_run,
        message=options.get("message", f"{__name__}.apply_spec()"),
    )