Skip to content

Commit

Permalink
Add spores scenarios to example and add notebook
Browse files Browse the repository at this point in the history
  • Loading branch information
FLomb committed Nov 20, 2024
1 parent 325ab50 commit 6180cf1
Show file tree
Hide file tree
Showing 4 changed files with 195 additions and 30 deletions.
154 changes: 154 additions & 0 deletions docs/examples/national_scale/spores_run.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
# ---
# jupyter:
# jupytext:
# text_representation:
# extension: .py
# format_name: percent
# format_version: '1.3'
# jupytext_version: 1.16.4
# kernelspec:
# display_name: Python 3 (ipykernel)
# language: python
# name: python3
# ---

# %% [markdown]
# # Generating SPORES
# An interactive example of how to generate near-optimal system designs (or SPORES) out of a Calliope v0.7.0 model. This example relies solely on default software functionality and a custom Python function to determine how to assign penalties (scores) to previously explored system design options.

# %%
# Importing the required packages
import calliope
import xarray as xr

# %% [markdown]
# ## Cost-optimal model run and extraction of SPORES-relevant outputs

# %%
# Loading model files and building the model
model = calliope.Model(
"example_models/latest_national_scale/model.yaml", scenario="simplified_spores"
)
model.build()

# Solving
model.solve()

# Extracting SPORES-relevant data
least_feasible_cost = model.results.cost.loc[{"costs": "monetary"}].sum().sum()
print("The minimum cost for a feasible system design is {}".format(least_feasible_cost.values))


# %% [markdown]
# ## SPORES model run
# ### Definition of the penalty-assignment methods

# %%
def scoring_integer(results, backend):
# Filter for technologies of interest
spores_techs = backend.inputs["spores_tracker"].notnull()
# Look at capacity deployment in the previous iteration
previous_cap = results.flow_cap
# Make sure that penalties are applied only to non-negligible deployments of capacity
min_relevant_size = 0.1 * previous_cap.where(spores_techs).max(
["nodes", "carriers", "techs"]
)
# Where capacity was deployed more than the minimal relevant size, assign an integer penalty (score)
new_score = previous_cap.copy()
new_score = new_score.where(spores_techs, other=0)
new_score = new_score.where(new_score > min_relevant_size, other=0)
new_score = new_score.where(new_score == 0, other=1000)
# Transform the score into a "cost" parameter
new_score.rename("cost_flow_cap")
new_score = new_score.expand_dims(costs=["spores_score"]).copy()
new_score = new_score.sum("carriers")
# Extract the existing cost parameters from the backend
all_costs = backend.get_parameter("cost_flow_cap", as_backend_objs=False)
try:
all_costs = all_costs.expand_dims(nodes=results.nodes).copy()
except:
pass
# Create a new version of the cost parameters by adding up the calculated scores
new_all_costs = all_costs
new_all_costs.loc[{"costs":"spores_score"}] += new_score.loc[{"costs":"spores_score"}]

return new_all_costs


# %% [markdown]
# ### Iterating over the desired number of alternatives

# %%
# Create some lists to store results as they get generated
spores = [] # full results
scores = [] # scores only
spores_counter = 1
number_of_spores = 5

# %%
for i in range(spores_counter, spores_counter + number_of_spores):

if spores_counter == 1:
# Store the cost-optimal results
spores.append(model.results.expand_dims(spores=[0]))
scores.append(
model.backend.get_parameter("cost_flow_cap", as_backend_objs=False)
.sel(costs="spores_score")
.expand_dims(spores=[0])
)
# Update the slack-cost backend parameter based on the calculated minimum feasible system design cost
model.backend.update_parameter("spores_cost_max", least_feasible_cost)
# Update the objective_cost_weights to reflect the ones defined for the SPORES mode
model.backend.update_parameter(
"objective_cost_weights", model.inputs.spores_objective_cost_weights
)
else:
pass

# Calculate weights based on a scoring method
spores_score = scoring_integer(model.results, model.backend)
# Assign a new score based on the calculated penalties
model.backend.update_parameter(
"cost_flow_cap", spores_score.reindex_like(model.inputs.cost_flow_cap)
)
# Run the model again to get a solution that reflects the new penalties
model.solve(force=True)
# Store the results
spores.append(model.results.expand_dims(spores=[i]))
scores.append(
model.backend.get_parameter("cost_flow_cap", as_backend_objs=False)
.sel(costs="spores_score")
.expand_dims(spores=[i])
)

spores_counter += 1

# Concatenate the results in the storage lists into xarray objects
spore_ds = xr.concat(spores, dim="spores")
score_da = xr.concat(scores, dim="spores")

# %% [markdown]
# ## Plotting and sense-check

