def test_sum_generation(self):
     expected_return = pd.DataFrame({
         "type": ["hydro", "solar", "wind"],
         1: [0, 10, 15],
         2: [23, 0, 0]
     })
     expected_return.set_index("type", inplace=True)
     summed_generation = sum_generation_by_type_zone(self.scenario)
     check_dataframe_matches(summed_generation, expected_return)
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)
 def test_with_scenario_not_analyze(self):
     test_scenario = MockScenario(grid_attrs, pg=mock_pg)
     test_scenario.state = "Create"
     with self.assertRaises(ValueError):
         sum_generation_by_type_zone(test_scenario)
 def test_with_string(self):
     with self.assertRaises(TypeError):
         sum_generation_by_type_zone("scenario_number")
def plot_bar_generation_max_min_actual(
    scenario,
    interconnect,
    gen_type,
    percentage=False,
    show_as_state=True,
    fontsize=15,
    plot_show=True,
):
    """Generate for a given scenario the bar plot of total capacity vs. actual
    generation vs. minimum generation for a specific type of generators in an
    interconnection.

    :param powersimdata.scenario.scenario.Scenario scenario: scenario instance.
    :param str interconnect: the interconnection name of interest.
    :param str gen_type: type of generator.
    :param bool percentage: show bars in percentage of total capacity or not,
        defaults to False.
    :param bool show_as_state: each bar represents a state within the given
        interconnection or not, defaults to True, if not, each bar will represent a
        loadzone instead.
    :param int/float fontsize: font size of the texts shown on the plot.
    :param plot_show: show the plot or not, defaults to True.
    :return: (*matplotlib.axes.Axes*) -- axes object of the plot.
    """
    grid = scenario.state.get_grid()
    plant = grid.plant[grid.plant.type == gen_type]
    mi = ModelImmutables(scenario.info["grid_model"])
    hour_num = (pd.Timestamp(scenario.info["end_date"]) - pd.Timestamp(
        scenario.info["start_date"])).total_seconds() / 3600 + 1
    if show_as_state:
        zone_list = [
            mi.zones["abv2state"][abv]
            for abv in mi.zones["interconnect2abv"][interconnect]
        ]
        all_max_min = (
            plant.groupby(plant.zone_name.map(
                mi.zones["loadzone2state"]))[["Pmax", "Pmin"]].sum() *
            hour_num)
        all_actual_gen = sum_generation_by_state(scenario)[gen_type] * 1000
    else:
        zone_list = mi.zones["interconnect2loadzone"][interconnect]
        all_max_min = plant.groupby("zone_name")[["Pmax", "Pmin"
                                                  ]].sum() * hour_num
        all_actual_gen = (sum_generation_by_type_zone(scenario).rename(
            columns=grid.id2zone).T[gen_type])
    df = pd.concat([all_max_min, all_actual_gen], axis=1).loc[zone_list]
    if percentage:
        df_pct = df.copy()
        df_pct["Pmax"] = df["Pmax"].apply(lambda x: 1 if x > 0 else 0)
        df_pct["Pmin"] = df["Pmin"] / df["Pmax"]
        df_pct[gen_type] = df[gen_type] / df["Pmax"]
        df = df_pct
    df.sort_index(inplace=True)

    width = 0.8
    fig, ax = plt.subplots(figsize=[30, 15])
    df["Pmax"].plot(kind="bar", width=width, color="yellowgreen", ax=ax)
    df[gen_type].plot(kind="bar", width=width * 0.7, color="steelblue", ax=ax)
    df["Pmin"].plot(kind="bar", width=width * 0.4, color="firebrick", ax=ax)
    ax.legend(
        ["Total Capacity", "Actual Generation", "Minimum Generation"],
        bbox_to_anchor=(0.5, 0.95),
        loc="lower center",
        ncol=3,
        fontsize=fontsize,
    )
    ax.grid()
    if percentage:
        ax.set_title(f"{gen_type.capitalize()} Generation %")
    else:
        ax.set_title(f"{gen_type.capitalize()} Generation MWh")
    for item in ([ax.title, ax.xaxis.label, ax.yaxis.label] +
                 ax.get_xticklabels() + ax.get_yticklabels()):
        item.set_fontsize(fontsize)
    if plot_show:
        plt.show()
    return ax
Ejemplo n.º 6
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)
Ejemplo n.º 7
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