Exemplo n.º 1
0
def _get_shadowprice_data(scenario_id):
    """Gets data necessary for plotting shadow price

    :param str/int scenario_id: scenario id
    :return: (*tuple*) -- interconnect as a str, bus data as a data frame, lmp data
        as a data frame, branch data as a data frame and congestion data as a data
        frame
    """
    s = Scenario(scenario_id)

    interconnect = s.info["interconnect"]
    interconnect = " ".join(interconnect.split("_"))

    s_grid = s.state.get_grid()

    # Get bus and add location data
    bus_map = project_bus(s_grid.bus)

    # get branch and add location data
    branch_map = project_branch(s_grid.branch)

    # get congestion
    congu = s.state.get_congu()
    congl = s.state.get_congl()
    cong_abs = pd.DataFrame(
        np.maximum(congu.to_numpy(), congl.to_numpy()),
        columns=congu.columns,
        index=congu.index,
    )

    return interconnect, bus_map, s.state.get_lmp(), branch_map, cong_abs
Exemplo n.º 2
0
def _get_data_chart_from_scenario(scenario_id, time, zone_list):
    """Use :class:`postreise.plot.analyze_pg.AnalyzePG` to fetch and format data for a
    scenario

    :param str scenario_id: the id of the scenario to fetch.
    :param list zone_list: list of zone names
    :return: (*tuple*) -- scenario data chart and scenario name.

    .. todo::
        do this ourselves instead of using apg as a middle man
    """
    scenario = Scenario(scenario_id)
    scenario_name = scenario.info["name"]
    data_chart = apg(scenario,
                     time,
                     zone_list,
                     SCENARIO_RESOURCE_TYPES,
                     "chart",
                     normalize=False).get_data()
    plt.close("all")
    return data_chart, scenario_name
