Skip to content

Commit 2df9f32

Browse files
committed
Improve state of charge plot
1 parent d0716b6 commit 2df9f32

File tree

2 files changed

+58
-55
lines changed

2 files changed

+58
-55
lines changed

switch_model/generators/extensions/storage.py

Lines changed: 57 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
storage, when to charge, energy accounting, etc.
88
"""
99
import math
10+
11+
import pandas as pd
1012
from scipy import fft
1113

1214
from pyomo.environ import *
@@ -515,90 +517,91 @@ def post_solve(instance, outdir):
515517
"state_of_charge",
516518
title="State of Charge Throughout the Year",
517519
supports_multi_scenario=True,
520+
note="The daily charge/discharge amount is calculated as"
521+
" the difference between the maximum and minimum"
522+
" state of charge in a 1-day rolling window.\n"
523+
"The black line is the 14-day rolling mean of the state of charge.",
518524
)
519525
def graph_state_of_charge(tools):
526+
# Each panel is a period and scenario
527+
panel_group = ["period", "scenario_name"]
528+
rolling_mean_window_size = "14D"
529+
520530
# Get the total state of charge per timepoint and scenario
521-
df = tools.get_dataframe("storage_dispatch")
522-
df = df.groupby(["timepoint", "scenario_name"], as_index=False)[
523-
"StateOfCharge"
524-
].sum()
531+
soc = tools.get_dataframe("storage_dispatch.csv").rename(
532+
{"StateOfCharge": "value"}, axis=1
533+
)
534+
soc = soc.groupby(["timepoint", "scenario_name"], as_index=False).value.sum()
535+
# Convert values to TWh
536+
soc.value /= 1e6
525537
# Add datetime information
526-
df = tools.transform.timestamp(df, key_col="timepoint")
538+
soc = tools.transform.timestamp(soc, key_col="timepoint")[
539+
panel_group + ["datetime", "value"]
540+
]
527541
# Count num rows
528-
num_periods = len(df["period"].unique())
542+
num_periods = len(soc["period"].unique())
529543

530-
# Get the total capacity per period and scenario
531-
capacity = tools.get_dataframe("storage_capacity.csv")
532-
capacity = capacity.groupby(["period", "scenario_name"], as_index=False)[
533-
"OnlineEnergyCapacityMWh"
534-
].sum()
544+
# Used later
545+
grouped_soc = soc.set_index("datetime").groupby(panel_group, as_index=False)
535546

536-
# Add the capacity to our dataframe
537-
df = df.merge(
538-
capacity, on=["period", "scenario_name"], validate="many_to_one", how="left"
547+
# Calculate the weekly SOC
548+
weekly_soc = (
549+
grouped_soc.rolling(rolling_mean_window_size, center=True)
550+
.value.mean()
551+
.reset_index()
539552
)
540553

541-
# Convert values to TWh
542-
df["StateOfCharge"] /= 1e6
543-
df["OnlineEnergyCapacityMWh"] /= 1e6
554+
# Get the total capacity per period and scenario
555+
capacity = tools.get_dataframe("storage_capacity.csv")
556+
capacity = (
557+
capacity.groupby(panel_group, as_index=False)["OnlineEnergyCapacityMWh"]
558+
.sum()
559+
.rename({"OnlineEnergyCapacityMWh": "value"}, axis=1)
560+
)
561+
capacity.value /= 1e6
562+
capacity["type"] = "Total Energy Capacity"
544563

545564
# Add information regarding the diurnal cycle to the dataframe
546565
# Find the difference between the min and max for every day of the year
547-
group = df.groupby(
548-
["period", "scenario_name", tools.pd.Grouper(freq="D", key="datetime")]
549-
)["StateOfCharge"]
550-
daily_size = (group.max() - group.min()).reset_index()
551-
# Find the mean between the difference of the min and max
566+
group = grouped_soc.rolling("D", center=True).value
552567
daily_size = (
553-
daily_size.groupby(["period", "scenario_name"], as_index=False)
554-
.mean()
555-
.reset_index()
556-
)
557-
# Add the mean to the dataframe under the name DiurnalCycle
558-
df = df.merge(
559-
daily_size.rename({"StateOfCharge": "DiurnalCycle"}, axis=1),
560-
on=["period", "scenario_name"],
561-
how="left",
568+
(group.max() - group.min()).reset_index().groupby(panel_group, as_index=False)
562569
)
570+
# Find the mean between the difference of the min and max
571+
avg_daily_size = daily_size.mean()[panel_group + ["value"]]
572+
avg_daily_size["type"] = "Mean Daily Charge/Discharge"
573+
max_daily_size = daily_size.max()[panel_group + ["value"]]
574+
max_daily_size["type"] = "Maximum Daily Charge/Discharge"
563575

564576
# Determine information for the labels
565-
y_axis_max = df["OnlineEnergyCapacityMWh"].max()
566-
label_offset = y_axis_max * 0.05
567-
label_x_pos = df["datetime"].median()
577+
y_axis_max = capacity.value.max()
578+
label_x_pos = soc["datetime"].median()
579+
580+
hlines = pd.concat([capacity, avg_daily_size, max_daily_size])
581+
568582
# For the max label
569-
df["label_position"] = df["OnlineEnergyCapacityMWh"] + label_offset
570-
df["label"] = df["OnlineEnergyCapacityMWh"].round(decimals=2)
571-
# For the diurnal cycle label
572-
df["diurnal_label"] = df["DiurnalCycle"].round(decimals=2)
573-
df["diurnal_label_pos"] = df["DiurnalCycle"] + label_offset
583+
hlines["label_pos"] = hlines.value + y_axis_max * 0.05
584+
hlines["label"] = hlines.value.round(decimals=2)
574585

575586
# Plot with plotnine
576587
pn = tools.pn
577588
plot = (
578-
pn.ggplot(df, pn.aes(x="datetime", y="StateOfCharge"))
579-
+ pn.geom_line()
589+
pn.ggplot(soc, pn.aes(x="datetime", y="value"))
590+
+ pn.geom_line(color="gray")
591+
+ pn.geom_line(data=weekly_soc, color="black")
580592
+ pn.labs(y="State of Charge (TWh)", x="Time of Year")
581593
+ pn.geom_hline(
582-
pn.aes(yintercept="OnlineEnergyCapacityMWh"),
594+
pn.aes(yintercept="value", label="label", color="type"),
595+
data=hlines,
583596
linetype="dashed",
584-
color="blue",
585597
)
586598
+ pn.geom_text(
587-
pn.aes(label="label", x=label_x_pos, y="label_position"),
599+
pn.aes(label="label", x=label_x_pos, y="label_pos"),
600+
data=hlines,
588601
fontweight="light",
589602
size="10",
590603
)
591-
+ pn.geom_hline(
592-
pn.aes(yintercept="DiurnalCycle"), linetype="dashed", color="red"
593-
)
594-
+ pn.geom_text(
595-
pn.aes(label="diurnal_label", x=label_x_pos, y="diurnal_label_pos"),
596-
fontweight="light",
597-
size="10",
598-
color="red",
599-
)
600604
)
601-
602605
tools.save_figure(by_scenario_and_period(tools, plot, num_periods).draw())
603606

604607

switch_model/tools/graph/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -774,7 +774,7 @@ def __init__(self, scenarios: List[Scenario], graph_dir: str, skip_long: bool):
774774
# Set the style to Seaborn default style
775775
sns.set()
776776
# Don't show white outline around shapes to avoid confusion
777-
plt.rcParams["patch.edgecolor"] = 'none'
777+
plt.rcParams["patch.edgecolor"] = "none"
778778

779779
# Disables pandas warnings that will occur since we are constantly returning only a slice of our master dataframe
780780
pd.options.mode.chained_assignment = None

0 commit comments

Comments
 (0)