Skip to content

Commit 644376c

Browse files
pesapstaadecker
authored andcommitted
Merge pull request #103 from staadecker/plots
Plot improvements
2 parents d313aae + b42ae45 commit 644376c

File tree

21 files changed

+1097
-69
lines changed

21 files changed

+1097
-69
lines changed

papers/Martin_Staadecker_Value_of_LDES_and_Factors/LDES_paper_graphs/plots.py

Lines changed: 480 additions & 0 deletions
Large diffs are not rendered by default.

switch_model/balancing/load_zones.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,11 +287,12 @@ def graph_energy_balance(tools):
287287
)
288288
load_balance = load_balance[["energy_balance_duals", "time_row"]]
289289
load_balance = load_balance.pivot(columns="time_row", values="energy_balance_duals")
290+
percent_of_zeroes = sum(load_balance == 0) / len(load_balance) * 100
290291
# Don't include the zero-valued duals
291292
load_balance = load_balance.replace(0, tools.np.nan)
292293
if load_balance.count().sum() != 0:
293294
load_balance.plot.box(
294-
ax=tools.get_axes(),
295+
ax=tools.get_axes(note=f"{percent_of_zeroes:.1f}% of duals are zero"),
295296
xlabel="Period",
296297
ylabel="Energy balance duals (cents/kWh)",
297298
showfliers=False,

switch_model/generators/core/build.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -915,6 +915,33 @@ def graph_buildout_per_tech(tools):
915915
ax = tools.get_axes()
916916
df.plot(ax=ax, kind="line", color=colors, xlabel="Period", marker="x")
917917
# Set the y-axis to use percent
918-
ax.yaxis.set_major_formatter(tools.mplt.ticker.PercentFormatter(1.0))
918+
ax.yaxis.set_major_formatter(tools.plt.ticker.PercentFormatter(1.0))
919919
# Horizontal line at 100%
920920
ax.axhline(y=1, linestyle="--", color="b")
921+
922+
923+
@graph("buildout_map", title="Map of online capacity per load zone.")
924+
def buildout_map(tools):
925+
buildout = tools.get_dataframe("gen_cap.csv").rename(
926+
{"GenCapacity": "value"}, axis=1
927+
)
928+
buildout = tools.transform.gen_type(buildout)
929+
buildout = buildout.groupby(["gen_type", "gen_load_zone"], as_index=False)[
930+
"value"
931+
].sum()
932+
ax = tools.maps.graph_pie_chart(buildout)
933+
transmission = tools.get_dataframe(
934+
"transmission.csv", convert_dot_to_na=True
935+
).fillna(0)
936+
transmission = transmission.rename(
937+
{"trans_lz1": "from", "trans_lz2": "to", "BuildTx": "value"}, axis=1
938+
)
939+
transmission = transmission[["from", "to", "value", "PERIOD"]]
940+
transmission = (
941+
transmission.groupby(["from", "to", "PERIOD"], as_index=False)
942+
.sum()
943+
.drop("PERIOD", axis=1)
944+
)
945+
# Rename the columns appropriately
946+
transmission.value *= 1e-3
947+
tools.maps.graph_transmission(transmission, cutoff=0.1, ax=ax, legend=True)

switch_model/generators/core/dispatch.py

Lines changed: 141 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -922,6 +922,146 @@ def graph_curtailment_per_tech(tools):
922922
df.plot(ax=ax, kind="line", xlabel="Period", marker="x", **kwargs)
923923

924924
# Set the y-axis to use percent
925-
ax.yaxis.set_major_formatter(tools.mplt.ticker.PercentFormatter(1.0))
925+
ax.yaxis.set_major_formatter(tools.plt.ticker.PercentFormatter(1.0))
926926
# Horizontal line at 100%
927927
# ax.axhline(y=1, linestyle="--", color='b')
928+
929+
930+
@graph(
931+
"energy_balance_2",
932+
title="Balance between demand, generation and storage for last period",
933+
note="Dashed green and red lines are total generation and total demand (incl. transmission losses),"
934+
" respectively.\nDotted line is the total state of charge (scaled for readability)."
935+
"\nWe used a 14-day rolling mean to smoothen out values.",
936+
)
937+
def graph_energy_balance_2(tools):
938+
# Get dispatch dataframe
939+
dispatch = tools.get_dataframe(
940+
"dispatch.csv",
941+
usecols=[
942+
"timestamp",
943+
"gen_tech",
944+
"gen_energy_source",
945+
"DispatchGen_MW",
946+
"scenario_name",
947+
],
948+
).rename({"DispatchGen_MW": "value"}, axis=1)
949+
dispatch = tools.transform.gen_type(dispatch)
950+
951+
# Sum dispatch across all the projects of the same type and timepoint
952+
dispatch = dispatch.groupby(["timestamp", "gen_type"], as_index=False).sum()
953+
dispatch = dispatch[dispatch["gen_type"] != "Storage"]
954+
955+
# Get load dataframe
956+
load = tools.get_dataframe(
957+
"load_balance.csv",
958+
usecols=["timestamp", "zone_demand_mw", "TXPowerNet", "scenario_name"],
959+
)
960+
961+
def process_time(df):
962+
df = df.astype({"period": int})
963+
df = df[df["period"] == df["period"].max()].drop(columns="period")
964+
return df.set_index("datetime")
965+
966+
# Sum load across all the load zones
967+
load = load.groupby(["timestamp"], as_index=False).sum()
968+
969+
# Include Tx Losses in demand and flip sign
970+
load["value"] = (load["zone_demand_mw"] + load["TXPowerNet"]) * -1
971+
972+
# Rename and convert from wide to long format
973+
load = load[["timestamp", "value"]]
974+
975+
# Add the timestamp information and make period string to ensure it doesn't mess up the graphing
976+
dispatch = process_time(tools.transform.timestamp(dispatch))
977+
load = process_time(tools.transform.timestamp(load))
978+
979+
# Convert to TWh (incl. multiply by timepoint duration)
980+
dispatch["value"] *= dispatch["tp_duration"] / 1e6
981+
load["value"] *= load["tp_duration"] / 1e6
982+
983+
days = 14
984+
freq = str(days) + "D"
985+
offset = tools.pd.Timedelta(freq) / 2
986+
987+
def rolling_sum(df):
988+
df = df.rolling(freq, center=True).value.sum().reset_index()
989+
df["value"] /= days
990+
df = df[
991+
(df.datetime.min() + offset < df.datetime)
992+
& (df.datetime < df.datetime.max() - offset)
993+
]
994+
return df
995+
996+
dispatch = rolling_sum(dispatch.groupby("gen_type", as_index=False))
997+
load = rolling_sum(load).set_index("datetime")["value"]
998+
999+
# Get the state of charge data
1000+
soc = tools.get_dataframe(
1001+
"StateOfCharge.csv", dtype={"STORAGE_GEN_TPS_1": str}
1002+
).rename(columns={"STORAGE_GEN_TPS_2": "timepoint", "StateOfCharge": "value"})
1003+
# Sum over all the projects that are in the same scenario with the same timepoint
1004+
soc = soc.groupby(["timepoint"], as_index=False).sum()
1005+
soc["value"] /= 1e6 # Convert to TWh
1006+
max_soc = soc["value"].max()
1007+
1008+
# Group by time
1009+
soc = process_time(
1010+
tools.transform.timestamp(soc, use_timepoint=True, key_col="timepoint")
1011+
)
1012+
soc = soc.rolling(freq, center=True)["value"].mean().reset_index()
1013+
soc = soc[
1014+
(soc.datetime.min() + offset < soc.datetime)
1015+
& (soc.datetime < soc.datetime.max() - offset)
1016+
]
1017+
soc = soc.set_index("datetime")["value"]
1018+
1019+
dispatch = dispatch[dispatch["value"] != 0]
1020+
dispatch = dispatch.pivot(columns="gen_type", index="datetime", values="value")
1021+
dispatch = dispatch[dispatch.std().sort_values().index].rename_axis(
1022+
"Technology", axis=1
1023+
)
1024+
total_dispatch = dispatch.sum(axis=1)
1025+
1026+
max_val = max(total_dispatch.max(), load.max())
1027+
1028+
# Scale soc to the graph
1029+
soc *= max_val / max_soc
1030+
1031+
# Plot
1032+
# Get the colors for the lines
1033+
# plot
1034+
ax = tools.get_axes(ylabel="Average Daily Generation (TWh)")
1035+
ax.set_ylim(0, max_val * 1.05)
1036+
dispatch.plot(ax=ax, color=tools.get_colors())
1037+
soc.plot(ax=ax, color="black", linestyle="dotted")
1038+
load.plot(ax=ax, color="red", linestyle="dashed")
1039+
total_dispatch.plot(ax=ax, color="green", linestyle="dashed")
1040+
ax.fill_between(
1041+
total_dispatch.index,
1042+
total_dispatch.values,
1043+
load.values,
1044+
alpha=0.2,
1045+
where=load < total_dispatch,
1046+
facecolor="green",
1047+
)
1048+
ax.fill_between(
1049+
total_dispatch.index,
1050+
total_dispatch.values,
1051+
load.values,
1052+
alpha=0.2,
1053+
where=load > total_dispatch,
1054+
facecolor="red",
1055+
)
1056+
1057+
1058+
@graph("dispatch_map", title="Dispatched electricity per load zone")
1059+
def dispatch_map(tools):
1060+
dispatch = tools.get_dataframe("dispatch_zonal_annual_summary.csv").rename(
1061+
{"Energy_GWh_typical_yr": "value"}, axis=1
1062+
)
1063+
dispatch = tools.transform.gen_type(dispatch)
1064+
dispatch = dispatch.groupby(["gen_type", "gen_load_zone"], as_index=False)[
1065+
"value"
1066+
].sum()
1067+
tools.maps.graph_pie_chart(dispatch)

switch_model/generators/extensions/storage.py

Lines changed: 62 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
3333
"""
3434
import math
35+
36+
import pandas as pd
3537
from scipy import fft
3638

3739
from pyomo.environ import *
@@ -502,58 +504,91 @@ def post_solve(instance, outdir):
502504
"state_of_charge",
503505
title="State of Charge Throughout the Year",
504506
supports_multi_scenario=True,
507+
note="The daily charge/discharge amount is calculated as"
508+
" the difference between the maximum and minimum"
509+
" state of charge in a 1-day rolling window.\n"
510+
"The black line is the 14-day rolling mean of the state of charge.",
505511
)
506512
def graph_state_of_charge(tools):
513+
# Each panel is a period and scenario
514+
panel_group = ["period", "scenario_name"]
515+
rolling_mean_window_size = "14D"
516+
507517
# Get the total state of charge per timepoint and scenario
508-
df = tools.get_dataframe("storage_dispatch")
509-
df = df.groupby(["timepoint", "scenario_name"], as_index=False)[
510-
"StateOfCharge"
511-
].sum()
518+
soc = tools.get_dataframe("storage_dispatch.csv").rename(
519+
{"StateOfCharge": "value"}, axis=1
520+
)
521+
soc = soc.groupby(["timepoint", "scenario_name"], as_index=False).value.sum()
522+
# Convert values to TWh
523+
soc.value /= 1e6
512524
# Add datetime information
513-
df = tools.transform.timestamp(df, key_col="timepoint")
525+
soc = tools.transform.timestamp(soc, key_col="timepoint")[
526+
panel_group + ["datetime", "value"]
527+
]
514528
# Count num rows
515-
num_periods = len(df["period"].unique())
529+
num_periods = len(soc["period"].unique())
530+
531+
# Used later
532+
grouped_soc = soc.set_index("datetime").groupby(panel_group, as_index=False)
533+
534+
# Calculate the weekly SOC
535+
weekly_soc = (
536+
grouped_soc.rolling(rolling_mean_window_size, center=True)
537+
.value.mean()
538+
.reset_index()
539+
)
516540

517541
# Get the total capacity per period and scenario
518542
capacity = tools.get_dataframe("storage_capacity.csv")
519-
capacity = capacity.groupby(["period", "scenario_name"], as_index=False)[
520-
"OnlineEnergyCapacityMWh"
521-
].sum()
543+
capacity = (
544+
capacity.groupby(panel_group, as_index=False)["OnlineEnergyCapacityMWh"]
545+
.sum()
546+
.rename({"OnlineEnergyCapacityMWh": "value"}, axis=1)
547+
)
548+
capacity.value /= 1e6
549+
capacity["type"] = "Total Energy Capacity"
522550

523-
# Add the capacity to our dataframe
524-
df = df.merge(
525-
capacity, on=["period", "scenario_name"], validate="many_to_one", how="left"
551+
# Add information regarding the diurnal cycle to the dataframe
552+
# Find the difference between the min and max for every day of the year
553+
group = grouped_soc.rolling("D", center=True).value
554+
daily_size = (
555+
(group.max() - group.min()).reset_index().groupby(panel_group, as_index=False)
526556
)
557+
# Find the mean between the difference of the min and max
558+
avg_daily_size = daily_size.mean()[panel_group + ["value"]]
559+
avg_daily_size["type"] = "Mean Daily Charge/Discharge"
560+
max_daily_size = daily_size.max()[panel_group + ["value"]]
561+
max_daily_size["type"] = "Maximum Daily Charge/Discharge"
527562

528-
# Convert values to TWh
529-
df["StateOfCharge"] /= 1e6
530-
df["OnlineEnergyCapacityMWh"] /= 1e6
563+
# Determine information for the labels
564+
y_axis_max = capacity.value.max()
565+
label_x_pos = soc["datetime"].median()
566+
567+
hlines = pd.concat([capacity, avg_daily_size, max_daily_size])
531568

532-
# Determine information for the label
533-
y_axis_lim = df["OnlineEnergyCapacityMWh"].max()
534-
offset = y_axis_lim * 0.05
535-
df["label_position"] = df["OnlineEnergyCapacityMWh"] + offset
536-
df["label"] = df["OnlineEnergyCapacityMWh"].round(decimals=2)
537-
label_x_pos = df["datetime"].median()
569+
# For the max label
570+
hlines["label_pos"] = hlines.value + y_axis_max * 0.05
571+
hlines["label"] = hlines.value.round(decimals=2)
538572

539573
# Plot with plotnine
540574
pn = tools.pn
541575
plot = (
542-
pn.ggplot(df, pn.aes(x="datetime", y="StateOfCharge"))
543-
+ pn.geom_line()
576+
pn.ggplot(soc, pn.aes(x="datetime", y="value"))
577+
+ pn.geom_line(color="gray")
578+
+ pn.geom_line(data=weekly_soc, color="black")
544579
+ pn.labs(y="State of Charge (TWh)", x="Time of Year")
545580
+ pn.geom_hline(
546-
pn.aes(yintercept="OnlineEnergyCapacityMWh"),
581+
pn.aes(yintercept="value", label="label", color="type"),
582+
data=hlines,
547583
linetype="dashed",
548-
color="blue",
549584
)
550585
+ pn.geom_text(
551-
pn.aes(label="label", x=label_x_pos, y="label_position"),
586+
pn.aes(label="label", x=label_x_pos, y="label_pos"),
587+
data=hlines,
552588
fontweight="light",
553589
size="10",
554590
)
555591
)
556-
557592
tools.save_figure(by_scenario_and_period(tools, plot, num_periods).draw())
558593

559594

switch_model/tools/graph/cli.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,10 @@ def add_arguments(parser):
2929
)
3030
parser.add_argument(
3131
"-f",
32-
"--figure",
32+
"--figures",
3333
default=None,
34-
help="Name of the figure to graph. Figure names are the first argument in the @graph() decorator."
34+
nargs="+",
35+
help="Name of the figures to graph. Figure names are the first argument in the @graph() decorator."
3536
" If unspecified graphs all the figures.",
3637
)
3738
parser.add_argument(
@@ -53,5 +54,5 @@ def graph_scenarios_from_cli(scenarios, args):
5354
args.overwrite,
5455
args.skip_long,
5556
args.modules,
56-
args.figure,
57+
args.figures,
5758
)

0 commit comments

Comments
 (0)