def plot_pie_generation_vs_capacity(
    areas,
    area_types=None,
    scenario_ids=None,
    scenario_names=None,
    time_range=None,
    time_zone=None,
    custom_data=None,
    resource_labels=None,
    resource_colors=None,
    min_percentage=0,
):
    """Plot any number of scenarios as pie charts with two columns per scenario -
    generation and capacity.

    :param list/str areas: list of area(s), each area is one of *loadzone*, *state*,
        *state abbreviation*, *interconnect*, *'all'*
    :param list/str area_types: list of area_type(s), each area_type is one of
        *'loadzone'*, *'state'*, *'state_abbr'*, *'interconnect'*, defaults to None.
    :param int/list/str scenario_ids: list of scenario id(s), defaults to None.
    :param list/str scenario_names: list of scenario name(s) of same len as scenario
        ids, defaults to None
    :param tuple time_range: [start_timestamp, end_timestamp] where each time stamp
        is pandas.Timestamp/numpy.datetime64/datetime.datetime. If None, the entire
        time range is used for the given scenario.
    :param str time_zone: new time zone, defaults to None, which uses UTC.
    :param list custom_data: list of dictionaries with each element being
        hand-generated data as returned by
        :func:`postreise.plot_bar_generation_vs_capacity.make_gen_cap_custom_data`,
        defaults to None.
    :param dict resource_labels: a dictionary with keys being resource types and values
        being labels, which is used to customize resource labels for selected resource
        types to show in the plots. Defaults to None, in which case a default set of
        labels is used.
    :param dict resource_colors: a dictionary with keys being resource types and values
        being colors, which is used to customize resource colors for selected resource
        types to show in the plots. Defaults to None, in which case a default set of
        colors is used.
    :param float min_percentage: roll up small pie pieces into a single category,
        resources with percentage less than the set value will be pooled together,
        defaults to 0.
    :raises ValueError:
        if length of area_types and areas is different and/or
        if length of scenario_names and scenario_ids is different and/or
        if less than two scenario_ids and/or custom_data in total is provided.
    :raises TypeError:
        if resource_labels are provided but not in a dictionary format and/or
        if resource_colors are provided but not in a dictionary format.

    .. note::
        if one wants to plot scenario data and custom data together, custom data MUST be
        in TWh for generation and GW for capacity in order to conduct appropriate
        comparison.
    """
    if isinstance(areas, str):
        areas = [areas]
    if isinstance(area_types, str):
        area_types = [area_types]
    if not area_types:
        area_types = [None] * len(areas)
    if len(areas) != len(area_types):
        raise ValueError(
            "ERROR: if area_types are provided, number of area_types must match number of areas"
        )

    if not scenario_ids:
        scenario_ids = []
    if isinstance(scenario_ids, (int, str)):
        scenario_ids = [scenario_ids]
    if isinstance(scenario_names, str):
        scenario_names = [scenario_names]
    if scenario_names and len(scenario_names) != len(scenario_ids):
        raise ValueError(
            "ERROR: if scenario names are provided, number of scenario names must match number of scenario ids"
        )
    if not custom_data:
        custom_data = {}
    if len(scenario_ids) + len(custom_data) <= 1:
        raise ValueError(
            "ERROR: must include at least two scenario ids and/or custom data")
    if not resource_labels:
        resource_labels = dict()
    if not isinstance(resource_labels, dict):
        raise TypeError("ERROR: resource_labels should be a dictionary")
    if not resource_colors:
        resource_colors = dict()
    if not isinstance(resource_colors, dict):
        raise TypeError("ERROR: resource_colors should be a dictionary")

    all_loadzone_data = {}
    scenario_data = {}
    for i, sid in enumerate(scenario_ids):
        scenario = Scenario(sid)
        mi = ModelImmutables(scenario.info["grid_model"])
        all_loadzone_data[sid] = {
            "gen":
            sum_generation_by_type_zone(
                scenario, time_range,
                time_zone).rename(columns=mi.zones["id2loadzone"]),
            "cap":
            sum_capacity_by_type_zone(scenario).rename(
                columns=mi.zones["id2loadzone"]),
        }
        scenario_data[sid] = {
            "name":
            scenario_names[i] if scenario_names else scenario.info["name"],
            "grid_model": mi.model,
            "type2color": {
                **mi.plants["type2color"],
                **resource_colors
            },
            "type2label": {
                **mi.plants["type2label"],
                **resource_labels
            },
            "gen": {
                "label": "Generation",
                "unit": "TWh",
                "data": {}
            },
            "cap": {
                "label": "Capacity",
                "unit": "GW",
                "data": {}
            },
        }
    for area, area_type in zip(areas, area_types):
        for sid in scenario_ids:
            loadzone_set = area_to_loadzone(scenario_data[sid]["grid_model"],
                                            area, area_type)
            scenario_data[sid]["gen"]["data"][area] = (
                all_loadzone_data[sid]["gen"][loadzone_set].sum(
                    axis=1).divide(1e6).astype("float").round(2).to_dict())
            scenario_data[sid]["cap"]["data"][area] = (
                all_loadzone_data[sid]["cap"][loadzone_set].sum(
                    axis=1).divide(1e3).astype("float").round(2).to_dict())
    for c_data in custom_data:
        scenario_data[c_data["name"]] = c_data

    for area in areas:
        ax_data_list = []
        for sd in scenario_data.values():
            for side in ["gen", "cap"]:
                ax_data, labels = _roll_up_small_pie_wedges(
                    sd[side]["data"][area], sd["type2label"], min_percentage)

                ax_data_list.append({
                    "title":
                    "{0}\n{1}".format(sd["name"], sd[side]["label"]),
                    "labels":
                    labels,
                    "values":
                    list(ax_data.values()),
                    "colors": [sd["type2color"][r] for r in ax_data.keys()],
                    "unit":
                    sd[side]["unit"],
                })

        _construct_pie_visuals(area, ax_data_list)
import pandas as pd
from powersimdata.scenario.scenario import Scenario

from postreise.plot.plot_powerflow_snapshot import plot_powerflow_snapshot

