Skip to content

Commit a76ca45

Browse files
FLombbrynpickering
andauthored
Introduce SPORES in v0.7.0 (#716)
* Add working SPORES mode back into core code. * Add `set_objective` as a backend method. * Add spores scenarios to example models and add run mode comparison example notebook. * (Compared to v0.6) add config option to point to another, boolean, parameter which will define which techs, and at which nodes, the SPORES scoring should be applied. * (Compared to v0.6) add config option to choose from a selection of scoring algorithms. * (Compared to v0.6) apply SPORES score objective as a new objective, rather than an update to the base cost minimisation objective. --------- Co-authored-by: Bryn Pickering <17178478+brynpickering@users.noreply.github.com>
1 parent c2a7563 commit a76ca45

26 files changed

+964
-197
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
### User-facing changes
44

5+
|new| working SPORES mode, upgraded from v0.6 to include a selection of scoring algorithms.
6+
7+
|new| backend `set_objective` method, to switch between pre-defined objectives.
8+
59
|changed| coin-or-cbc is now available cross-platform on conda-forge and so is the recommended open-source solver to install a user environment with.
610

711
|changed| Upper bound pins for dependencies removed where possible, to minimise clashes when using calliope as a dependency in a project.

docs/advanced/mode.md

+8-16
Original file line numberDiff line numberDiff line change
@@ -65,30 +65,19 @@ For this reason, `horizon` must always be equal to or larger than `window`.
6565

6666
## SPORES mode
6767

68-
!!! warning
69-
SPORES mode has not yet been re-implemented in Calliope v0.7.
70-
7168
`SPORES` refers to Spatially-explicit Practically Optimal REsultS.
7269
This run mode allows a user to generate any number of alternative results which are within a certain range of the optimal cost.
7370
It follows on from previous work in the field of `modelling to generate alternatives` (MGA), with a particular emphasis on alternatives that vary maximally in the spatial dimension.
74-
This run mode was developed for and implemented in a [study on the future Italian energy system](https://doi.org/10.1016/j.joule.2020.08.002).
71+
This run mode was developed for and first implemented in a [study on the future Italian energy system](https://doi.org/10.1016/j.joule.2020.08.002). We later expanded it to enable greater [computational efficiency and versatility](https://doi.org/10.1016/j.apenergy.2023.121002) in what kind of alternative results are prioritised.
7572

7673
As an example, if you wanted to generate 10 SPORES, all of which are within 10% of the optimal system cost, you would define the following in your model configuration:
7774

7875
```yaml
7976
config.build.mode: spores
80-
config.solve:
81-
# The number of SPORES to generate:
82-
spores_number: 10
83-
# The cost class to optimise against when generating SPORES:
84-
spores_score_cost_class: spores_score
85-
# The initial system cost to limit the SPORES to fit within:
86-
spores_cost_max: .inf
87-
# The cost class to constrain to be less than or equal to `spores_cost_max`:
88-
spores_slack_cost_group: monetary
89-
parameters:
90-
# The fraction above the cost-optimal cost to set the maximum cost during SPORES:
91-
slack: 0.1
77+
# The number of SPORES to generate:
78+
config.solve.spores.number: 10:
79+
# The fraction above the cost-optimal cost to set the maximum cost during SPORES:
80+
parameters.spores_slack: 0.1
9281
```
9382

9483
You will now also need a `spores_score` cost class in your model.
@@ -128,3 +117,6 @@ techs:
128117
!!! note
129118
We ourselves use and recommend using `spores_score` to define the cost class that you will now optimise against.
130119
However, it is user-defined, allowing you to choose any terminology that best fits your use case.
120+
121+
To get a glimpse of how the results generated via SPORES compare to simple cost optimisation, check out our documentation
122+
on [comparing run modes](../examples/modes.py).

docs/examples/loading_tabular_data.py

-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
# ---
22
# jupyter:
33
# jupytext:
4-
# custom_cell_magics: kql
54
# text_representation:
65
# extension: .py
76
# format_name: percent

docs/examples/modes.py

+227
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
# ---
2+
# jupyter:
3+
# jupytext:
4+
# text_representation:
5+
# extension: .py
6+
# format_name: percent
7+
# format_version: '1.3'
8+
# jupytext_version: 1.16.4
9+
# kernelspec:
10+
# display_name: calliope_docs_build
11+
# language: python
12+
# name: calliope_docs_build
13+
# ---
14+
15+
# %% [markdown]
16+
# # Running models in different modes
17+
#
18+
# Models can be built and solved in different modes:
19+
20+
# - `plan` mode.
21+
# In `plan` mode, the user defines upper and lower boundaries for technology capacities and the model decides on an optimal system configuration.
22+
# In this configuration, the total cost of investing in technologies and then using them to meet demand in every _timestep_ (e.g., every hour) is as low as possible.
23+
# - `operate` mode.
24+
# In `operate` mode, all capacity constraints are fixed and the system is operated with a receding horizon control algorithm.
25+
# This is sometimes known as a `dispatch` model - we're only concerned with the _dispatch_ of technologies whose capacities are already fixed.
26+
# Optimisation is limited to a time horizon which
27+
# - `spores` mode.
28+
# `SPORES` refers to Spatially-explicit Practically Optimal REsultS.
29+
# This run mode allows a user to generate any number of alternative results which are within a certain range of the optimal cost.
30+
31+
# In this notebook we will run the Calliope national scale example model in these three modes.
32+
33+
# More detail on these modes is given in the [_advanced_ section of the Calliope documentation](https://calliope.readthedocs.io/en/latest/advanced/mode/).
34+
35+
# %%
36+
37+
import plotly.express as px
38+
import plotly.graph_objects as go
39+
import xarray as xr
40+
41+
import calliope
42+
43+
# We update logging to show a bit more information but to hide the solver output, which can be long.
44+
calliope.set_log_verbosity("INFO", include_solver_output=False)
45+
46+
# %% [markdown]
47+
# ## Running in `plan` mode.
48+
49+
# %%
50+
# We subset to the same time range as operate mode
51+
model_plan = calliope.examples.national_scale(time_subset=["2005-01-01", "2005-01-10"])
52+
model_plan.build()
53+
model_plan.solve()
54+
55+
# %% [markdown]
56+
# ## Running in `operate` mode.
57+
58+
# %%
59+
model_operate = calliope.examples.national_scale(scenario="operate")
60+
model_operate.build()
61+
model_operate.solve()
62+
63+
# %% [markdown]
64+
# Note how we have capacity variables as parameters in the inputs and only dispatch variables in the results
65+
66+
# %%
67+
model_operate.inputs[["flow_cap", "storage_cap", "area_use"]]
68+
69+
# %%
70+
model_operate.results
71+
72+
# %% [markdown]
73+
# ## Running in `spores` mode.
74+
75+
# %%
76+
# We subset to the same time range as operate/plan mode
77+
model_spores = calliope.examples.national_scale(
78+
scenario="spores", time_subset=["2005-01-01", "2005-01-10"]
79+
)
80+
model_spores.build()
81+
model_spores.solve()
82+
83+
# %% [markdown]
84+
# Note how we have a new `spores` dimension in our results.
85+
86+
# %%
87+
model_spores.results
88+
89+
# %% [markdown]
90+
# We can track the SPORES scores used between iterations using the `spores_score_cumulative` result.
91+
# This scoring mechanism is based on increasing the score of any technology-node combination where the
92+
93+
# %%
94+
# We do some prettification of the outputs
95+
model_spores.results.spores_score_cumulative.to_series().where(
96+
lambda x: x > 0
97+
).dropna().unstack("spores")
98+
99+
# %% [markdown]
100+
# ## Visualising results
101+
#
102+
# We can use [plotly](https://plotly.com/) to quickly examine our results.
103+
# These are just some examples of how to visualise Calliope data.
104+
105+
# %%
106+
# We set the color mapping to use in all our plots by extracting the colors defined in the technology definitions of our model.
107+
# We also create some reusable plotting functions.
108+
colors = model_plan.inputs.color.to_series().to_dict()
109+
110+
111+
def plot_flows(results: xr.Dataset) -> go.Figure:
112+
df_electricity = (
113+
(results.flow_out.fillna(0) - results.flow_in.fillna(0))
114+
.sel(carriers="power")
115+
.sum("nodes")
116+
.to_series()
117+
.where(lambda x: x != 0)
118+
.dropna()
119+
.to_frame("Flow in/out (kWh)")
120+
.reset_index()
121+
)
122+
df_electricity_demand = df_electricity[df_electricity.techs == "demand_power"]
123+
df_electricity_other = df_electricity[df_electricity.techs != "demand_power"]
124+
125+
fig = px.bar(
126+
df_electricity_other,
127+
x="timesteps",
128+
y="Flow in/out (kWh)",
129+
color="techs",
130+
color_discrete_map=colors,
131+
)
132+
fig.add_scatter(
133+
x=df_electricity_demand.timesteps,
134+
y=-1 * df_electricity_demand["Flow in/out (kWh)"],
135+
marker_color="black",
136+
name="demand",
137+
)
138+
return fig
139+
140+
141+
def plot_capacity(results: xr.Dataset, **plotly_kwargs) -> go.Figure:
142+
df_capacity = (
143+
results.flow_cap.where(results.techs != "demand_power")
144+
.sel(carriers="power")
145+
.to_series()
146+
.where(lambda x: x != 0)
147+
.dropna()
148+
.to_frame("Flow capacity (kW)")
149+
.reset_index()
150+
)
151+
152+
fig = px.bar(
153+
df_capacity,
154+
x="nodes",
155+
y="Flow capacity (kW)",
156+
color="techs",
157+
color_discrete_map=colors,
158+
**plotly_kwargs,
159+
)
160+
return fig
161+
162+
163+
# %% [markdown]
164+
# ### Using different `spores` scoring algorithms.
165+
#
166+
# We make a number of scoring algorithms accessible out-of-the-box, based on those we present in [Lombardi et al. (2023)](https://doi.org/10.1016/j.apenergy.2023.121002).
167+
# You can call them on solving the model.
168+
# Here, we'll compare the result on `flow_cap` from running each.
169+
170+
# %%
171+
# We subset to the same time range as operate/plan mode
172+
model_spores = calliope.examples.national_scale(
173+
scenario="spores", time_subset=["2005-01-01", "2005-01-10"]
174+
)
175+
model_spores.build()
176+
177+
spores_results = []
178+
for algorithm in ["integer", "evolving_average", "random", "relative_deployment"]:
179+
model_spores.solve(**{"spores.scoring_algorithm": algorithm}, force=True)
180+
spores_results.append(model_spores.results.expand_dims(algorithm=[algorithm]))
181+
182+
spores_results_da = xr.concat(spores_results, dim="algorithm")
183+
184+
spores_results_da.flow_cap.to_series().dropna().unstack("spores")
185+
186+
# %% [markdown]
187+
# ## `plan` vs `operate`
188+
# Here, we compare flows over the 10 days.
189+
# Note how flows do not match as the rolling horizon makes it difficult to make the correct storage charge/discharge decisions.
190+
191+
# %%
192+
fig_flows_plan = plot_flows(
193+
model_plan.results.sel(timesteps=model_operate.results.timesteps)
194+
)
195+
fig_flows_plan.update_layout(title="Plan mode flows")
196+
197+
198+
# %%
199+
fig_flows_operate = plot_flows(model_operate.results)
200+
fig_flows_operate.update_layout(title="Operate mode flows")
201+
202+
# %% [markdown]
203+
# ## `plan` vs `spores`
204+
# Here, we compare installed capacities between the baseline run (== `plan` mode) and the SPORES.
205+
# Note how the baseline SPORE is the same as `plan` mode and then results deviate considerably.
206+
207+
# %%
208+
fig_flows_plan = plot_capacity(model_plan.results)
209+
fig_flows_plan.update_layout(title="Plan mode capacities")
210+
211+
# %%
212+
fig_flows_spores = plot_capacity(model_spores.results, facet_col="spores")
213+
fig_flows_spores.update_layout(title="SPORES mode capacities")
214+
215+
# %% [markdown]
216+
# ## Comparing `spores` scoring algorithms
217+
# Here, we compare installed capacities between the different SPORES runs.
218+
219+
# %%
220+
fig_flows_spores = plot_capacity(
221+
spores_results_da, facet_col="spores", facet_row="algorithm"
222+
)
223+
fig_flows_spores.update_layout(
224+
title="SPORES mode capacities using different scoring algorithms",
225+
autosize=False,
226+
height=800,
227+
)

docs/examples/national_scale/notebook.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@
109109

110110
# %% [markdown]
111111
# #### Plotting flows
112-
# We do this by combinging in- and out-flows and separating demand from other technologies.
112+
# We do this by combining in- and out-flows and separating demand from other technologies.
113113
# First, we look at the aggregated result across all nodes, then we look at each node separately.
114114

115115
# %%

docs/examples/piecewise_constraints.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@
129129
"""
130130

131131
# %% [markdown]
132-
# # Building and checking the optimisation problem
132+
# # Building and checking the piecewise constraint
133133
#
134134
# With our piecewise constraint defined, we can build our optimisation problem and inject this new math.
135135

docs/hooks/dummy_model/model.yaml

+9-2
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,19 @@ overrides:
55
time_cluster: cluster_days.csv
66
config.build:
77
add_math: ["storage_inter_cluster"]
8+
spores:
9+
config:
10+
init.name: SPORES solve mode
11+
build.mode: spores
12+
solve.spores.number: 2
13+
parameters:
14+
spores_slack: 0.1
815

916
config.init.name: base
1017

1118
nodes:
12-
A.techs: {demand_tech, conversion_tech, supply_tech, storage_tech}
13-
B.techs: {demand_tech, conversion_tech, supply_tech, storage_tech}
19+
A.techs: { demand_tech, conversion_tech, supply_tech, storage_tech }
20+
B.techs: { demand_tech, conversion_tech, supply_tech, storage_tech }
1421

1522
techs:
1623
tech_transmission:

docs/hooks/generate_math_docs.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def on_files(files: list, config: dict, **kwargs):
6767
f"{override}.yaml",
6868
textwrap.dedent(
6969
f"""
70-
Pre-defined additional math to apply {custom_documentation.name} math on top of the [base mathematical formulation][base-math].
70+
Pre-defined additional math to apply {custom_documentation.name} __on top of__ the [base mathematical formulation][base-math].
7171
This math is _only_ applied if referenced in the `config.init.add_math` list as `{override}`.
7272
"""
7373
),

mkdocs.yml

+1
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ nav:
132132
- examples/milp/index.md
133133
- examples/milp/notebook.py
134134
- examples/loading_tabular_data.py
135+
- examples/modes.py
135136
- examples/piecewise_constraints.py
136137
- examples/calliope_model_object.py
137138
- examples/calliope_logging.py

0 commit comments

Comments
 (0)