Skip to content

Commit 2585613

Browse files
committed
Merge branch 'wecc' into improve_load_aug
# Conflicts: # switch_model/utilities/__init__.py
2 parents 5dd96a6 + 9db1fb5 commit 2585613

File tree

11 files changed

+449
-74
lines changed

11 files changed

+449
-74
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*.vcf
1616
*.xml
1717
*.pickle
18+
examples/**/temp/**
1819
examples/**/outputs/*.csv
1920
examples/**/outputs/*.txt
2021
examples/**/graphs/*.png
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/*
2+
####################
3+
Add wind_to_solar_ratio column
4+
5+
Date applied: 2021-07-07
6+
Description:
7+
Adds a column called wind_to_solar_ratio to the database which is used by
8+
switch_model.policies.wind_to_solar_ratio
9+
#################
10+
*/
11+
12+
ALTER TABLE switch.scenario ADD COLUMN wind_to_solar_ratio real;

switch_model/balancing/load_zones.py

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -244,10 +244,11 @@ def get_component_per_year(m, z, p, component):
244244
title="Energy balance duals per period",
245245
note="Note: Outliers and zero-valued duals are ignored."
246246
)
247-
def graph(tools):
247+
def graph_energy_balance(tools):
248248
load_balance = tools.get_dataframe('load_balance.csv')
249249
load_balance = tools.transform.timestamp(load_balance)
250-
load_balance["energy_balance_duals"] = tools.pd.to_numeric(load_balance["normalized_energy_balance_duals_dollar_per_mwh"], errors="coerce") / 10
250+
load_balance["energy_balance_duals"] = tools.pd.to_numeric(
251+
load_balance["normalized_energy_balance_duals_dollar_per_mwh"], errors="coerce") / 10
251252
load_balance = load_balance[["energy_balance_duals", "time_row"]]
252253
load_balance = load_balance.pivot(columns="time_row", values="energy_balance_duals")
253254
# Don't include the zero-valued duals
@@ -259,3 +260,44 @@ def graph(tools):
259260
ylabel='Energy balance duals (cents/kWh)',
260261
showfliers=False
261262
)
263+
264+
265+
@graph(
266+
"daily_demand",
267+
title="Total daily demand",
268+
supports_multi_scenario=True
269+
)
270+
def demand(tools):
271+
df = tools.get_dataframe("loads.csv", from_inputs=True, drop_scenario_info=False)
272+
df = df.groupby(["TIMEPOINT", "scenario_name"], as_index=False).sum()
273+
df = tools.transform.timestamp(df, key_col="TIMEPOINT", use_timepoint=True)
274+
df = df.groupby(["season", "hour", "scenario_name", "time_row"], as_index=False).mean()
275+
df["zone_demand_mw"] /= 1e3
276+
pn = tools.pn
277+
278+
plot = pn.ggplot(df) + \
279+
pn.geom_line(pn.aes(x="hour", y="zone_demand_mw", color="scenario_name")) + \
280+
pn.facet_grid("time_row ~ season") + \
281+
pn.labs(x="Hour (PST)", y="Demand (GW)", color="Scenario")
282+
tools.save_figure(plot.draw())
283+
284+
285+
@graph(
286+
"demand",
287+
title="Total demand",
288+
supports_multi_scenario=True
289+
)
290+
def yearly_demand(tools):
291+
df = tools.get_dataframe("loads.csv", from_inputs=True, drop_scenario_info=False)
292+
df = df.groupby(["TIMEPOINT", "scenario_name"], as_index=False).sum()
293+
df = tools.transform.timestamp(df, key_col="TIMEPOINT", use_timepoint=True)
294+
df["zone_demand_mw"] *= df["tp_duration"] / 1e3
295+
df["day"] = df["datetime"].dt.day_of_year
296+
df = df.groupby(["day", "scenario_name", "time_row"], as_index=False)["zone_demand_mw"].sum()
297+
pn = tools.pn
298+
299+
plot = pn.ggplot(df) + \
300+
pn.geom_line(pn.aes(x="day", y="zone_demand_mw", color="scenario_name")) + \
301+
pn.facet_grid("time_row ~ .") + \
302+
pn.labs(x="Day of Year", y="Demand (GW)", color="Scenario")
303+
tools.save_figure(plot.draw())

switch_model/generators/core/build.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -666,14 +666,15 @@ def graph_capacity(tools):
666666
color=tools.get_colors(len(capacity_df.index)),
667667
)
668668

669+
tools.bar_label()
669670