if __name__ == "__main__":
    demand_centers = pd.read_csv("loadzone_centers.csv", index_col=0)
    scenario = Scenario(1270)
    b2b_dclines = {"from": range(7), "to": [7, 8]}
    hour_of_interest = pd.Timestamp(2016, 11, 2, 22)
    # Plot the whole USA
    plot_powerflow_snapshot(
        scenario,
        hour_of_interest,
        b2b_dclines=b2b_dclines,
        demand_centers=demand_centers,
        legend_font_size=20,
    )
    # Plot just Texas
    plot_powerflow_snapshot(
        scenario,
        hour_of_interest,
        b2b_dclines=b2b_dclines,
        demand_centers=demand_centers,
        x_range=(-11.9e6, -10.2e6),
        y_range=(2.7e6, 4.4e6),
        figsize=(800, 800),
        pf_width_scale_factor=0.002,
        bg_width_scale_factor=0.00125,
        circle_scale_factor=(1 / 3),
        legend_font_size=20,
Exemplo n.º 5
0
def test_bad_scenario_name():
    # This test will fail if we do add a scenario with this name
    with pytest.raises(ValueError):
        Scenario("this_scenario_does_not_exist")
def test_get_demand_and_get_bus_demand():
    scenario = Scenario("")
    scenario.set_grid(interconnect="Texas")
    # Before we set the profile version, we should get errors trying to access
    with pytest.raises(Exception):
        scenario.get_bus_demand()
    with pytest.raises(Exception):
        scenario.get_demand()
    # After we set the profile version, we should get the right len (default full year)
    scenario.set_base_profile("demand", "vJan2021")
    assert len(scenario.get_bus_demand()) == 8784
    scenario.set_time("2016-01-01 00:00", "2016-01-03 23:00", "24H")
    demand = scenario.get_demand()
    bus_demand = scenario.get_bus_demand()
    assert len(demand) == 72
    assert len(bus_demand) == 72
    assert_series_equal(demand.sum(axis=1), bus_demand.sum(axis=1))
    unscaled_total_demand = demand.sum().sum()
    scenario.change_table.scale_demand(zone_id={301: 1.5})
    new_demand = scenario.get_demand()
    new_bus_demand = scenario.get_bus_demand()
    assert_series_equal(new_demand.sum(axis=1), new_bus_demand.sum(axis=1))
    assert new_demand.sum().sum() > unscaled_total_demand
def test_get_solar():
    scenario = Scenario("")
    scenario.set_grid(interconnect="Texas")
    with pytest.raises(Exception):
        scenario.get_solar()
    scenario.set_base_profile("solar", "vJan2021")
    assert len(scenario.get_solar()) == 8784
    scenario.set_time("2016-01-01 00:00", "2016-01-03 23:00", "24H")
    assert len(scenario.get_solar()) == 72
Exemplo n.º 8
0
def plot_bar_generation_vs_capacity(
    areas,
    area_types=None,
    scenario_ids=None,
    scenario_names=None,
    time_range=None,
    time_zone=None,
    custom_data=None,
    resource_types=None,
    resource_labels=None,
    horizontal=False,
):
    """Plot any number of scenarios as bar or horizontal bar charts with two columns per
    scenario - generation and capacity.

    :param list/str areas: list of area(s), each area is one of *loadzone*, *state*,
        *state abbreviation*, *interconnect*, *'all'*.
    :param list/str area_types: list of area_type(s), each area_type is one of
        *'loadzone'*, *'state'*, *'state_abbr'*, *'interconnect'*, defaults to None.
    :param int/list/str scenario_ids: list of scenario id(s), defaults to None.
    :param list/str scenario_names: list of scenario name(s) of same len as scenario
        ids, defaults to None.
    :param tuple time_range: [start_timestamp, end_timestamp] where each time stamp
        is pandas.Timestamp/numpy.datetime64/datetime.datetime. If None, the entire
        time range is used for the given scenario.
    :param str time_zone: new time zone, defaults to None, which uses UTC.
    :param list custom_data: list of dictionaries with each element being
        hand-generated data as returned by :func:`make_gen_cap_custom_data`, defaults
        to None.
    :param list/str resource_types: list of resource type(s) to show, defaults to None,
        which shows all available resources in the area of the corresponding scenario.
    :param dict resource_labels: a dictionary with keys being resource_types and values
        being labels to show in the plots, defaults to None, which uses
        resource_types as labels.
    :param bool horizontal: display bars horizontally, default to False.
    """
    if isinstance(areas, str):
        areas = [areas]
    if isinstance(area_types, str):
        area_types = [area_types]
    if not area_types:
        area_types = [None] * len(areas)
    if len(areas) != len(area_types):
        raise ValueError(
            "ERROR: if area_types are provided, it should have the same number of entries with areas."
        )

    if not scenario_ids:
        scenario_ids = []
    if isinstance(scenario_ids, (int, str)):
        scenario_ids = [scenario_ids]
    if isinstance(scenario_names, str):
        scenario_names = [scenario_names]
    if scenario_names and len(scenario_names) != len(scenario_ids):
        raise ValueError(
            "ERROR: if scenario names are provided, number of scenario names must match number of scenario ids"
        )
    if not custom_data:
        custom_data = {}
    if len(scenario_ids) + len(custom_data) <= 1:
        raise ValueError(
            "ERROR: must include at least two scenario ids and/or custom data")
    if isinstance(resource_types, str):
        resource_types = [resource_types]
    if not resource_labels:
        resource_labels = dict()
    if not isinstance(resource_labels, dict):
        raise TypeError("ERROR: resource_labels should be a dictionary")

    all_loadzone_data = {}
    scenario_data = {}
    for i, sid in enumerate(scenario_ids):
        scenario = Scenario(sid)
        mi = ModelImmutables(scenario.info["grid_model"])
        all_loadzone_data[sid] = {
            "gen":
            sum_generation_by_type_zone(
                scenario, time_range,
                time_zone).rename(columns=mi.zones["id2loadzone"]),
            "cap":
            sum_capacity_by_type_zone(scenario).rename(
                columns=mi.zones["id2loadzone"]),
        }
        scenario_data[sid] = {
            "name":
            scenario_names[i] if scenario_names else scenario.info["name"],
            "grid_model": mi.model,
            "gen": {
                "label": "Generation",
                "unit": "TWh",
                "data": {}
            },
            "cap": {
                "label": "Capacity",
                "unit": "GW",
                "data": {}
            },
        }
    for area, area_type in zip(areas, area_types):
        for sid in scenario_ids:
            loadzone_set = area_to_loadzone(scenario_data[sid]["grid_model"],
                                            area, area_type)
            scenario_data[sid]["gen"]["data"][area] = (
                all_loadzone_data[sid]["gen"][loadzone_set].sum(
                    axis=1).divide(1e6).astype("float").round(2).to_dict())
            scenario_data[sid]["cap"]["data"][area] = (
                all_loadzone_data[sid]["cap"][loadzone_set].sum(
                    axis=1).divide(1e3).astype("float").round(2).to_dict())
    for c_data in custom_data:
        scenario_data[c_data["name"]] = c_data

    for area in areas:
        if not resource_types:
            area_resource_types = sorted(
                set(r for sd in scenario_data.values()
                    for side in ["gen", "cap"]
                    for r, v in sd[side]["data"][area].items() if v > 0))
        else:
            area_resource_types = resource_types

        ax_data_list = []
        for side in ["gen", "cap"]:
            ax_data = {}
            for sd in scenario_data.values():
                # If we don't have data for a resource type, set it to 0
                ax_data[sd["name"]] = [
                    sd[side]["data"][area].get(r, 0)
                    for r in area_resource_types
                ]
            ax_data_list.append({
                "title":
                f"""{sd[side]["label"]} ({sd[side]["unit"]})""",
                "labels":
                [resource_labels.get(r, r) for r in area_resource_types],
                "values":
                ax_data,
                "unit":
                sd[side]["unit"],
            })

        if horizontal:
            _construct_hbar_visuals(area, ax_data_list)
        else:
            _construct_bar_visuals(area, ax_data_list)
Exemplo n.º 9
0
def test_scenario_workflow(monkeypatch):
    mock_context = MockContext()
    monkeypatch.setattr(Context, "get_data_access",
                        mock_context.get_data_access)

    s = Scenario()
    print(s.state.name)

    s.set_grid(interconnect="Texas")

    s.set_name("test", "dummy")
    s.set_time("2016-01-01 00:00:00", "2016-01-01 03:00:00", "1H")

    s.set_base_profile("demand", "vJan2021")
    s.set_base_profile("hydro", "vJan2021")
    s.set_base_profile("solar", "vJan2021")
    s.set_base_profile("wind", "vJan2021")
    s.change_table.ct = {
        "wind": {
            "zone_id": {
                301: 1.1293320940114195,
                302: 2.2996731828360466,
                303: 1.1460693669609412,
                304: 1.5378918905751389,
                305: 1.6606575751914816,
            },
            "plant_id": {
                12912: 0
            },
        }
    }

    s.get_grid()
    s.get_ct()

    rfs = s.data_access.fs
    lfs = s.data_access.local_fs
    tmp_dir = s.data_access.tmp_folder(1)

    s.print_scenario_info()
    s.create_scenario()

    scenario_list = s.get_scenario_table()
    assert 1 == scenario_list.shape[0]

    for fs in (lfs, rfs):
        assert fs.exists("data/input/1_ct.pkl")

    # hack to use truncated profiles so the test runs quickly
    s.info["grid_model"] = "test_usa_tamu"
    s.prepare_simulation_input()

    tmp_files = rfs.listdir(tmp_dir)
    assert len(tmp_files) > 0

    s.change(Delete)
    s.delete_scenario(confirm=False)

    for fs in (rfs, lfs):
        assert not fs.exists(tmp_dir)
        assert len(fs.listdir("data/input")) == 0

    scenario_list = Scenario().get_scenario_table()
    assert 0 == scenario_list.shape[0]
Exemplo n.º 10
0
def plot_bar_shortfall(
    areas,
    scenario_ids,
    target_df,
    strategy=None,
    scenario_names=None,
    baseline_scenario=None,
    baseline_scenario_name=None,
):
    """Plot a stacked bar chart of generation shortfall based on given targets for
        any number of scenarios.

    :param list/str areas: list of area(s) to show shortfall bar plots. If the target of
        an area is not defined in target_df, it will be ignored.
    :param int/list/str scenario_ids: list of scenario id(s).
    :param pandas.DataFrame target_df: target data frame, which defines the clean
        energy target fraction, allowed resources and external historical amount of
        qualified energy for each area.
    :param dict strategy: a dictionary with keys being scenario ids and values being
        strategies, either *"collaborative"* or *"independent"*. *"collaborative"* is
        used if None.
    :param list/str scenario_names: list of scenario name(s) of same len as scenario
        ids, defaults to None.
    :param str/int baseline_scenario: scenario id that serves as a baseline in the
        bar chart, default to None.
    :param str baseline_scenario_name: specify the label of the baseline scenario
        shown in the bar chart, default to None, in which case the name of the
        scenario will be used.
    :raises ValueError:
        if length of scenario_names and scenario_ids is different.
    :raises TypeError:
        if target_df is not a pandas.DataFrame and/or
        if strategy is provided but not in a dictionary format and/or
        if baseline_scenario is provided but not in a str/int format and/or
        if baseline_scenario_name is provided but not in a str format.
    """
    if isinstance(areas, str):
        areas = [areas]
    if isinstance(scenario_ids, (int, str)):
        scenario_ids = [scenario_ids]
    if not isinstance(target_df, pd.DataFrame):
        raise TypeError("ERROR: target_df must be a pandas.DataFrame")
    if strategy is None:
        strategy = dict()
    if not isinstance(strategy, dict):
        raise TypeError("ERROR: strategy must be a dictionary")
    if isinstance(scenario_names, str):
        scenario_names = [scenario_names]
    if scenario_names is not None and len(scenario_names) != len(scenario_ids):
        raise ValueError(
            "ERROR: if scenario names are provided, number of scenario names must match number of scenario ids"
        )
    if baseline_scenario is not None and not isinstance(baseline_scenario, (str, int)):
        raise TypeError("ERROR: baseline_scenario must be a string or integer")
    if baseline_scenario_name is not None and not isinstance(
        baseline_scenario_name, str
    ):
        raise TypeError("ERROR: baseline_scenario_name must be a string")

    scenarios = dict()
    targets = dict()
    all_sids = scenario_ids + [baseline_scenario] if baseline_scenario else scenario_ids
    for sid in all_sids:
        s = Scenario(sid)
        tmp_df = target_df.copy()
        tmp_df = add_resource_data_to_targets(tmp_df, s)
        tmp_df = add_demand_to_targets(tmp_df, s)
        tmp_df = add_shortfall_to_targets(tmp_df)
        scenarios[sid] = s
        targets[sid] = tmp_df
    for area in areas:
        if area not in target_df.index and area != "all":
            print(f"{area} is skipped due to lack of target information in target_df!")
            continue
        ax_data = {}
        for i, sid in enumerate(scenario_ids):
            label = scenario_names[i] if scenario_names else scenarios[sid].info["name"]
            if area == "all":
                demand = targets[sid]["demand"].sum()
                shortfall = calculate_overall_shortfall(
                    targets[sid], method=strategy.get(sid, "collaborative")
                )
                ce_generated = targets[sid]["ce_target"].sum() - shortfall
                if baseline_scenario:
                    baseline_shortfall = calculate_overall_shortfall(
                        targets[baseline_scenario],
                        method=strategy.get(baseline_scenario, "collaborative"),
                    )
                    baseline = (
                        targets[baseline_scenario]["ce_target"].sum()
                        - baseline_shortfall
                    )
                shortfall = max(shortfall, 0)
            else:
                demand = targets[sid].loc[area, "demand"]
                shortfall = targets[sid].loc[area, "ce_shortfall"]
                ce_generated = (
                    targets[sid].loc[area, "prev_ce_generation"]
                    + targets[sid].loc[area, "external_ce_addl_historical_amount"]
                )
                if baseline_scenario:
                    baseline = (
                        targets[baseline_scenario].loc[area, "prev_ce_generation"]
                        + targets[baseline_scenario].loc[
                            area, "external_ce_addl_historical_amount"
                        ]
                    )
            if baseline_scenario:
                ax_data.update(
                    {
                        label: {
                            baseline_scenario_name: round(100 * baseline / demand, 2),
                            "Increment from baseline": max(
                                0, round(100 * (ce_generated - baseline) / demand, 2)
                            ),
                            "Missed target": round(100 * shortfall / demand, 2),
                        }
                    }
                )
            else:
                ax_data.update(
                    {
                        label: {
                            "Qualified clean energy": round(
                                100 * ce_generated / demand, 2
                            ),
                            "Missed target": round(100 * shortfall / demand, 2),
                        }
                    }
                )
        if area != "all":
            target_pct = round(100 * target_df.loc[area, "ce_target_fraction"], 2)
        else:
            # The overall target percentage is different among scenarios given the
            # demand might be different. Here we pick the first scenario in scenario_ids
            # to calculate the overall target percentage
            target_demand = targets[scenario_ids[0]]["demand"].sum()
            target_generation = targets[scenario_ids[0]]["ce_target"].sum()
            target_pct = round(100 * target_generation / target_demand, 2)

        if baseline_scenario:
            _construct_shortfall_visuals(
                area,
                ax_data,
                target_pct,
                baseline=True,
                baseline_scenario_name=baseline_scenario_name,
            )
        else:
            _construct_shortfall_visuals(
                area,
                ax_data,
                target_pct,
            )
Exemplo n.º 11
0
def test_get_bus_demand():
    scenario = Scenario("")
    scenario.state.set_grid(interconnect="Texas")
    scenario.state.builder.set_base_profile("demand", "vJan2021")
    scenario.state.get_bus_demand()
Exemplo n.º 12
0
def plot_bar_generation_stack(
    areas,
    scenario_ids,
    resources,
    area_types=None,
    scenario_names=None,
    curtailment_split=True,
    t2c=None,
    t2l=None,
    t2hc=None,
    titles=None,
    plot_show=True,
    save=False,
    filenames=None,
    filepath=None,
):
    """Plot any number of scenarios as generation stack bar for selected resources in
    each specified areas.

    :param list/str areas: list of area(s), each area is one of *loadzone*, *state*,
        *state abbreviation*, *interconnect*, *'all'*.
    :param int/list/str scenario_ids: list of scenario id(s), defaults to None.
    :param str/list resources: one or a list of resources. *'curtailment'*,
        *'solar_curtailment'*, *'wind_curtailment'*, *'wind_offshore_curtailment'*
        are valid entries together with all available generator types in the area(s).
        The order of the resources determines the stack order in the figure.
    :param list/str area_types: list of area_type(s), each area_type is one of
        *'loadzone'*, *'state'*, *'state_abbr'*, *'interconnect'*, defaults to None.
    :param list/str scenario_names: list of scenario name(s) of same len as scenario
        ids, defaults to None.
    :param bool curtailment_split: if curtailments are split into different
        categories, defaults to True.
    :param dict t2c: user specified color of resource type to overwrite pre-defined ones
        key: resource type, value: color code.
    :param dict t2l: user specified label of resource type to overwrite pre-defined ones
        key: resource type, value: label.
    :param dict t2hc: user specified color of curtailable resource hatches to overwrite
        pre-defined ones. key: resource type, valid keys are *'curtailment'*,
        *'solar_curtailment'*, *'wind_curtailment'*, *'wind_offshore_curtailment'*,
        value: color code.
    :param dict titles: user specified figure titles, key: area, value: new figure
        title in string, use area as title if None.
    :param bool plot_show: display the generated figure or not, defaults to True.
    :param bool save: save the generated figure or not, defaults to False.
    :param dict filenames: user specified filenames, key: area, value: new filename
        in string, use area as filename if None.
    :param str filepath: if save is True, user specified filepath, use current
        directory if None.
    :return: (*list*) -- matplotlib.axes.Axes object of each plot in a list.
    :raises TypeError:
        if resources is not a list/str and/or
        if titles is provided but not in a dictionary format and/or
        if filenames is provided but not in a dictionary format.
    :raises ValueError:
        if length of area_types and areas is different and/or
        if length of scenario_names and scenario_ids is different.
    """
    if isinstance(areas, str):
        areas = [areas]
    if isinstance(scenario_ids, (int, str)):
        scenario_ids = [scenario_ids]
    if not isinstance(scenario_ids, list):
        raise TypeError("ERROR: scenario_ids should be a int/str/list")
    if isinstance(resources, str):
        resources = [resources]
    if not isinstance(resources, list):
        raise TypeError("ERROR: resources should be a list/str")
    if isinstance(area_types, str):
        area_types = [area_types]
    if not area_types:
        area_types = [None] * len(areas)
    if len(areas) != len(area_types):
        raise ValueError(
            "ERROR: if area_types are provided, number of area_types must match number of areas"
        )
    if isinstance(scenario_names, str):
        scenario_names = [scenario_names]
    if scenario_names and len(scenario_names) != len(scenario_ids):
        raise ValueError(
            "ERROR: if scenario names are provided, number of scenario names must match number of scenario ids"
        )
    if titles is not None and not isinstance(titles, dict):
        raise TypeError("ERROR: titles should be a dictionary if provided")
    if filenames is not None and not isinstance(filenames, dict):
        raise TypeError("ERROR: filenames should be a dictionary if provided")
    s_list = []
    for sid in scenario_ids:
        s_list.append(Scenario(sid))
    mi = ModelImmutables(s_list[0].info["grid_model"])
    type2color = mi.plants["type2color"]
    type2label = mi.plants["type2label"]
    type2hatchcolor = mi.plants["type2hatchcolor"]
    if t2c:
        type2color.update(t2c)
    if t2l:
        type2label.update(t2l)
    if t2hc:
        type2hatchcolor.update(t2hc)
    all_loadzone_data = dict()
    for sid, scenario in zip(scenario_ids, s_list):
        curtailment = calculate_curtailment_time_series_by_areas_and_resources(
            scenario, areas={"loadzone": mi.zones["loadzone"]})
        for area in curtailment:
            for r in curtailment[area]:
                curtailment[area][r] = curtailment[area][r].sum().sum()
        curtailment = (pd.DataFrame(curtailment).rename(
            columns=mi.zones["loadzone2id"]).T)
        curtailment.rename(
            columns={c: c + "_curtailment"
                     for c in curtailment.columns},
            inplace=True)
        curtailment["curtailment"] = curtailment.sum(axis=1)
        all_loadzone_data[sid] = pd.concat(
            [
                sum_generation_by_type_zone(scenario).T,
                scenario.state.get_demand().sum().T.rename("load"),
                curtailment,
            ],
            axis=1,
        ).rename(index=mi.zones["id2loadzone"])

    width = 0.4
    x_scale = 0.6
    ax_list = []
    for area, area_type in zip(areas, area_types):
        fig, ax = plt.subplots(figsize=(10, 8))
        for ind, s in enumerate(s_list):
            patches = []
            fuels = []
            bottom = 0
            loadzone_set = area_to_loadzone(s.info["grid_model"], area,
                                            area_type)
            data = (all_loadzone_data[scenario_ids[ind]].loc[loadzone_set].sum(
            ).divide(1e6).astype("float").round(2))
            for i, f in enumerate(resources[::-1]):
                if f == "load":
                    continue
                if curtailment_split and f == "curtailment":
                    continue
                if not curtailment_split and f in {
                        "wind_curtailment",
                        "solar_curtailment",
                        "wind_offshore_curtailment",
                }:
                    continue
                fuels.append(f)
                if "curtailment" in f:
                    patches.append(
                        ax.bar(
                            ind * x_scale,
                            data[f],
                            width,
                            bottom=bottom,
                            color=type2color.get(f, "red"),
                            hatch="//",
                            edgecolor=type2hatchcolor.get(f, "black"),
                            lw=0,
                        ))
                else:
                    patches.append(
                        ax.bar(
                            ind * x_scale,
                            data[f],
                            width,
                            bottom=bottom,
                            color=type2color[f],
                        ))
                bottom += data[f]

            # plot load line
            xs = [ind * x_scale - 0.5 * width, ind * x_scale + 0.5 * width]
            ys = [data["load"]] * 2
            line_patch = ax.plot(xs, ys, "--", color="black")

        if scenario_names:
            labels = scenario_names
        else:
            labels = [s.info["name'"] for s in s_list]
        ax.set_xticks([i * x_scale for i in range(len(s_list))])
        ax.set_xticklabels(labels, fontsize=12)
        ax.set_ylabel("TWh", fontsize=12)
        bar_legend = ax.legend(
            handles=patches[::-1] + line_patch,
            labels=[type2label.get(f, f.capitalize())
                    for f in fuels[::-1]] + ["Demand"],
            fontsize=12,
            bbox_to_anchor=(1, 1),
            loc="upper left",
        )
        ax.add_artist(bar_legend)
        ax.set_axisbelow(True)
        ax.grid(axis="y")
        if titles is not None and area in titles:
            ax.set_title(titles[area])
        else:
            ax.set_title(area)
        fig.tight_layout()
        ax_list.append(ax)
        if plot_show:
            plt.show()
        if save:
            if filenames is not None and area in filenames:
                filename = filenames[area]
            else:
                filename = area
            if not filepath:
                filepath = os.getcwd()
            fig.savefig(
                f"{os.path.join(filepath, filename)}.pdf",
                bbox_inches="tight",
                pad_inches=0,
            )
    return ax_list