Skip to content

Commit f3e95be

Browse files
pesapstaadecker
authored andcommitted
Merge branch 'wecc' of github.com:RAEL-Berkeley/switch into wecc
2 parents 2357c4c + d189392 commit f3e95be

File tree

22 files changed

+1615
-802
lines changed

22 files changed

+1615
-802
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
@@ -76,7 +76,10 @@ def read(*rnames):
7676
"gurobipy", # used to provided python bindings for Gurobi for faster solving
7777
"pyyaml", # used to read configurations for switch
7878
"matplotlib",
79-
"seaborn"
79+
"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: 12 additions & 7 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"
@@ -282,9 +283,14 @@ def get_component_per_year(m, z, p, component):
282283
)
283284

284285

286+
@graph(
287+
"energy_balance_duals",
288+
title="Energy balance duals per period",
289+
note="Note: Outliers and zero-valued duals are ignored.",
290+
)
285291
def graph(tools):
286-
load_balance = tools.get_dataframe(csv="load_balance")
287-
load_balance = tools.add_timestamp_info(load_balance)
292+
load_balance = tools.get_dataframe("load_balance.csv")
293+
load_balance = tools.transform.timestamp(load_balance)
288294
load_balance["energy_balance_duals"] = (
289295
tools.pd.to_numeric(
290296
load_balance["normalized_energy_balance_duals_dollar_per_mwh"],
@@ -294,13 +300,12 @@ def graph(tools):
294300
)
295301
load_balance = load_balance[["energy_balance_duals", "time_row"]]
296302
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)
297305
if load_balance.count().sum() != 0:
298-
ax = tools.get_new_axes(
299-
"energy_balance_duals", title="Energy balance duals per period"
300-
)
301306
load_balance.plot.box(
302-
ax=ax,
307+
ax=tools.get_axes(),
303308
xlabel="Period",
304309
ylabel="Energy balance duals (cents/kWh)",
305-
logy=True,
310+
showfliers=False,
306311
)

switch_model/financials.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import os
1212
import pandas as pd
1313
from switch_model.reporting import write_table
14+
from switch_model.tools.graph import graph
1415

1516
dependencies = "switch_model.timescales"
1617

@@ -337,7 +338,6 @@ def load_inputs(mod, switch_data, inputs_dir):
337338
def post_solve(instance, outdir):
338339
m = instance
339340
# Overall electricity costs
340-
# TODO use write_table
341341
normalized_dat = [
342342
{
343343
"PERIOD": p,
@@ -401,16 +401,28 @@ def post_solve(instance, outdir):
401401
write_table(instance, output_file=os.path.join(outdir, "costs_itemized.csv"), df=df)
402402

403403

404+
@graph("costs", title="Itemized costs per period", supports_multi_scenario=True)
404405
def graph(tools):
405-
costs_itemized = tools.get_dataframe(csv="costs_itemized")
406+
costs_itemized = tools.get_dataframe("costs_itemized.csv")
406407
# Remove elements with zero cost
407408
costs_itemized = costs_itemized[costs_itemized["AnnualCost_Real"] != 0]
409+
groupby = "PERIOD" if tools.num_scenarios == 1 else ["PERIOD", "scenario_name"]
408410
costs_itemized = costs_itemized.pivot(
409-
columns="Component", index="PERIOD", values="AnnualCost_Real"
411+
columns="Component", index=groupby, values="AnnualCost_Real"
412+
)
413+
costs_itemized *= 1e-9 # Converting to billions
414+
costs_itemized = costs_itemized.rename(
415+
{
416+
"GenVariableOMCostsInTP": "Variable O & M Generation Costs",
417+
"FuelCostsPerPeriod": "Fuel Costs",
418+
"StorageEnergyFixedCost": "Storage Energy Capacity Costs",
419+
"TotalGenFixedCosts": "Generation Fixed Costs",
420+
"TxFixedCosts": "Transmission Costs",
421+
},
422+
axis=1,
410423
)
411-
costs_itemized *= 1e-9
412424
costs_itemized = costs_itemized.sort_values(axis=1, by=costs_itemized.index[-1])
413-
ax = tools.get_new_axes(out="costs", title="Itemized costs per period")
425+
ax = tools.get_axes()
414426
costs_itemized.plot(
415427
ax=ax,
416428
kind="bar",

0 commit comments

Comments
 (0)