670671
@graph(
671672
"buildout_gen_per_period",
672673
title="Built Capacity per Period",
673674
supports_multi_scenario=True
674675
)
675676
def graph_buildout(tools):
676-
build_gen = tools.get_dataframe("BuildGen.csv")
677+
build_gen = tools.get_dataframe("BuildGen.csv", dtype={"GEN_BLD_YRS_1": str})
677678
build_gen = build_gen.rename(
678679
{"GEN_BLD_YRS_1": "GENERATION_PROJECT", "GEN_BLD_YRS_2": "build_year", "BuildGen": "Amount"},
679680
axis=1

switch_model/generators/core/dispatch.py

Lines changed: 128 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -609,7 +609,6 @@ def graph_hourly_curtailment(tools):
609609
@graph(
610610
"total_dispatch",
611611
title="Total dispatched electricity",
612-
is_long=True,
613612
)
614613
def graph_total_dispatch(tools):
615614
# ---------------------------------- #
@@ -649,6 +648,134 @@ def graph_total_dispatch(tools):
649648
ylabel="Total dispatched electricity (TWh)"
650649
)
651650

651+
tools.bar_label()
652+
653+
@graph(
654+
"energy_balance",
655+
title="Energy Balance For Every Month",
656+
supports_multi_scenario=True,
657+
is_long=True
658+
)
659+
def energy_balance(tools):
660+
# Get dispatch dataframe
661+
cols = ["timestamp", "gen_tech", "gen_energy_source", "DispatchGen_MW", "scenario_name", "scenario_index",
662+
"Curtailment_MW"]
663+
df = tools.get_dataframe("dispatch.csv", drop_scenario_info=False)[cols]
664+
df = tools.transform.gen_type(df)
665+
666+
# Rename and add needed columns
667+
df["Dispatch Limit"] = df["DispatchGen_MW"] + df["Curtailment_MW"]
668+
df = df.drop("Curtailment_MW", axis=1)
669+
df = df.rename({"DispatchGen_MW": "Dispatch"}, axis=1)
670+
# Sum dispatch across all the projects of the same type and timepoint
671+
key_columns = ["timestamp", "gen_type", "scenario_name", "scenario_index"]
672+
df = df.groupby(key_columns, as_index=False).sum()
673+
df = df.melt(id_vars=key_columns, value_vars=["Dispatch", "Dispatch Limit"], var_name="Type")
674+
df = df.rename({"gen_type": "Source"}, axis=1)
675+
676+
discharge = df[(df["Source"] == "Storage") & (df["Type"] == "Dispatch")].drop(["Source", "Type"], axis=1).rename(
677+
{"value": "discharge"}, axis=1)
678+
679+
# Get load dataframe
680+
load = tools.get_dataframe("load_balance.csv", drop_scenario_info=False)
681+
load = load.drop("normalized_energy_balance_duals_dollar_per_mwh", axis=1)
682+
683+
# Sum load across all the load zones
684+
key_columns = ["timestamp", "scenario_name", "scenario_index"]
685+
load = load.groupby(key_columns, as_index=False).sum()
686+
687+
# Subtract storage dispatch from generation and add it to the storage charge to get net flow
688+
load = load.merge(
689+
discharge,
690+
how="left",
691+
on=key_columns,
692+
validate="one_to_one"
693+
)
694+
load["ZoneTotalCentralDispatch"] -= load["discharge"]
695+
load["StorageNetCharge"] += load["discharge"]
696+
load = load.drop("discharge", axis=1)
697+
698+
# Rename and convert from wide to long format
699+
load = load.rename({
700+
"ZoneTotalCentralDispatch": "Total Generation (excl. storage discharge)",
701+
"TXPowerNet": "Transmission Losses",
702+
"StorageNetCharge": "Storage Net Flow",
703+
"zone_demand_mw": "Demand",
704+
}, axis=1).sort_index(axis=1)
705+
load = load.melt(id_vars=key_columns, var_name="Source")
706+
load["Type"] = "Dispatch"
707+
708+
# Merge dispatch contributions with load contributions
709+
df = pd.concat([load, df])
710+
711+
# Add the timestamp information and make period string to ensure it doesn't mess up the graphing
712+
df = tools.transform.timestamp(df).astype({"period": str})
713+
714+
# Convert to TWh (incl. multiply by timepoint duration)
715+
df["value"] *= df["tp_duration"] / 1e6
716+
717+
FREQUENCY = "1W"
718+
719+
def groupby_time(df):
720+
return df.groupby([
721+
"scenario_name",
722+
"period",
723+
"Source",
724+
"Type",
725+
tools.pd.Grouper(key="datetime", freq=FREQUENCY, origin="start")
726+
])["value"]
727+
728+
df = groupby_time(df).sum().reset_index()
729+
730+
# Get the state of charge data
731+
soc = tools.get_dataframe("StateOfCharge.csv", dtype={"STORAGE_GEN_TPS_1": str}, drop_scenario_info=False)
732+
soc = soc.rename({"STORAGE_GEN_TPS_2": "timepoint", "StateOfCharge": "value"}, axis=1)
733+
# Sum over all the projects that are in the same scenario with the same timepoint
734+
soc = soc.groupby(["timepoint", "scenario_name"], as_index=False).sum()
735+
soc["Source"] = "State Of Charge"
736+
soc["value"] /= 1e6 # Convert to TWh
737+
738+
# Group by time
739+
soc = tools.transform.timestamp(soc, use_timepoint=True, key_col="timepoint").astype({"period": str})
740+
soc["Type"] = "Dispatch"
741+
soc = groupby_time(soc).mean().reset_index()
742+
743+
# Add state of charge to dataframe
744+
df = pd.concat([df, soc])
745+
# Add column for day since that's what we really care about
746+
df["day"] = df["datetime"].dt.dayofyear
747+
748+
# Plot
749+
# Get the colors for the lines
750+
colors = tools.get_colors()
751+
colors.update({
752+
"Transmission Losses": "brown",
753+
"Storage Net Flow": "cadetblue",
754+
"Demand": "black",
755+
"Total Generation (excl. storage discharge)": "black",
756+
"State Of Charge": "green"
757+
})
758+
759+
# plot
760+
num_periods = df["period"].nunique()
761+
pn = tools.pn
762+
plot = pn.ggplot(df) + \
763+
pn.geom_line(pn.aes(x="day", y="value", color="Source", linetype="Type")) + \
764+
pn.facet_grid("period ~ scenario_name") + \
765+
pn.labs(y="Contribution to Energy Balance (TWh)") + \
766+
pn.scales.scale_color_manual(values=colors, aesthetics="color", na_value=colors["Other"]) + \
767+
pn.scales.scale_x_continuous(
768+
name="Month",
769+
labels=["J", "F", "M", "A", "M", "J", "J", "A", "S", "O", "N", "D"],
770+
breaks=(15, 46, 76, 106, 137, 167, 198, 228, 259, 289, 319, 350),
771+
limits=(0, 366)) + \
772+
pn.scales.scale_linetype_manual(
773+
values={"Dispatch Limit": "dotted", "Dispatch": "solid"}
774+
) + \
775+
pn.theme(
776+
figure_size=(pn.options.figure_size[0] * tools.num_scenarios, pn.options.figure_size[1] * num_periods))
777+
778+
tools.save_figure(plot.draw())
652779

