import os
import matplotlib.pyplot as plt
import pandas as pd
from powersimdata.network.model import ModelImmutables, area_to_loadzone
from powersimdata.scenario.scenario import Scenario
from postreise.analyze.generation.curtailment import (
calculate_curtailment_time_series_by_areas_and_resources,
)
from postreise.analyze.generation.summarize import sum_generation_by_type_zone
[docs]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 or str.
if ``titles`` is not a dict.
if ``filenames`` is not a dict.
:raises ValueError:
if length of ``area_types`` and ``areas`` is different.
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("scenario_ids must be a int, str or list")
if isinstance(resources, str):
resources = [resources]
if not isinstance(resources, list):
raise TypeError("resources must be a list or 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("area_types must have same size as areas")
if isinstance(scenario_names, str):
scenario_names = [scenario_names]
if scenario_names and len(scenario_names) != len(scenario_ids):
raise ValueError("scenario_names must have same size as scenario_ids")
if titles is not None and not isinstance(titles, dict):
raise TypeError("titles must be a dictionary")
if filenames is not None and not isinstance(filenames, dict):
raise TypeError("filenames must be a dictionary")
s_list = []
for sid in scenario_ids:
s_list.append(Scenario(sid))
mi = ModelImmutables(s_list[0].info["grid_model"])
type2color = mi.plants["type2color"]
type2color.update(
{k + "_curtailment": v for k, v in mi.plants["curtailable2color"].items()}
)
type2label = mi.plants["type2label"]
type2label.update(
{k + "_curtailment": v for k, v in mi.plants["curtailable2label"].items()}
)
type2hatchcolor = {
k + "_curtailment": v for k, v in mi.plants["curtailable2hatchcolor"].items()
}
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["interconnect2loadzone"][
scenario.info["interconnect"]
]
},
)
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.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
zone_list = list(area_to_loadzone(s.info["grid_model"], area, area_type))
data = (
all_loadzone_data[scenario_ids[ind]]
.loc[zone_list]
.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 {
r + "_curtailment" for r in mi.plants["curtailable_resources"]
}:
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