Skip to content

Commit 1c6c6f1

Browse files
committed
Add plots
1 parent 68d535f commit 1c6c6f1

File tree

5 files changed

+500
-25
lines changed

5 files changed

+500
-25
lines changed
Lines changed: 362 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,362 @@
1+
import pandas as pd
2+
from matplotlib import pyplot as plt
3+
from matplotlib.pyplot import setp
4+
from matplotlib.ticker import PercentFormatter
5+
6+
from switch_model.tools.graph.main import graph_scenarios, graph, Scenario
7+
8+
X_LABEL = "WECC-wide Storage Capacity (TWh)"
9+
10+
11+
def set_styles(tools):
12+
tools.plt.rcParams['font.family'] = 'sans-serif'
13+
14+
15+
@graph(
16+
"figure_1",
17+
supports_multi_scenario=True,
18+
title="Figure 1: The value of storage in the WECC",
19+
note="Panel 1: Shaded area represents the 25th to 75th percentile range"
20+
" for dual values.\nDual values for non-binding energy balance constraints"
21+
" are ignored."
22+
)
23+
def figure_1(tools):
24+
set_styles(tools)
25+
26+
figure_size = (12, 12)
27+
fig = tools.get_figure(size=figure_size)
28+
axes = fig.subplots(2, 2)
29+
30+
tools.plt.rcParams['lines.marker'] = '.'
31+
figure_1_panel_1(tools, axes[0][0])
32+
figure_1_panel_2(tools, axes[0][1])
33+
figure_1_panel_3(tools, axes[1][0])
34+
figure_1_panel_4(tools, axes[1][1])
35+
tools.plt.rcParams['lines.marker'] = None
36+
37+
for row in axes:
38+
for ax in row:
39+
ax.set_xlabel(X_LABEL)
40+
41+
42+
def figure_1_panel_1(tools, ax):
43+
df = tools.get_dataframe('load_balance.csv') \
44+
.rename({"normalized_energy_balance_duals_dollar_per_mwh": "value"}, axis=1)
45+
df = df[df.value != 0]
46+
load_balance_group = df \
47+
.groupby("scenario_name") \
48+
.value
49+
mean = load_balance_group.mean().rename("mean_val")
50+
upper = load_balance_group.quantile(0.75).rename("upper")
51+
lower = load_balance_group.quantile(0.25).rename("lower")
52+
df = pd.concat([lower, mean, upper], axis=1)
53+
# Convert from $/MWh to cents/kWh
54+
df *= 0.1
55+
56+
x = df.index.values
57+
ax.plot(x, df.mean_val.values, color="black")
58+
ax.fill_between(x, df.lower.values, df.upper.values, alpha=0.5, color="gray")
59+
ax.set_ylabel("Mean Energy Balance Duals (cents/kWh)")
60+
61+
62+
def figure_1_panel_2(tools, ax):
63+
# Read dispatch.csv
64+
df = tools.get_dataframe(
65+
'dispatch.csv',
66+
usecols=["gen_tech", "gen_energy_source", "Curtailment_MW", "is_renewable", "tp_weight_in_year_hrs"],
67+
na_filter=False, # For performance
68+
)
69+
# Keep only renewable
70+
df = df[df["is_renewable"]]
71+
# Add the gen_type column
72+
df = tools.transform.gen_type(df)
73+
# Convert to GW
74+
df["value"] = df["Curtailment_MW"] * df["tp_weight_in_year_hrs"] / 1000
75+
df = df.groupby(["scenario_name", "gen_type"], as_index=False).value.sum()
76+
df = df.pivot(index="scenario_name", columns="gen_type", values="value")
77+
df /= 1000
78+
df = df.rename_axis("Technology", axis=1)
79+
df.plot(
80+
ax=ax,
81+
color=tools.get_colors(),
82+
)
83+
ax.set_ylabel("Yearly Curtailment (GWh)")
84+
85+
86+
def figure_1_panel_3(tools, ax):
87+
df = tools.get_dataframe("transmission.csv", usecols=["BuildTx"], convert_dot_to_na=True)
88+
df = df.fillna(0).rename({"BuildTx": "value"}, axis=1)
89+
df = df.groupby("scenario_name").value.sum() / 1000
90+
df.plot(ax=ax, color="black")
91+
ax.set_ylim(0, 60)
92+
ax.set_ylabel("New Transmission Capacity Built (GW)")
93+
94+
95+
def figure_1_panel_4(tools, ax):
96+
df = tools.get_dataframe("BuildGen.csv").rename(columns={
97+
"GEN_BLD_YRS_1": "GENERATION_PROJECT",
98+
"GEN_BLD_YRS_2": "period",
99+
"BuildGen": "value"
100+
})
101+
df = df[df.period == 2050]
102+
projects = tools.get_dataframe("generation_projects_info.csv", from_inputs=True, usecols=[
103+
"GENERATION_PROJECT", "gen_tech", "gen_energy_source"
104+
]).drop("scenario_index", axis=1)
105+
df = df.merge(
106+
projects,
107+
on=["GENERATION_PROJECT", "scenario_name"],
108+
validate="one_to_one",
109+
how="left"
110+
).drop("GENERATION_PROJECT", axis=1)
111+
del projects
112+
df = tools.transform.gen_type(df)
113+
df = df.rename({"GenCapacity": "value"}, axis=1)
114+
df = df.groupby(["scenario_name", "gen_type"], as_index=False).value.sum()
115+
df.value /= 1000
116+
df = df[df["gen_type"] != "Storage"]
117+
df = df[df.value != 0]
118+
df = df.pivot(index="scenario_name", columns="gen_type", values="value")
119+
df.loc["total", :] = df.sum()
120+
df = df.sort_values("total", axis=1)
121+
df = df.drop("total")
122+
df = df.rename_axis("Technology", axis=1)
123+
df.plot(
124+
ax=ax,
125+
kind="area",
126+
color=tools.get_colors(),
127+
)
128+
ax.set_ylabel("Newly Capacity Installations (GW)")
129+
130+
131+
@graph(
132+
"figure_2",
133+
title="Figure 2: Generation Mix",
134+
supports_multi_scenario=True
135+
)
136+
def figure_2(tools):
137+
set_styles(tools)
138+
fig = tools.get_figure(size=(12, 12))
139+
ax1 = fig.add_subplot(3, 2, 1)
140+
ax2 = fig.add_subplot(3, 2, 2, sharey=ax1)
141+
ax3 = fig.add_subplot(3, 2, 3, sharey=ax1, sharex=ax1)
142+
ax4 = fig.add_subplot(3, 2, 4, sharey=ax1, sharex=ax2)
143+
ax5 = fig.add_subplot(3, 2, (5, 6))
144+
145+
setp(ax2.get_yticklabels(), visible=False)
146+
setp(ax4.get_yticklabels(), visible=False)
147+
setp(ax1.get_xticklabels(), visible=False)
148+
setp(ax2.get_xticklabels(), visible=False)
149+
150+
figure_2_energy_balance(tools, ax1, scenario_name=1.94)
151+
figure_2_energy_balance(tools, ax2, scenario_name=4)
152+
figure_2_energy_balance(tools, ax3, scenario_name=16)
153+
figure_2_energy_balance(tools, ax4, scenario_name=64)
154+
figure_2_panel_5(tools, ax5)
155+
handles, labels = ax1.get_legend_handles_labels()
156+
unique = [(h, l) for i, (h, l) in enumerate(zip(handles, labels)) if l not in labels[:i]]
157+
plt.figlegend(*zip(*unique))
158+
159+
160+
def figure_2_panel_5(tools, ax):
161+
df = tools.get_dataframe("gen_cap.csv",
162+
usecols=["gen_tech", "gen_energy_source", "GenCapacity"])
163+
df = tools.transform.gen_type(df)
164+
df = df.rename({"GenCapacity": "value"}, axis=1)
165+
df = df.groupby(["scenario_name", "gen_type"], as_index=False).value.sum()
166+
scaling = df[df["scenario_name"] == 1.94][["gen_type", "value"]].rename(columns={"value": "scaling"})
167+
df = df.merge(scaling, on="gen_type")
168+
df.value /= df.scaling
169+
df.value = (df.value - 1) * 100
170+
df = df[df["gen_type"].isin(("Wind", "Solar", "Biomass"))]
171+
df = df.pivot(index="scenario_name", columns="gen_type", values="value")
172+
df = df.rename_axis("Technology", axis=1)
173+
df.plot(
174+
ax=ax,
175+
color=tools.get_colors(),
176+
legend=False
177+
)
178+
ax.set_ylabel("Percent Change in Installed Capacity against Baseline")
179+
ax.yaxis.set_major_formatter(PercentFormatter())
180+
ax.set_xlabel(X_LABEL)
181+
182+
183+
def filter_scenario(df, scenario_name):
184+
return df[df["scenario_name"] == scenario_name].drop(columns=["scenario_name", "scenario_index"]).copy()
185+
186+
187+
def figure_2_energy_balance(tools, ax, scenario_name):
188+
# Get dispatch dataframe
189+
dispatch = tools.get_dataframe("dispatch.csv", usecols=[
190+
"timestamp", "gen_tech", "gen_energy_source", "DispatchGen_MW"
191+
]).rename({"DispatchGen_MW": "value"}, axis=1)
192+
dispatch = filter_scenario(dispatch, scenario_name)
193+
dispatch = tools.transform.gen_type(dispatch)
194+
195+
# Sum dispatch across all the projects of the same type and timepoint
196+
dispatch = dispatch.groupby(["timestamp", "gen_type"], as_index=False).sum()
197+
dispatch = dispatch[dispatch["gen_type"] != "Storage"]
198+
199+
# Get load dataframe
200+
load = tools.get_dataframe("load_balance.csv", usecols=[
201+
"timestamp", "zone_demand_mw", "TXPowerNet"
202+
])
203+
load = filter_scenario(load, scenario_name)
204+
205+
def process_time(df):
206+
df = df.astype({"period": int})
207+
df = df[df["period"] == df["period"].max()].drop(columns="period")
208+
return df.set_index("datetime")
209+
210+
# Sum load across all the load zones
211+
load = load.groupby("timestamp", as_index=False).sum()
212+
213+
# Include Tx Losses in demand and flip sign
214+
load["value"] = (load["zone_demand_mw"] + load["TXPowerNet"]) * -1
215+
216+
# Rename and convert from wide to long format
217+
load = load[["timestamp", "value"]]
218+
219+
# Add the timestamp information and make period string to ensure it doesn't mess up the graphing
220+
dispatch = process_time(tools.transform.timestamp(dispatch))
221+
load = process_time(tools.transform.timestamp(load))
222+
223+
# Convert to TWh (incl. multiply by timepoint duration)
224+
dispatch["value"] *= dispatch["tp_duration"] / 1e6
225+
load["value"] *= load["tp_duration"] / 1e6
226+
227+
days = 14
228+
freq = str(days) + "D"
229+
offset = tools.pd.Timedelta(freq) / 2
230+
231+
def rolling_sum(df):
232+
df = df.rolling(freq, center=True).value.sum().reset_index()
233+
df["value"] /= days
234+
df = df[(df.datetime.min() + offset < df.datetime) & (df.datetime < df.datetime.max() - offset)]
235+
return df
236+
237+
dispatch = rolling_sum(dispatch.groupby("gen_type", as_index=False))
238+
load = rolling_sum(load).set_index("datetime")["value"]
239+
240+
# Get the state of charge data
241+
soc = tools.get_dataframe("StateOfCharge.csv", dtype={"STORAGE_GEN_TPS_1": str}) \
242+
.rename(columns={"STORAGE_GEN_TPS_2": "timepoint", "StateOfCharge": "value"})
243+
soc = filter_scenario(soc, scenario_name)
244+
# Sum over all the projects that are in the same scenario with the same timepoint
245+
soc = soc.groupby(["timepoint"], as_index=False).sum()
246+
soc["value"] /= 1e6 # Convert to TWh
247+
max_soc = soc["value"].max()
248+
249+
# Group by time
250+
soc = process_time(tools.transform.timestamp(soc, use_timepoint=True, key_col="timepoint"))
251+
soc = soc.rolling(freq, center=True)["value"].mean().reset_index()
252+
soc = soc[(soc.datetime.min() + offset < soc.datetime) & (soc.datetime < soc.datetime.max() - offset)]
253+
soc = soc.set_index("datetime")["value"]
254+
255+
dispatch = dispatch[dispatch["value"] != 0]
256+
dispatch = dispatch.pivot(columns="gen_type", index="datetime", values="value")
257+
dispatch = dispatch[dispatch.std().sort_values().index].rename_axis("Technology", axis=1)
258+
total_dispatch = dispatch.sum(axis=1)
259+
260+
max_val = max(total_dispatch.max(), load.max())
261+
262+
# Scale soc to the graph
263+
soc *= 100 / scenario_name
264+
265+
# Plot
266+
# Get the colors for the lines
267+
# plot
268+
ax.set_ylim(0, max_val * 1.05)
269+
dispatch.plot(
270+
ax=ax,
271+
color=tools.get_colors(),
272+
legend=False,
273+
xlabel=""
274+
)
275+
ax2 = ax.twinx()
276+
ax2.yaxis.set_major_formatter(PercentFormatter())
277+
ax2.set_ylim(0, 100)
278+
soc.plot(ax=ax2, color="black", linestyle="dotted", label="State of Charge", xlabel="")
279+
load.plot(ax=ax, color="red", linestyle="dashed", label="Total Demand", xlabel="")
280+
total_dispatch.plot(ax=ax, color="green", linestyle="dashed", label="Total Generation", xlabel="")
281+
ax.fill_between(total_dispatch.index, total_dispatch.values, load.values, alpha=0.2, where=load < total_dispatch,
282+
facecolor="green")
283+
ax.fill_between(total_dispatch.index, total_dispatch.values, load.values, alpha=0.2, where=load > total_dispatch,
284+
facecolor="red")
285+
ax.set_title(str(scenario_name) + "TWh of storage")
286+
287+
288+
@graph(
289+
"figure_3",
290+
title="Figure 3: Map of buildout",
291+
supports_multi_scenario=True
292+
)
293+
def figure_3(tools):
294+
fig = tools.get_figure(size=(24, 12))
295+
axes = fig.subplots(2, 4)
296+
297+
plot_buildout(tools, 1.94, axes[0][0])
298+
plot_buildout(tools, 4, axes[0][1])
299+
plot_buildout(tools, 16, axes[0][2])
300+
plot_buildout(tools, 64, axes[0][3])
301+
302+
dispatch_map(tools, 1.94, axes[1][0])
303+
dispatch_map(tools, 4, axes[1][1])
304+
dispatch_map(tools, 16, axes[1][2])
305+
dispatch_map(tools, 64, axes[1][3])
306+
307+
308+
def plot_buildout(tools, scenario_name, ax):
309+
buildout = tools.get_dataframe("gen_cap.csv").rename({"GenCapacity": "value"}, axis=1)
310+
buildout = filter_scenario(buildout, scenario_name)
311+
buildout = tools.transform.gen_type(buildout)
312+
buildout = buildout.groupby(["gen_type", "gen_load_zone"], as_index=False)["value"].sum()
313+
tools.maps.graph_pie_chart(buildout, ax=ax, max_size=1500)
314+
transmission = tools.get_dataframe("transmission.csv", convert_dot_to_na=True).fillna(0)
315+
transmission = filter_scenario(transmission, scenario_name)
316+
transmission = transmission.rename({"trans_lz1": "from", "trans_lz2": "to", "BuildTx": "value"}, axis=1)
317+
transmission = transmission[["from", "to", "value", "PERIOD"]]
318+
transmission = transmission.groupby(["from", "to", "PERIOD"], as_index=False).sum().drop("PERIOD", axis=1)
319+
# Rename the columns appropriately
320+
transmission.value *= 1e-3
321+
tools.maps.graph_transmission(transmission, cutoff=0.1, ax=ax, zorder=50, legend=False)
322+
ax.set_title(str(scenario_name) + "TWh of storage")
323+
324+
def dispatch_map(tools, scenario_name, ax):
325+
dispatch = tools.get_dataframe("transmission_dispatch.csv")
326+
dispatch = filter_scenario(dispatch, scenario_name)
327+
dispatch = tools.transform.timestamp(dispatch).astype({"period": int})
328+
# Keep only the last period
329+
last_period = dispatch["period"].max()
330+
dispatch = dispatch[dispatch["period"] == last_period]
331+
dispatch = dispatch.rename({"load_zone_from": "from", "load_zone_to": "to", "transmission_dispatch": "value"},
332+
axis=1)
333+
dispatch["value"] *= dispatch["tp_duration"] * 1e-6 # Change from power value to energy value
334+
dispatch = dispatch.groupby(["from", "to"], as_index=False)["value"].sum()
335+
ax = tools.maps.graph_transmission(dispatch, cutoff=1, ax=ax)
336+
exports = dispatch[["from", "value"]].rename({"from": "gen_load_zone"}, axis=1).copy()
337+
imports = dispatch[["to", "value"]].rename({"to": "gen_load_zone"}, axis=1).copy()
338+
imports["value"] *= -1
339+
exports = pd.concat([imports, exports])
340+
exports = exports.groupby("gen_load_zone", as_index=False).sum()
341+
tools.maps.graph_points(exports, ax)
342+
343+
344+
if __name__ == "__main__":
345+
baseline = Scenario("1342", name=1.94)
346+
min_4 = Scenario("M6", name=4)
347+
min_8 = Scenario("M5", name=8)
348+
min_16 = Scenario("M4", name=16)
349+
min_32 = Scenario("M3", name=32)
350+
min_64 = Scenario("M2", name=64)
351+
minimum_scenarios = [baseline, min_4, min_8, min_16, min_32, min_64]
352+
kwargs = dict(graph_dir="LDES_paper_graphs", overwrite=True, module_names=[])
353+
# graph_scenarios(
354+
# scenarios=minimum_scenarios,
355+
# figures=["figure_2"],
356+
# **kwargs
357+
# )
358+
graph_scenarios(
359+
scenarios=[baseline, min_4, min_16, min_64],
360+
figures=["figure_3"],
361+
**kwargs
362+
)