# %%
# Import plotting libraries
import matplotlib.pyplot as plt
import matplotlib as mpl

# %%
# Extract the deployed capacities across SPORES, which we want to visualise
flow_caps = spore_ds.flow_cap.where(
model.backend.inputs["spores_tracker"].notnull()).sel(
carriers='power').to_series().dropna().unstack("spores")
flow_caps

# %%
# Plot the capacities per location across the generated SPORES
ax = plt.subplot(111)
colors = mpl.colormaps['Pastel1'].colors
flow_caps.plot.bar(
ax=ax, ylabel="Capacity (kW)", ylim=[0, 40000],
color=colors[0:len(flow_caps.columns)]
)

# %%
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
constraints:
total_system_cost_max:
description: >
Limit total system cost. Conceived for use in SPORES mode to apply a maximum
relaxation to the system cost compared to the least-cost feasible option.
equations:
- expression: sum(cost[costs=monetary], over=[nodes, techs]) <= spores_cost_max * (1 + spores_slack)
2 changes: 1 addition & 1 deletion src/calliope/example_models/national_scale/model.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ config:
mode: plan # Choices: plan, operate

solve:
solver: cbc
solver: gurobi
zero_threshold: 1e-10 # Any value coming out of the backend that is smaller than this (due to floating point errors, probably) will be set to zero
# --8<-- [end:config]

Expand Down
62 changes: 33 additions & 29 deletions src/calliope/example_models/national_scale/scenarios.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,41 +30,45 @@ overrides:
time_cluster: data_tables/cluster_days.csv

spores:
config:
build.mode: spores
solve:
spores_score_cost_class: "spores_score"
spores_number: 3
config.build.add_math: ["additional_math.yaml"] # to create a slack-cost constraint
config.init.broadcast_param_data: false
parameters:
# In SPORES mode, slack to apply when setting the upper limit on system `spores_slack_cost_group` costs.
# The constraint will limit the costs to `(spores_slack + 1) * sum(costs[cost=spores_slack_cost_group])`.
spores_slack: 0.1
objective_cost_weights:
objective_cost_weights: # makes sure that a 'spores_score' class exists in the model and in the objective
data: [1, 0]
index: ["monetary", "spores_score"]
# In SPORES mode, the new objective function cost class weightings to apply after the initial base run.
spores_objective_cost_weights:
data: [1, 0]
index: [spores_score, monetary]
data: [0, 1]
index: ["monetary", "spores_score"]
dims: costs

# FIXME: replace group constraint in SPORES mode
# group_constraints:
# systemwide_cost_max.cost_max.monetary: 1e10 # very large, non-infinite value
templates:
cost_dim_setter:
cost_flow_cap:
index: [monetary, spores_score]
dims: costs
cost_interest_rate:
data: [0.10, 1]
index: [monetary, spores_score]
dims: costs
spores_slack: 0.5
spores_tracker: # defines which techs are going to be subject to the SPORES weighting process
data: [true,true,true]
index: [ccgt,csp,battery]
dims: techs
spores_cost_max: 1e6 # parameter used in the custom-math slack cost constraint. Initially very high (infinite)
templates:
cost_dim_setter:
cost_interest_rate:
data: 1
index: "spores_score"
dims: costs
techs:
ccgt.cost_flow_cap.data: [750, 0]
csp.cost_flow_cap.data: [1000, 0]
battery.cost_flow_cap.data: [null, 0]
region1_to_region2.cost_flow_cap.data: [200, 0]
ccgt:
cost_flow_cap: {'data': [0], 'index': ['spores_score'], 'dims': 'costs'}
cost_flow_in: {'data': [0], 'index': ['spores_score'], 'dims': 'costs'}
csp:
cost_storage_cap: {'data': [0], 'index': ['spores_score'], 'dims': 'costs'}
cost_area_use: {'data': [0], 'index': ['spores_score'], 'dims': 'costs'}
cost_source_cap: {'data': [0], 'index': ['spores_score'], 'dims': 'costs'}
cost_flow_cap: {'data': [0], 'index': ['spores_score'], 'dims': 'costs'}
cost_flow_out: {'data': [0], 'index': ['spores_score'], 'dims': 'costs'}
battery:
cost_storage_cap: {'data': [0], 'index': ['spores_score'], 'dims': 'costs'}
region1_to_region2:
cost_flow_cap: {'data': [0], 'index': ['spores_score'], 'dims': 'costs'}
cost_flow_out: {'data': [0], 'index': ['spores_score'], 'dims': 'costs'}


operate:
config:
init.time_subset: ["2005-01-01", "2005-01-10"]
Expand Down

0 comments on commit 6180cf1

Please sign in to comment.