Skip to content

Commit 87f0b94

Browse files
pesapstaadecker
authored andcommitted
Merge branch 'wecc' of github.com:RAEL-Berkeley/switch into wecc
2 parents 959ee10 + 91c44c8 commit 87f0b94

File tree

22 files changed

+2087
-776
lines changed

22 files changed

+2087
-776
lines changed

docs/Graphs.md

Lines changed: 132 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -18,92 +18,128 @@ and `ca_policies` have already been solved).
1818

1919
## Adding new graphs
2020

21-
Graphs can be defined in any module by adding the following function to the file.
22-
21+
New graphs can be added with the `@graph(...)` annotation.
2322
```python
24-
def graph(tools):
23+
from switch_model.tools.graph import graph
24+
25+
@graph(
26+
name="my_custom_graph",
27+
title="An example plot",
28+
note="Some optional note to add below the graph",
29+
# Other options are possible see code documentation
30+
)
31+
def my_graphing_function(tools):
2532
# Your graphing code
2633
...
2734
```
2835

29-
In `graph()` you can use the `tools` object to create graphs. Here are some important methods.
30-
31-
- `tools.get_dataframe(csv=filename)` will return a pandas dataframe for the file called `filename`. You can also
32-
specify `folder=tools.folders.INPUTS` to load a csv from the inputs directory.
33-
34-
- `tools.get_new_axes(out, title, note)` will return a matplotlib axes. This should be the axes used while
35-
graphing. `out` is the name of the `.png` file that will be created with this graph. `title` and `note` are optional
36-
and will be the title and footnote for the graph.
37-
38-
- `tools.pd`, `tools.sns`, `tools.np`, `tools.mplt` are references to the pandas, seaborn, numpy and matplotlib
39-
libraries. This is useful if your graphing code needs to access these libraries since it doesn't require adding an
36+
In `my_graphing_function()` you can use the `tools` object to create graphs. Here are some important methods.
37+
38+
- `tools.get_dataframe(filename)` will return a pandas dataframe for the file called `filename`. You can also
39+
specify `from_inputs=True` to load a csv from the inputs directory.
40+
41+
- `tools.get_axes()` or `tools.get_figure()` will return a matplotlib axes or figure
42+
that should be used while graphing. When possible, always use `get_axes` instead of `get_figure` since
43+
this allows plots from different scenarios to share the same figure.
44+
45+
- `tools.save_figure(fig)`. Some libraries (e.g. plotnine)
46+
always generate their own figures. In this case we can add the figure
47+
to our outputs with this function. When possible, use `tools.get_axes()` instead.
48+
49+
- `tools.pd`, `tools.sns`, `tools.np`, `tools.mplt`, `tools.pn` are references to the pandas, seaborn, numpy, matplotlib
50+
and plotnine graphing libraries. This is useful if your graphing code needs to access these libraries since it doesn't require adding an
4051
import to your file.
4152

42-
- `tools.add_gen_type_column(df)` adds a column called `gen_type` to a dataframe with columns
43-
`gen_tech` and `gen_energy_source`. `gen_type` is a user-friendly name for the technology (e.g. Nuclear instead of
44-
Uranium). The mapping of `gen_energy_source` and `gen_tech` to `gen_type` is defined in
45-
a `inputs/graph_tech_types.csv`. If this file isn't present, a default mapping will be used. You can also use other
46-
mappings found in `graph_tech_types.csv` by specifying `map_name=` when calling `add_gen_type_column()`.
47-
53+
- `tools.transform` is a reference to a `TransformTools` object that provides
54+
useful helper methods for modyfing a dataframe for graphing. Full documentation
55+
can be found in the `TransformTools` class but some examples include.
56+
57+
- `tools.transform.build_year(df)` which will convert build years that aren't
58+
a period to the string `Pre-existing`.
59+
60+
- `tools.transform.gen_type(df)` which adds a column called `gen_type` to the dataframe.
61+
`gen_type` is a user-friendly name for the technology (e.g. Nuclear instead of
62+
Uranium) and is determined using the mappings in `inputs/graph_tech_types.csv`.
63+
64+
- `tools.transform.timestamp(df)`: which adds columns such as the hour, the timestamp in datetime format
65+
in the correct timezone, etc.
66+
67+
- `tools.transform.load_zone(df)`: Adds a column called 'region' to the dataframe which
68+
normally corresponds to the load zone state.
69+
4870
- `tools.get_colors()` returns a mapping of `gen_type` to its color. This is useful for graphing and can normally be
49-
passed straight to `color=` in standard plotting libraries. You can also specify a different color mapping using a
50-
similar process to above (`map_name=`)
71+
passed straight to `color=` in standard plotting libraries. The color mapping is based on `inputs/graph_tech_colors.csv`.
5172