653780
@graph(
654781
"curtailment_per_period",

switch_model/generators/extensions/storage.py

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -429,7 +429,7 @@ def graph_state_of_charge(tools):
429429
df = tools.get_dataframe("storage_dispatch")
430430
df = df.groupby(["timepoint", "scenario_name"], as_index=False)["StateOfCharge"].sum()
431431
# Add datetime information
432-
df = tools.transform.timestamp(df, timestamp_col="timepoint")
432+
df = tools.transform.timestamp(df, key_col="timepoint")
433433
# Count num rows
434434
num_periods = len(df["period"].unique())
435435

@@ -445,16 +445,24 @@ def graph_state_of_charge(tools):
445445
how="left"
446446
)
447447

448-
# Convert values to GWh
449-
df["StateOfCharge"] /= 1e3
450-
df["OnlineEnergyCapacityMWh"] /= 1e3
448+
# Convert values to TWh
449+
df["StateOfCharge"] /= 1e6
450+
df["OnlineEnergyCapacityMWh"] /= 1e6
451+
452+
# Determine information for the label
453+
y_axis_lim = df["OnlineEnergyCapacityMWh"].max()
454+
offset = y_axis_lim * 0.05
455+
df["label_position"] = df["OnlineEnergyCapacityMWh"] + offset
456+
df["label"] = df["OnlineEnergyCapacityMWh"].round(decimals=2)
457+
label_x_pos = df["datetime"].median()
451458

