Skip to content

Commit 6ed6a8f

Browse files
committed
Create energy balance throughout the year plot
1 parent ce5ec8f commit 6ed6a8f

File tree

4 files changed

+198
-55
lines changed

4 files changed

+198
-55
lines changed

switch_model/balancing/load_zones.py

Lines changed: 44 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -286,9 +286,9 @@ def get_component_per_year(m, z, p, component):
286286
@graph(
287287
"energy_balance_duals",
288288
title="Energy balance duals per period",
289-
note="Note: Outliers and zero-valued duals are ignored."
289+
note="Note: Outliers and zero-valued duals are ignored.",
290290
)
291-
def graph(tools):
291+
def graph_energy_balance(tools):
292292
load_balance = tools.get_dataframe("load_balance.csv")
293293
load_balance = tools.transform.timestamp(load_balance)
294294
load_balance["energy_balance_duals"] = (
@@ -303,22 +303,50 @@ def graph(tools):
303303
# Don't include the zero-valued duals
304304
load_balance = load_balance.replace(0, tools.np.nan)
305305
if load_balance.count().sum() != 0:
306-
<<<<<<< HEAD
307-
ax = tools.get_axes(
308-
"energy_balance_duals",
309-
title="Energy balance duals per period",
310-
note="Note: Outliers and zero-valued duals are ignored.",
311-
)
312306
load_balance.plot.box(
313-
ax=ax,
307+
ax=tools.get_axes(),
314308
xlabel="Period",
315309
ylabel="Energy balance duals (cents/kWh)",
316310
showfliers=False,
317-
=======
318-
load_balance.plot.box(
319-
ax=tools.get_axes(),
320-
xlabel='Period',
321-
ylabel='Energy balance duals (cents/kWh)',
322-
showfliers=False
323-
>>>>>>> b3590fdb (Redesign graphing API)
324311
)
312+
313+
314+
@graph("daily_demand", title="Total daily demand", supports_multi_scenario=True)
315+
def demand(tools):
316+
df = tools.get_dataframe("loads.csv", from_inputs=True, drop_scenario_info=False)
317+
df = df.groupby(["TIMEPOINT", "scenario_name"], as_index=False).sum()
318+
df = tools.transform.timestamp(df, key_col="TIMEPOINT", use_timepoint=True)
319+
df = df.groupby(
320+
["season", "hour", "scenario_name", "time_row"], as_index=False
321+
).mean()
322+
df["zone_demand_mw"] /= 1e3
323+
pn = tools.pn
324+
325+
plot = (
326+
pn.ggplot(df)
327+
+ pn.geom_line(pn.aes(x="hour", y="zone_demand_mw", color="scenario_name"))
328+
+ pn.facet_grid("time_row ~ season")
329+
+ pn.labs(x="Hour (PST)", y="Demand (GW)", color="Scenario")
330+
)
331+
tools.save_figure(plot.draw())
332+
333+
334+
@graph("demand", title="Total demand", supports_multi_scenario=True)
335+
def yearly_demand(tools):
336+
df = tools.get_dataframe("loads.csv", from_inputs=True, drop_scenario_info=False)
337+
df = df.groupby(["TIMEPOINT", "scenario_name"], as_index=False).sum()
338+
df = tools.transform.timestamp(df, key_col="TIMEPOINT", use_timepoint=True)
339+
df["zone_demand_mw"] *= df["tp_duration"] / 1e3
340+
df["day"] = df["datetime"].dt.day_of_year
341+
df = df.groupby(["day", "scenario_name", "time_row"], as_index=False)[
342+
"zone_demand_mw"
343+
].sum()
344+
pn = tools.pn
345+
346+
plot = (
347+
pn.ggplot(df)
348+
+ pn.geom_line(pn.aes(x="day", y="zone_demand_mw", color="scenario_name"))
349+
+ pn.facet_grid("time_row ~ .")
350+
+ pn.labs(x="Day of Year", y="Demand (GW)", color="Scenario")
351+
)
352+
tools.save_figure(plot.draw())

switch_model/generators/core/dispatch.py

Lines changed: 106 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -788,29 +788,114 @@ def graph_total_dispatch(tools):
788788

789789
tools.bar_label()
790790

791-
@graph("energy_balance", title="Annual Contributions to the energy balance")
791+
@graph(
792+
"energy_balance",
793+
title="Energy Balance For Every Month",
794+
supports_multi_scenario=True,
795+
is_long=True
796+
)
792797
def energy_balance(tools):
793-
load = tools.get_dataframe("load_balance_annual.csv").set_index("period")
794-
load.loc["total", :] = load.sum()
795-
load = load.rename(
796-
{
797-
"zone_demand_mw": "Demand",
798-
"ZoneTotalCentralDispatch": "Generation (incl. storage discharge)",
799-
"TXPowerNet": "Transmission Losses",
800-
"StorageNetCharge": "Storage Charging"
801-
}, axis=1
802-
).sort_values(axis=1, by="total")
803-
load = load.drop("total", axis=0)
804-
805-
load /= 1e6 # Convert to TWh
806-
load.plot(
807-
kind="bar",
808-
ax=tools.get_axes(),
809-
ylabel="Annual Contribution to Energy Balance (TWh)",
810-
xlabel="Period"
811-
)
798+
# Get dispatch dataframe
799+
cols = ["timestamp", "gen_tech", "gen_energy_source", "DispatchGen_MW", "scenario_name", "scenario_index"]
800+
df = tools.get_dataframe("dispatch.csv", drop_scenario_info=False)[cols]
801+
df = tools.transform.gen_type(df)
802+
# Sum dispatch across all the projects of the same type and timepoint
803+
df = df.groupby(["timestamp", "gen_type", "scenario_name", "scenario_index"], as_index=False).sum()
804+
df = df.rename({"gen_type": "Type", "DispatchGen_MW": "value"}, axis=1)
805+
806+
discharge = df[df["Type"] == "Storage"].drop("Type", axis=1).rename({"value": "discharge"}, axis=1)
807+
808+
# Get load dataframe
809+
load = tools.get_dataframe("load_balance.csv", drop_scenario_info=False)
810+
load = load.drop("normalized_energy_balance_duals_dollar_per_mwh", axis=1)
811+
812+
# Sum load across all the load zones
813+
key_columns = ["timestamp", "scenario_name", "scenario_index"]
814+
load = load.groupby(key_columns, as_index=False).sum()
815+
816+
# Subtract storage dispatch from generation and add it to the storage charge to get net flow
817+
load = load.merge(
818+
discharge,
819+
how="left",
820+
on=key_columns,
821+
validate="one_to_one"
822+
)
823+
load["ZoneTotalCentralDispatch"] -= load["discharge"]
824+
load["StorageNetCharge"] += load["discharge"]
825+
load = load.drop("discharge", axis=1)
826+
827+
# Rename and convert from wide to long format
828+
load = load.rename({
829+
"ZoneTotalCentralDispatch": "Total Generation (excl. storage discharge)",
830+
"TXPowerNet": "Transmission Losses",
831+
"StorageNetCharge": "Storage Net Flow",
832+
"zone_demand_mw": "Demand",
833+
}, axis=1).sort_index(axis=1)
834+
load = load.melt(id_vars=key_columns, var_name="Type")
835+
836+
# Merge dispatch contributions with load contributions
837+
df = pd.concat([load, df])
838+
839+
# Add the timestamp information and make period string to ensure it doesn't mess up the graphing
840+
df = tools.transform.timestamp(df).astype({"period": str})
841+
842+
# Convert to TWh (incl. multiply by timepoint duration)
843+
df["value"] *= df["tp_duration"] / 1e6
844+
845+
FREQUENCY = "1W"
846+
847+
def groupby_time(df):
848+
return df.groupby(
849+
["scenario_name", "period", "Type", tools.pd.Grouper(key="datetime", freq=FREQUENCY, origin="start")])[
850+
"value"]
851+
852+
df = groupby_time(df).sum().reset_index()
853+
854+
# Get the state of charge data
855+
soc = tools.get_dataframe("StateOfCharge.csv", dtype={"STORAGE_GEN_TPS_1": str}, drop_scenario_info=False)
856+
soc = soc.rename({"STORAGE_GEN_TPS_2": "timepoint", "StateOfCharge": "value"}, axis=1)
857+
# Sum over all the projects that are in the same scenario with the same timepoint
858+
soc = soc.groupby(["timepoint", "scenario_name"], as_index=False).sum()
859+
soc["Type"] = "State Of Charge"
860+
soc["value"] /= 1e6 # Convert to TWh
861+
862+
# Group by time
863+
soc = tools.transform.timestamp(soc, use_timepoint=True, key_col="timepoint").astype({"period": str})
864+
soc = groupby_time(soc).mean().reset_index()
865+
866+
# Add state of charge to dataframe
867+
df = pd.concat([df, soc])
868+
# Add column for day since that's what we really care about
869+
df["day"] = df["datetime"].dt.dayofyear
812870

813-
tools.bar_label()
871+
# Plot
872+
# Get the colors for the lines
873+
colors = tools.get_colors()
874+
colors.update({
875+
"Transmission Losses": "brown",
876+
"Storage Net Flow": "cadetblue",
877+
"Demand": "black",
878+
"Total Generation (excl. storage discharge)": "black",
879+
"State Of Charge": "green"
880+
})
881+
882+
# plot
883+
num_periods = df["period"].nunique()
884+
pn = tools.pn
885+
plot = pn.ggplot(df) + \
886+
pn.geom_line(pn.aes(x="day", y="value", color="Type")) + \
887+
pn.facet_grid("period ~ scenario_name") + \
888+
pn.labs(y="Contribution to Energy Balance (TWh)") + \
889+
pn.scales.scale_color_manual(values=colors, aesthetics="color", na_value=colors["Other"]) + \
890+
pn.scales.scale_x_continuous(
891+
name="Month",
892+
labels=["J", "F", "M", "A", "M", "J", "J", "A", "S", "O", "N", "D"],
893+
breaks=(15, 46, 76, 106, 137, 167, 198, 228, 259, 289, 319, 350),
894+
limits=(0, 366)) + \
895+
pn.theme(
896+
figure_size=(pn.options.figure_size[0] * tools.num_scenarios, pn.options.figure_size[1] * num_periods))
897+
898+
tools.save_figure(plot.draw())
814899

815900
@graph(
816901
"curtailment_per_period",

switch_model/generators/extensions/storage.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -523,7 +523,7 @@ def graph_state_of_charge(tools):
523523
"StateOfCharge"
524524
].sum()
525525
# Add datetime information
526-
df = tools.transform.timestamp(df, timestamp_col="timepoint")
526+
df = tools.transform.timestamp(df, key_col="timepoint")
527527
# Count num rows
528528
num_periods = len(df["period"].unique())
529529

@@ -591,7 +591,7 @@ def graph_state_of_charge_per_duration(tools):
591591
df = tools.get_dataframe("storage_dispatch")[
592592
["generation_project", "timepoint", "StateOfCharge", "scenario_name"]
593593
]
594-
df = tools.transform.timestamp(df, timestamp_col="timepoint")
594+
df = tools.transform.timestamp(df, key_col="timepoint")
595595

596596
# Add the capacity information to the state of charge information
597597
df = df.merge(
@@ -631,7 +631,7 @@ def graph_dispatch_cycles(tools):
631631
# Aggregate by timepoint
632632
df = df.groupby("timepoint", as_index=False).sum()
633633
# Add datetime column
634-
df = tools.transform.timestamp(df, timestamp_col="timepoint")
634+
df = tools.transform.timestamp(df, key_col="timepoint")
635635
# Find charge in GWh
636636
df["StateOfCharge"] /= 1e3
637637

switch_model/tools/graph/main.py

Lines changed: 45 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ def gen_type(
148148
map_name="default",
149149
gen_tech_col="gen_tech",
150150
energy_source_col="gen_energy_source",
151+
drop_previous_col=True,
151152
):
152153
"""
153154
Returns a dataframe that contains a column 'gen_type'.
@@ -157,11 +158,17 @@ def gen_type(
157158
"""
158159
# If there's no mapping, we simply make the mapping the sum of both columns
159160
# Read the tech_colors and tech_types csv files.
160-
df = df.copy() # Don't edit the original
161161
try:
162+
cols = [
163+
"map_name",
164+
"gen_type",
165+
"gen_tech",
166+
"energy_source",
167+
"scenario_index",
168+
]
162169
tech_types = self.tools.get_dataframe(
163-
"graph_tech_types.csv", from_inputs=True
164-
)
170+
"graph_tech_types.csv", from_inputs=True, drop_scenario_info=False
171+
)[cols]
165172
except FileNotFoundError:
166173
df["gen_type"] = df[gen_tech_col] + "_" + df[energy_source_col]
167174
return df
@@ -172,9 +179,9 @@ def gen_type(
172179
# we want to merge by scenario
173180
left_on = [gen_tech_col, energy_source_col]
174181
right_on = ["gen_tech", "energy_source"]
175-
if "scenario_name" in tech_types:
176-
left_on.append("scenario_name")
177-
right_on.append("scenario_name")
182+
if "scenario_index" in df:
183+
left_on.append("scenario_index")
184+
right_on.append("scenario_index")
178185
df = df.merge(
179186
tech_types,
180187
left_on=left_on,
@@ -185,6 +192,8 @@ def gen_type(
185192
df["gen_type"] = df["gen_type"].fillna(
186193
"Other"
187194
) # Fill with Other so the colors still work
195+
if drop_previous_col:
196+
df = df.drop([gen_tech_col, energy_source_col], axis=1)
188197
return df
189198

190199
def build_year(self, df, build_year_col="build_year"):
@@ -203,7 +212,7 @@ def build_year(self, df, build_year_col="build_year"):
203212
)
204213
return df
205214

206-
def timestamp(self, df, timestamp_col="timestamp"):
215+
def timestamp(self, df, key_col="timestamp", use_timepoint=False):
207216
"""
208217
Adds the following columns to the dataframe:
209218
- time_row: by default the period but can be overridden by graph_timestamp_map.csv
@@ -226,17 +235,34 @@ def timestamp(self, df, timestamp_col="timestamp"):
226235
validate="many_to_one",
227236
)
228237
timestamp_mapping = timepoints[
229-
["timestamp", "ts_period", "timeseries"]
238+
[
239+
"timepoint_id",
240+
"timestamp",
241+
"ts_period",
242+
"timeseries",
243+
"ts_duration_of_tp",
244+
]
230245
].drop_duplicates()
231-
timestamp_mapping = timestamp_mapping.rename({"ts_period": "period"}, axis=1)
246+
timestamp_mapping = timestamp_mapping.rename(
247+
{
248+
"ts_period": "period",
249+
"timepoint_id": "timepoint",
250+
"ts_duration_of_tp": "tp_duration",
251+
},
252+
axis=1,
253+
)
232254
timestamp_mapping = timestamp_mapping.astype({"period": "category"})
233255

234-
df = df.rename({timestamp_col: "timestamp"}, axis=1)
235-
df = df.merge(
236-
timestamp_mapping,
237-
how="left",
238-
on="timestamp",
239-
)
256+
if use_timepoint:
257+
df = df.rename({key_col: "timepoint"}, axis=1)
258+
df = df.merge(timestamp_mapping, how="left", on="timepoint")
259+
else:
260+
df = df.rename({key_col: "timestamp"}, axis=1)
261+
df = df.merge(
262+
timestamp_mapping,
263+
how="left",
264+
on="timestamp",
265+
)
240266

241267
try:
242268
# TODO support using graph_timestamp_map on multiple scenarios
@@ -258,6 +284,8 @@ def timestamp(self, df, timestamp_col="timestamp"):
258284
.dt.tz_convert(self.time_zone)
259285
)
260286
df["hour"] = df["datetime"].dt.hour
287+
season_map = {1: "Winter", 2: "Spring", 3: "Summer", 4: "Fall"}
288+
df["season"] = df["datetime"].dt.quarter.apply(lambda x: season_map[x])
261289

262290
return df
263291

@@ -515,6 +543,8 @@ def _load_dataframe(self, path, dtype=None, **kwargs) -> pd.DataFrame:
515543
index_col=False,
516544
# Fix: force the datatype to str for some columns to avoid warnings of mismatched types
517545
dtype=dtype,
546+
sep=",",
547+
engine="c",
518548
**kwargs,
519549
)
520550
df["scenario_name"] = scenario.name

0 commit comments

Comments
 (0)