5273
## Adding a comparison graph
5374

54-
By default, `tools.get_dataframe` will return the data for only one scenario (the one you are graphing).
55-
56-
Sometimes, you may wish to create a graph that compares multiple scenarios. To do this create a function
57-
called `compare`.
75+
Sometimes you may want to create graphs that compare data from multiple scenarios.
76+
To do this, add `supports_multi_scenario=True` inside the `@graph()` decorator.
5877

5978
```python
60-
def compare(tools):
61-
# Your graphing code
62-
...
79+
from switch_model.tools.graph import graph
80+
81+
@graph(
82+
name="my_custom_comparison_graph",
83+
title="My Comparison plot",
84+
supports_multi_scenario=True,
85+
# Instead of supports_multi_scenario, you can use
86+
# requires_multi_scenario if you want the graphing function
87+
# to *only* be run when we have multiple scenarios.
88+
# requires_multi_scenario=True,
89+
)
90+
def my_graphing_comparison_function(tools):
91+
# Read data from all the scenarios
92+
df = tools.get_dataframe("some_file.csv")
93+
# Plot data
94+
...
6395
```
6496

65-
If you call `tools.get_dataframe(...)` from within `compare`, then
66-
`tools.get_dataframe` will return a dataframe containing the data from *all*
67-
the scenarios. The dataframe will contain a column called `scenario` to indicate which rows correspond to which
68-
scenarios. You can then use this column to create a graph comparing the different scenarios (still
69-
using `tools.get_new_axes`).
97+
Now everytime you call `tools.get_dataframe(filename)`, data for *all* the scenarios
98+
gets returned. The way this works is that the
99+
returned dataframe will contain a column called `scenario_name`
100+
to indicate which rows correspond to which scenarios.
101+
You can then use this column to create a graph comparing the different scenarios (still
102+
using `tools.get_axes`).
70103

71-
At this point, when you run `switch compare`, your `compare(tools)` function will be called and your comparison graph
104+
At this point, when you run `switch compare`, your `my_graphing_comparison_function` function will be called and your comparison graph
72105
will be generated.
73106

74107
## Example
75108

76109
In this example we create a graph that shows the power capacity during each period broken down by technology.
77110

78111
```python
112+
from switch_model.tools.graph import graph
113+
114+
@graph(
115+
"capacity_per_period",
116+
title="Capacity per period"
117+
)
79118
def graph(tools):
80-
# Get a dataframe of gen_cap.csv
81-
gen_cap = tools.get_dataframe(csv="gen_cap")
82-
83-
# Add a 'gen_type' column to your dataframe
84-
gen_cap = tools.add_gen_type_column(gen_cap)
85-
86-
# Aggregate the generation capacity by gen_type and PERIOD
87-
capacity_df = gen_cap.pivot_table(
88-
index='PERIOD',
89-
columns='gen_type',
90-
values='GenCapacity',
91-
aggfunc=tools.np.sum,
92-
fill_value=0 # Missing values become 0
93-
)
94-
95-
# Get a new pair of axis to plot onto
96-
ax = tools.get_new_axes(out="capacity_per_period")
97-
98-
# Plot
99-
capacity_df.plot(
100-
kind='bar',
101-
ax=ax, # Notice we pass in the axis
102-
stacked=True,
103-
ylabel="Capacity Online (MW)",
104-
xlabel="Period",
105-
color=tools.get_colors(len(capacity_df.index))
106-
)
119+
# Get a dataframe of gen_cap.csv
120+
df = tools.get_dataframe("gen_cap.csv")
121+
122+
# Add a 'gen_type' column to your dataframe
123+
df = tools.transform.gen_type(df)
124+
125+
# Aggregate the generation capacity by gen_type and PERIOD
126+
df = df.pivot_table(
127+
index='PERIOD',
128+
columns='gen_type',
129+
values='GenCapacity',
130+
aggfunc=tools.np.sum,
131+
fill_value=0 # Missing values become 0
132+
)
133+
134+
# Plot
135+
df.plot(
136+
kind='bar',
137+
ax=tools.get_axes(),
138+
stacked=True,
139+
ylabel="Capacity Online (MW)",
140+
xlabel="Period",
141+
color=tools.get_colors(len(df.index))
142+
)
107143
```
108144