switch_model/generators/core/build.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -776,7 +776,7 @@ def graph_buildout_per_tech(tools):
776776
ax = tools.get_axes()
777777
df.plot(ax=ax, kind='line', color=colors, xlabel='Period', marker="x")
778778
# Set the y-axis to use percent
779-
ax.yaxis.set_major_formatter(tools.mplt.ticker.PercentFormatter(1.0))
779+
ax.yaxis.set_major_formatter(tools.plt.ticker.PercentFormatter(1.0))
780780
# Horizontal line at 100%
781781
ax.axhline(y=1, linestyle="--", color='b')
782782

@@ -788,4 +788,11 @@ def buildout_map(tools):
788788
buildout = tools.get_dataframe("gen_cap.csv").rename({"GenCapacity": "value"}, axis=1)
789789
buildout = tools.transform.gen_type(buildout)
790790
buildout = buildout.groupby(["gen_type", "gen_load_zone"], as_index=False)["value"].sum()
791-
tools.maps.graph_pie_chart(buildout)
791+
ax = tools.maps.graph_pie_chart(buildout)
792+
transmission = tools.get_dataframe("transmission.csv", convert_dot_to_na=True).fillna(0)
793+
transmission = transmission.rename({"trans_lz1": "from", "trans_lz2": "to", "BuildTx": "value"}, axis=1)
794+
transmission = transmission[["from", "to", "value", "PERIOD"]]
795+
transmission = transmission.groupby(["from", "to", "PERIOD"], as_index=False).sum().drop("PERIOD", axis=1)
796+
# Rename the columns appropriately
797+
transmission.value *= 1e-3
798+
tools.maps.graph_transmission(transmission, cutoff=0.1, ax=ax, legend=True)

0 commit comments

Comments
 (0)