Skip to content

Commit 38c64f4

Browse files
committed
Merge remote-tracking branch 'rael/wecc' into hydro_scenario
2 parents ec0d0a7 + d84a114 commit 38c64f4

File tree

12 files changed

+667
-140
lines changed

12 files changed

+667
-140
lines changed

docs/Performance.md

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# Performance
2+
3+
Memory use and solve time are two important factors that we try to keep to a minimum in our models. There are multiple
4+
things one can do to improve performance.
5+
6+
## Solving methods
7+
8+
By far the biggest factor that impacts performance is the method used by Gurobi. The fastest method is barrier solve
9+
without crossover (use `--recommended-fast`)
10+
however this method often returns a suboptimal solution. The next fastest is barrier solve followed by crossover and
11+
simplex (use `--recommended`) which almost always works. In some cases barrier solve encounters numerical issues (
12+
see [`Numerical Issues.md`](./Numerical%20Issues.md))
13+
in which case the slower Simplex method must be used (`--solver-options-string method=1`).
14+
15+
## Solver interface
16+
17+
Solver interfaces are how Pyomo communicates with Gurobi (or any solver).
18+
19+
There are two solver interfaces that you should know about: `gurobi` and `gurobi_direct`.
20+
21+
- When using `gurobi`, Pyomo will write the entire model to a temporary text file and then start a *separate Gurobi
22+
process* that will read the file, solve the model and write the results to another temporary text file. Once Gurobi
23+
finishes writing the results Pyomo will read the results text file and load the results back into the Python program
24+
before running post_solve (e.g. generate csv files, create graphs, etc). Note that these temporary text files are
25+
stored in `/tmp` but if you use `--recommended-debug` Pyomo and Gurobi will instead use a `temp` folder in your model.
26+
27+
- `gurobi_direct` uses Gurobi's Python library to create and solve the model directly in Python without the use of
28+
intermediate text files.
29+
30+
In theory `gurobi_direct` should be faster and more efficient however in practice we find that that's not the case. As
31+
such we recommend using `gurobi` and all our defaults do so. If someone has the time they could profile `gurobi_direct`
32+
to improve performance at which point we could make `gurobi_direct` the default (and enable `--save-warm-start` by default, see below).
33+
34+
The `gurobi` interface has the added advantage of separating Gurobi and Pyomo into separate threads. This means that
35+
while Gurobi is solving and Pyomo is idle, the operating system can automatically move Pyomo's memory usage
36+
to [virtual memory](https://serverfault.com/questions/48486/what-is-swap-memory)
37+
which will free up more memory for Gurobi.
38+
39+
## Warm starting
40+
41+
Warm starting is the act of using a solution from a previous similar model to start the solver closer to your expected
42+
solution. Theoretically this can help performance however in practice there are several limitations. For this section, *
43+
previous solution* refers to the results from an already solved model that you are using to warm start the solver. *
44+
Current solution* refers to the solution you are trying to find while using the warm start feature.
45+
46+
- To warm start a model use `switch solve --warm-start <path_to_previous_solution>`.
47+
48+
- Warm starting only works if the previous solution does not break any constraints of the current solution. This usually
49+
only happens if a) the model has the exact same set of variables b)
50+
the previous solution was "harder" (e.g. it had more constraints to satisfy).
51+
52+
- Warm starting always uses the slower Simplex method. This means unless you expect the previous solution and current
53+
solution to be very similar, it may be faster to solve without warm start using the barrier method.
54+
55+
- If your previous solution didn't use crossover (e.g. you used `--recommended-fast`) then warm starting will be even
56+
slower since the solver will need to first run crossover before warm starting.
57+
58+
- Our implementation of warm starting only works if your previous solution has an `outputs/warm_start.pickle`
59+
file. This file is only generated when you use `--save-warm-start`.
60+
61+
- `--save-warm-start` and `--warm-start` both use an extension of the `gurobi_direct` solver interface which is
62+
generally slower than the `gurobi` solver interface (see section above).
63+
64+
## Tools for improving performance
65+
66+
- [Memory profiler](https://pypi.org/project/memory-profiler/) for generating plots of the memory
67+
use over time. Use `mprof run --interval 60 --multiprocess switch solve ...` and once solving is done
68+
run `mprof plot -o profile.png` to make the plot.
69+
70+
- [Fil Profiler](https://pypi.org/project/filprofiler/) is an amazing tool for seeing which parts of the code are
71+
using up memory during peak memory usage.
72+
73+
- Using `switch_model.utilities.StepTimer` to measure how long certain code blocks take to run. See examples
74+
throughout the code.

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/build.py

Lines changed: 8 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -720,22 +720,7 @@ def post_solve(m, outdir):
720720
)
721721

722722

723-
<<<<<<< HEAD
724-
def graph(tools):
725-
graph_capacity(tools)
726-
graph_buildout_per_tech(tools)
727-
728-
729-
def compare(tools):
730-
graph_buildout(tools)
731-
732-
733-
=======
734-
@graph(
735-
"generation_capacity_per_period",
736-
title="Online Generation Capacity Per Period"
737-
)
738-
>>>>>>> b3590fdb (Redesign graphing API)
723+
@graph("generation_capacity_per_period", title="Online Generation Capacity Per Period")
739724
def graph_capacity(tools):
740725
# Load gen_cap.csv
741726
gen_cap = tools.get_dataframe("gen_cap.csv")
@@ -768,33 +753,25 @@ def graph_capacity(tools):
768753

769754
# Plot
770755
# Get a new set of axis to create a breakdown of the generation capacity
771-
<<<<<<< HEAD
772-
ax = tools.get_axes(
773-
out="generation_capacity_per_period",
774-
title="Online generating capacity by period",
775-
)
776756
capacity_df.plot(
777757
kind="bar",
778-
ax=ax,
779-
=======
780-
capacity_df.plot(
781-
kind='bar',
782758
ax=tools.get_axes(),
783-
>>>>>>> b3590fdb (Redesign graphing API)
784759
stacked=True,
785760
ylabel="Capacity Online (GW)",
786761
xlabel="Period",
787762
color=tools.get_colors(len(capacity_df.index)),
788763
)
789764

765+
tools.bar_label()
766+
790767

791768
@graph(
792769
"buildout_gen_per_period",
793770
title="Built Capacity per Period",
794-
supports_multi_scenario=True
771+
supports_multi_scenario=True,
795772
)
796773
def graph_buildout(tools):
797-
build_gen = tools.get_dataframe("BuildGen.csv")
774+
build_gen = tools.get_dataframe("BuildGen.csv", dtype={"GEN_BLD_YRS_1": str})
798775
build_gen = build_gen.rename(
799776
{
800777
"GEN_BLD_YRS_1": "GENERATION_PROJECT",
@@ -804,13 +781,7 @@ def graph_buildout(tools):
804781
axis=1,
805782
)
806783
build_gen = tools.transform.build_year(build_gen)
807-
<<<<<<< HEAD
808-
gen = tools.get_dataframe(
809-
"generation_projects_info", from_inputs=True, all_scenarios=True
810-
)
811-
=======
812784
gen = tools.get_dataframe("generation_projects_info", from_inputs=True)
813-
>>>>>>> b3590fdb (Redesign graphing API)
814785
gen = tools.transform.gen_type(gen)
815786
gen = gen[["GENERATION_PROJECT", "gen_type", "scenario_name"]]
816787
build_gen = build_gen.merge(
@@ -845,33 +816,22 @@ def graph_buildout(tools):
845816

846817
# Plot
847818
# Get a new set of axis to create a breakdown of the generation capacity
848-
<<<<<<< HEAD
849-
ax = tools.get_axes(out="buildout_per_period", title="Built capacity per period")
850819
build_gen.plot(
851820
kind="bar",
852-
ax=ax,
853-
=======
854-
build_gen.plot(
855-
kind='bar',
856821
ax=tools.get_axes(),
857-
>>>>>>> b3590fdb (Redesign graphing API)
858822
stacked=True,
859823
ylabel="Capacity Online (GW)",
860824
xlabel="Period",
861825
color=tools.get_colors(len(build_gen.index)),
862826
)
863-
<<<<<<< HEAD
864-
865-
=======
866-
>>>>>>> b3590fdb (Redesign graphing API)
867827

868828

869829
@graph(
870830
"gen_buildout_per_tech_period",
871831
title="Buildout relative to max allowed for period",
872832
note="\nNote 1: This graph excludes predetermined buildout and projects that have no capacity limit."
873-
"\nTechnologies that contain projects with no capacity limit are marked by a * and their graphs may"
874-
"be misleading."
833+
"\nTechnologies that contain projects with no capacity limit are marked by a * and their graphs may"
834+
"be misleading.",
875835
)
876836
def graph_buildout_per_tech(tools):
877837
# Load gen_cap.csv
@@ -920,32 +880,16 @@ def graph_buildout_per_tech(tools):
920880
# Set the name of the legend.
921881
df = df.rename_axis("Type", axis="columns")
922882
# Add a * to tech
923-
<<<<<<< HEAD
924883
df = df.rename(
925884
lambda c: f"{c}*" if c in unlimited_gen_types.values else c, axis="columns"
926885
)
927-
# Get axes to graph on
928-
ax = tools.get_axes(
929-
out="gen_buildout_per_tech_no_pred",
930-
title="Buildout relative to max allowed for period",
931-
note="\nNote 1: This graph excludes predetermined buildout and projects that have no capacity limit."
932-
"\nTechnologies that contain projects with no capacity limit are marked by a * and their graphs may"
933-
"be misleading.",
934-
)
935-
=======
936-
df = df.rename(lambda c: f"{c}*" if c in unlimited_gen_types.values else c, axis='columns')
937-
>>>>>>> b3590fdb (Redesign graphing API)
938886
# Plot
939887
colors = tools.get_colors()
940888
if colors is not None:
941889
# Add the same colors but with a * to support our legend.
942890
colors.update({f"{k}*": v for k, v in colors.items()})
943-
<<<<<<< HEAD
944-
df.plot(ax=ax, kind="line", color=colors, xlabel="Period", marker="x")
945-
=======
946891
ax = tools.get_axes()
947-
df.plot(ax=ax, kind='line', color=colors, xlabel='Period', marker="x")
948-
>>>>>>> b3590fdb (Redesign graphing API)
892+
df.plot(ax=ax, kind="line", color=colors, xlabel="Period", marker="x")
949893
# Set the y-axis to use percent
950894
ax.yaxis.set_major_formatter(tools.mplt.ticker.PercentFormatter(1.0))
951895
# Horizontal line at 100%

0 commit comments

Comments
 (0)