109145
Running `switch graph` would run the `graph()` function above and create
@@ -112,3 +148,37 @@ Running `switch graph` would run the `graph()` function above and create
112148
Running `switch compare` would create `capacity_per_period.png` containing
113149
your plot side-by-side with the same plot but for the scenario you're comparing to.
114150

151+
### Testing your graphs
152+
153+
To test your graphs, you can run `switch graph` or `switch compare`. However,
154+
this takes quite some time. If you want to test just one graphing function
155+
you can run `switch graph/compare -f FIGURE`. This will run only the graphing function
156+
you've defined. Here `FIGURE` should be the name of the graph (the first
157+
argument in `@graph()`, so `capacity_per_period` in the example above).
158+
159+
### Creating graphs outside of SWITCH
160+
161+
Sometimes you may want to create graphs but don't want to permently add
162+
them to the switch code. To do this create the following Python file anywhere
163+
on your computer.
164+
165+
```python
166+
from switch_model.tools.graph import graph
167+
from switch_model.tools.graph.cli_graph import main as graph
168+
169+
@graph(
170+
...
171+
)
172+
def my_first_graph(tools):
173+
...
174+
175+
@graph(
176+
...
177+
)
178+
def my_second_graph(tools):
179+
...
180+
181+
if __name__=="__main__":
182+
graph(["--ignore-modules-txt"])
183+
```
184+
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
128823582.05
1+
128823582.05

setup.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,14 +69,17 @@ def read(*rnames):
6969
],
7070
python_requires=">=3.7",
7171
install_requires=[
72-
"Pyomo>=6.0",
72+
"Pyomo>=6.0", # We need a version that works with glpk 4.60+
7373
"pint", # needed by Pyomo when we run our tests, but not included
7474
"testfixtures", # used for standard tests
7575
"pandas", # used for input upgrades and testing that functionality
7676
"gurobipy", # used to provided python bindings for Gurobi for faster solving
7777
"pyyaml", # used to read configurations for switch
7878
"matplotlib",
7979
"seaborn",
80+
"plotnine",
81+
"scipy",
82+
"pillow", # Image processing to make plots stick together
8083
],
8184
extras_require={
8285
# packages used for advanced demand response, progressive hedging

switch_model/__main__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,9 @@ def main():
6262
elif cmd == "new":
6363
from switch_model.tools.new import main
6464
elif cmd == "graph":
65-
from switch_model.tools.graphing.graph import main
65+
from switch_model.tools.graph.cli_graph import main
6666
elif cmd == "compare":
67-
from switch_model.tools.graphing.compare import main
67+
from switch_model.tools.graph.cli_compare import main
6868
main()
6969
else:
7070
print(

switch_model/balancing/load_zones.py

Lines changed: 38 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import os
88
from pyomo.environ import *
99
from switch_model.reporting import write_table
10+
from switch_model.tools.graph import graph
1011

1112
dependencies = "switch_model.timescales"
1213
optional_dependencies = "switch_model.transmission.local_td"
@@ -112,9 +113,8 @@ def define_components(mod):
112113
),
113114
)
114115

115-
# Make sure the model has a dual suffix since we use the duals in post_solve()
116-
if not hasattr(mod, "dual"):
117-
mod.dual = Suffix(direction=Suffix.IMPORT)
116+
# Make sure the model has duals enabled since we use the duals in post_solve()
117+
mod.enable_duals()
118118