452459
# Plot with plotnine
453460
pn = tools.pn
454461
plot = pn.ggplot(df, pn.aes(x="datetime", y="StateOfCharge")) \
455462
+ pn.geom_line() \
456-
+ pn.labs(y="State of Charge (GWh)", x="Time of Year") \
457-
+ pn.geom_hline(pn.aes(yintercept="OnlineEnergyCapacityMWh"), linetype="dashed", color='blue')
463+
+ pn.labs(y="State of Charge (TWh)", x="Time of Year") \
464+
+ pn.geom_hline(pn.aes(yintercept="OnlineEnergyCapacityMWh"), linetype="dashed", color='blue') \
465+
+ pn.geom_text(pn.aes(label="label", x=label_x_pos, y="label_position"), fontweight="light", size="10")
458466

459467
tools.save_figure(by_scenario_and_period(tools, plot, num_periods).draw())
460468

@@ -477,7 +485,7 @@ def graph_state_of_charge_per_duration(tools):
477485
# Get the total state of charge at each timepoint for each project
478486
df = tools.get_dataframe("storage_dispatch")[
479487
["generation_project", "timepoint", "StateOfCharge", "scenario_name"]]
480-
df = tools.transform.timestamp(df, timestamp_col="timepoint")
488+
df = tools.transform.timestamp(df, key_col="timepoint")
481489

482490
# Add the capacity information to the state of charge information
483491
df = df.merge(
@@ -489,7 +497,9 @@ def graph_state_of_charge_per_duration(tools):
489497
df = df.groupby(["duration", "scenario_name", "datetime", "period"], as_index=False)[
490498
["StateOfCharge", "OnlineEnergyCapacityMWh"]].sum()
491499
# Convert to GWh
492-
df["StateOfCharge"] /= 1e3
500+
# df["StateOfCharge"] /= 1e3
501+
# Convert to percent
502+
df["StateOfCharge"] /= df["OnlineEnergyCapacityMWh"]
493503

494504
# Plot with plotnine
495505
pn = tools.pn
@@ -510,7 +520,7 @@ def graph_dispatch_cycles(tools):
510520
# Aggregate by timepoint
511521
df = df.groupby("timepoint", as_index=False).sum()
512522
# Add datetime column
513-
df = tools.transform.timestamp(df, timestamp_col="timepoint")
523+
df = tools.transform.timestamp(df, key_col="timepoint")
514524
# Find charge in GWh
515525
df["StateOfCharge"] /= 1e3
516526

@@ -538,7 +548,12 @@ def graph_dispatch_cycles(tools):
538548
title="Storage cycle duration based on fourier transform"
539549
" of state of charge")
540550
ax.semilogx(1 / xfreq, yfreq)
551+
# Plot some key cycle lengths
552+
ax.axvline(24, linestyle="dotted", label="24 hours", color="red") # A day
553+
ax.axvline(24 * 21, linestyle="dotted", label="3 weeks", color="green") # 3 weeks
554+
ax.axvline(24 * 182.5, linestyle="dotted", label="1/2 Year", color="purple")
541555
ax.set_xlabel("Hours per cycle")
556+
ax.legend()
542557
ax.grid(True, which="both", axis="x")
543558

544559

@@ -556,6 +571,7 @@ def graph_buildout(tools):
556571
df = df[df["IncrementalPowerCapacityMW"] != 0]
557572
df["duration"] = df["IncrementalEnergyCapacityMWh"] / df["IncrementalPowerCapacityMW"]
558573
df["power"] = df["IncrementalPowerCapacityMW"] / 1e3
574+
df["energy"] = df["IncrementalEnergyCapacityMWh"] / 1e3
559575
df = tools.transform.build_year(df)
560576
pn = tools.pn
561577
num_regions = len(df["region"].unique())
@@ -576,6 +592,14 @@ def graph_buildout(tools):
576592
tools.save_figure(by_scenario(tools, plot).draw(), "storage_duration_histogram")
577593
tools.save_figure(by_scenario_and_region(tools, plot, num_regions).draw(), "storage_duration_histogram_by_region")
578594

595+
plot = pn.ggplot(df, pn.aes(x="duration")) \
596+
+ pn.geom_histogram(pn.aes(weight="energy"), binwidth=5) \
597+
+ pn.labs(title="Storage Duration Histogram", x="Duration (h)", y="Energy Capacity (GWh)")
598+
599+
tools.save_figure(by_scenario(tools, plot).draw(), "storage_duration_histogram_by_energy")
600+
tools.save_figure(by_scenario_and_region(tools, plot, num_regions).draw(), "storage_duration_histogram_by_region_and_energy")
601+
602+
579603

580604
def by_scenario(tools, plot):
581605
pn = tools.pn

0 commit comments

Comments
 (0)