119119

120120
def define_dynamic_components(mod):
@@ -209,37 +209,6 @@ def post_solve(instance, outdir):
209209
load_balance_annual.csv contains the energy injections and withdrawals
210210
throughout a year across all zones.
211211
"""
212-
213-
def get_load_balance_row(m, z, t):
214-
# Add index to row
215-
row = [z, m.tp_timestamp[t]]
216-
217-
# If duals are available, add the duals for the energy balance constraint
218-
# as a proxy for LMP
219-
if not m.has_discrete_variables and m.Zone_Energy_Balance[z, t] in m.dual:
220-
# We need to divide by the timepoint weight since the dual gives us
221-
# the cost of responding to an extra 1 MW of demand in that zone
222-
# for that timepoint. However, the cost during that timepoint gets
223-
# scaled up to fill part of the period. So if we want the cost
224-
# for just one hour we need to divide by the number of hours
225-
# taken by that timepoint, during the period. This is m.tp_weight.
226-
# Note that this is the cost per hour for an extra MW or
227-
# equivalently the cost of providing an extra MWh.
228-
# Note: We multiply by 1000 since our objective function is in terms of thousands of dollars
229-
row.append(m.dual[m.Zone_Energy_Balance[z, t]] * 1000 / m.tp_weight[t])
230-
else:
231-
row.append(".")
232-
233-
# Add contributions to energy balance to the row
234-
row.extend(
235-
[getattr(m, component)[z, t] for component in m.Zone_Power_Injections]
236-
)
237-
row.extend(
238-
[-getattr(m, component)[z, t] for component in m.Zone_Power_Withdrawals]
239-
)
240-
241-
return row
242-
243212
write_table(
244213
instance,
245214
instance.LOAD_ZONES,
@@ -251,7 +220,18 @@ def get_load_balance_row(m, z, t):
251220
"normalized_energy_balance_duals_dollar_per_mwh",
252221
)
253222
+ tuple(instance.Zone_Power_Injections + instance.Zone_Power_Withdrawals),
254-
values=get_load_balance_row,
223+
values=lambda m, z, t: (
224+
z,
225+
m.tp_timestamp[t],
226+
m.get_dual(
227+
"Zone_Energy_Balance",
228+
z,
229+
t,
230+
divider=m.bring_timepoint_costs_to_base_year[t],
231+
),
232+
)
233+
+ tuple(getattr(m, component)[z, t] for component in m.Zone_Power_Injections)
234+
+ tuple(-getattr(m, component)[z, t] for component in m.Zone_Power_Withdrawals),
255235
)
256236

257237
def get_component_per_year(m, z, p, component):
@@ -303,12 +283,14 @@ def get_component_per_year(m, z, p, component):
303283
)
304284

305285

286+
@graph(
287+
"energy_balance_duals",
288+
title="Energy balance duals per period",
289+
note="Note: Outliers and zero-valued duals are ignored."
290+
)
306291
def graph(tools):
307-
load_balance = tools.get_dataframe(csv="load_balance")
308-
load_balance = tools.add_timestamp_info(load_balance)
309-
ax = tools.get_new_axes(
310-
"energy_balance_duals", title="Energy balance duals per period"
311-
)
292+
load_balance = tools.get_dataframe("load_balance.csv")
293+
load_balance = tools.transform.timestamp(load_balance)
312294
load_balance["energy_balance_duals"] = (
313295
tools.pd.to_numeric(
314296
load_balance["normalized_energy_balance_duals_dollar_per_mwh"],
@@ -318,10 +300,25 @@ def graph(tools):
318300
)
319301
load_balance = load_balance[["energy_balance_duals", "time_row"]]
320302
load_balance = load_balance.pivot(columns="time_row", values="energy_balance_duals")
303+
# Don't include the zero-valued duals
304+
load_balance = load_balance.replace(0, tools.np.nan)
321305
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+
)
322312
load_balance.plot.box(
323313
ax=ax,
324314
xlabel="Period",
325315
ylabel="Energy balance duals (cents/kWh)",
326-
logy=True,
316+
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)
327324
)

0 commit comments

Comments
 (0)