-
Notifications
You must be signed in to change notification settings - Fork 94
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature: piecewise constraints #569
Changes from all commits
e052989
f8c0870
ab3f55b
4e5e644
d7e83ac
d90da69
d176dfe
fd091e4
ba70e5b
2199240
ec05159
b912279
d8946ac
881cb89
22559b0
b2ba3a6
1f9c8b1
53e7d44
e1b17b6
52043a9
1c596e1
5b6202e
3bb0535
9405c35
c0f8993
4188d06
4c7e198
d276c63
33f1f2d
ff81669
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,271 @@ | ||
# --- | ||
# jupyter: | ||
# jupytext: | ||
# custom_cell_magics: kql | ||
# text_representation: | ||
# extension: .py | ||
# format_name: percent | ||
# format_version: '1.3' | ||
# jupytext_version: 1.16.1 | ||
# kernelspec: | ||
# display_name: calliope_docs_build [conda env:calliope-docs-new] | ||
# language: python | ||
# name: conda-env-calliope-docs-new-calliope_docs_build | ||
# --- | ||
|
||
# %% [markdown] | ||
# # Defining piecewise linear constraints | ||
# | ||
# In this tutorial, we use the national scale example model to implement a piecewise linear constraint. | ||
# This constraint will represent a non-linear relationship between capacity and cost per unit capacity of Concentrating Solar Power (CSP). | ||
|
||
# %% | ||
|
||
import calliope | ||
import numpy as np | ||
import plotly.express as px | ||
|
||
calliope.set_log_verbosity("INFO", include_solver_output=False) | ||
|
||
# %% [markdown] | ||
# # Model setup | ||
|
||
# %% [markdown] | ||
# ## Defining our piecewise curve | ||
# | ||
# In the base national scale model, the CSP has a maximum rated capacity of 10,000 kW and a cost to invest in that capacity of 1000 USD / kW. | ||
# | ||
# In our updated model, the cost to invest in capacity will vary from 5000 USD / kW to 500 USD / kW as the CSP capacity increases: | ||
|
||
# %% | ||
capacity_steps = [0, 2500, 5000, 7500, 10000] | ||
cost_steps = [0, 3.75e6, 6e6, 7.5e6, 8e6] | ||
|
||
cost_per_cap = np.nan_to_num(np.divide(cost_steps, capacity_steps)).astype(int) | ||
|
||
fig = px.line( | ||
x=capacity_steps, | ||
y=cost_steps, | ||
labels={"x": "Capacity (kW)", "y": "Investment cost (USD)"}, | ||
markers="o", | ||
range_y=[0, 10e6], | ||
text=[f"{i} USD/kW" for i in cost_per_cap], | ||
) | ||
fig.update_traces(textposition="top center") | ||
fig.show() | ||
|
||
|
||
# %% [markdown] | ||
# We can then provide this data when we load our model: | ||
# | ||
# <div class="admonition note"> | ||
# <p class="admonition-title">Note</p> | ||
# <p> | ||
# We must index our piecewise data over "breakpoints". | ||
# </p> | ||
# </div> | ||
# | ||
|
||
# %% | ||
new_params = { | ||
"parameters": { | ||
"capacity_steps": { | ||
"data": capacity_steps, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice-to-have:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. They are defined in the previous Python cell (with only one markdown cell in between). I'd say that's close enough to be visible. Still, I'll print out |
||
"index": [0, 1, 2, 3, 4], | ||
"dims": "breakpoints", | ||
}, | ||
"cost_steps": { | ||
"data": cost_steps, | ||
"index": [0, 1, 2, 3, 4], | ||
"dims": "breakpoints", | ||
}, | ||
} | ||
} | ||
print(new_params) | ||
m = calliope.examples.national_scale(override_dict=new_params) | ||
|
||
# %% | ||
m.inputs.capacity_steps | ||
|
||
# %% | ||
m.inputs.cost_steps | ||
|
||
# %% [markdown] | ||
# ## Creating our piecewise constraint | ||
# | ||
# We create the piecewise constraint by linking decision variables to the piecewise curve we have created. | ||
# In this example, we require a new decision variable for investment costs that can take on the value defined by the curve at a given value of `flow_cap`. | ||
|
||
# %% | ||
m.math["variables"]["piecewise_cost_investment"] = { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice-to-have: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hmm, this perhaps exposes a problem with adding math snippets on-the-fly. I'd need to show it in YAML and then also store that in a file AND reference that in |
||
"description": "Investment cost that increases monotonically", | ||
"foreach": ["nodes", "techs", "carriers", "costs"], | ||
"where": "[csp] in techs", | ||
"bounds": {"min": 0, "max": np.inf}, | ||
"default": 0, | ||
} | ||
|
||
# %% [markdown] | ||
# We also need to link that decision variable to our total cost calculation. | ||
|
||
# %% | ||
# Before | ||
m.math["global_expressions"]["cost_investment_flow_cap"]["equations"] | ||
|
||
# %% | ||
# Updated - we split the equation into two expressions. | ||
m.math["global_expressions"]["cost_investment_flow_cap"]["equations"] = [ | ||
{"expression": "$cost_sum * flow_cap", "where": "NOT [csp] in techs"}, | ||
{"expression": "piecewise_cost_investment", "where": "[csp] in techs"}, | ||
] | ||
|
||
# %% [markdown] | ||
# We then need to define the piecewise constraint: | ||
|
||
# %% | ||
m.math["piecewise_constraints"]["csp_piecewise_costs"] = { | ||
"description": "Set investment costs values along a piecewise curve using special ordered sets of type 2 (SOS2).", | ||
"foreach": ["nodes", "techs", "carriers", "costs"], | ||
"where": "piecewise_cost_investment", | ||
"x_expression": "flow_cap", | ||
"x_values": "capacity_steps", | ||
"y_expression": "piecewise_cost_investment", | ||
"y_values": "cost_steps", | ||
} | ||
|
||
# %% [markdown] | ||
# Then we can build our optimisation problem: | ||
|
||
# %% [markdown] | ||
# # Building and checking the optimisation problem | ||
# | ||
# With our piecewise constraint defined, we can build our optimisation problem | ||
|
||
# %% | ||
m.build() | ||
|
||
# %% [markdown] | ||
# And we can see that our piecewise constraint exists in the built optimisation problem "backend" | ||
|
||
# %% | ||
m.backend.verbose_strings() | ||
m.backend.get_piecewise_constraint("csp_piecewise_costs").to_series().dropna() | ||
|
||
# %% [markdown] | ||
# ## Solve the optimisation problem | ||
# | ||
# Once we have all of our optimisation problem components set up as we desire, we can solve the problem. | ||
|
||
# %% | ||
m.solve() | ||
|
||
# %% [markdown] | ||
# The results are stored in `m._model_data` and can be accessed by the public property `m.results` | ||
|
||
# %% [markdown] | ||
# ## Analysing the outputs | ||
|
||
# %% | ||
# Absolute | ||
csp_cost = m.results.cost_investment_flow_cap.sel(techs="csp") | ||
csp_cost.to_series().dropna() | ||
|
||
# %% | ||
# Relative to capacity | ||
csp_cap = m.results.flow_cap.sel(techs="csp") | ||
csp_cost_rel = csp_cost / csp_cap | ||
csp_cost_rel.to_series().dropna() | ||
|
||
# %% | ||
# Plotted on our piecewise curve | ||
fig.add_scatter( | ||
x=csp_cap.to_series().dropna().values, | ||
y=csp_cost.to_series().dropna().values, | ||
mode="markers", | ||
marker_symbol="cross", | ||
marker_size=10, | ||
marker_color="cyan", | ||
name="Installed capacity", | ||
) | ||
fig.show() | ||
|
||
# %% [markdown] | ||
# ## YAML model definition | ||
# We have updated the model parameters and math interactively in Python in this tutorial, the definition in YAML would look like: | ||
|
||
# %% [markdown] | ||
# ### Math | ||
# | ||
# Saved as e.g., `csp_piecewise_math.yaml`. | ||
# | ||
# ```yaml | ||
# variables: | ||
# piecewise_cost_investment: | ||
# description: Investment cost that increases monotonically | ||
# foreach: [nodes, techs, carriers, costs] | ||
# where: "[csp] in techs" | ||
# bounds: | ||
# min: 0 | ||
# max: .inf | ||
# default: 0 | ||
# | ||
# piecewise_constraints: | ||
# csp_piecewise_costs: | ||
# description: > | ||
# Set investment costs values along a piecewise curve using special ordered sets of type 2 (SOS2). | ||
# foreach: [nodes, techs, carriers, costs] | ||
# where: "[csp] in techs" | ||
# x_expression: flow_cap | ||
# x_values: capacity_steps | ||
# y_expression: piecewise_cost_investment | ||
# y_values: cost_steps | ||
# | ||
# global_expressions: | ||
# cost_investment_flow_cap.equations: | ||
# - expression: "$cost_sum * flow_cap" | ||
# where: "NOT [csp] in techs" | ||
# - expression: "piecewise_cost_investment" | ||
# where: "[csp] in techs" | ||
# ``` | ||
|
||
# %% [markdown] | ||
# ### Scenario definition | ||
# | ||
# Loaded into the national-scale example model with: `calliope.examples.national_scale(scenario="piecewise_csp_cost")` | ||
# | ||
# ```yaml | ||
# overrides: | ||
# piecewise_csp_cost: | ||
# config.init.add_math: [csp_piecewise_math.yaml] | ||
# parameters: | ||
# capacity_steps: | ||
# data: [0, 2500, 5000, 7500, 10000] | ||
# index: [0, 1, 2, 3, 4] | ||
# dims: "breakpoints" | ||
# cost_steps: | ||
# data: [0, 3.75e6, 6e6, 7.5e6, 8e6] | ||
# index: [0, 1, 2, 3, 4] | ||
# dims: "breakpoints" | ||
# ``` | ||
|
||
# %% [markdown] | ||
# ## Troubleshooting | ||
# | ||
# If you are failing to load a piecewise constraint or it isn't working as expected, here are some common things to note: | ||
# | ||
# 1. The extent of your `x_values` and `y_values` will dictate the maximum values of your piecewise decision variables. | ||
# In this example, we define `capacity_steps` over the full capacity range that we allow our CSP to cover in the model. | ||
# However, if we set `capacity_steps` to `[0, 2500, 5000, 7500, 9000]` then `flow_cap` would _never_ go above a value of 9000. | ||
# | ||
# 2. The `x_values` and `y_values` parameters must have the same number of breakpoints and be indexed over `breakpoints`. | ||
# It is possible to extend these parameters to be indexed over other dimensions (e.g., different technologies with different piecewise curves) but it must _always_ include the `breakpoints` dimension. | ||
# | ||
# 3. `x_values` must increase monotonically. That is, `[0, 5000, 2500, 7500, 10000]` is not valid for `capacity_steps` in this example. | ||
# `y_values`, on the other hand, _can_ vary any way you like; `[0, 6e6, 3.75e6, 8e6, 7.5e6]` is valid for `cost_steps`. | ||
# | ||
# 4. `x_expression` and `y_expression` _must_ include reference to at least one decision variable. | ||
# It can be a math expression, not only a single decision variable. `flow_cap + storage_cap / 2` would be valid for `x_expression` in this example. | ||
# | ||
# 5. Piecewise constraints will make your problem more difficult to solve since each breakpoint adds a binary decision variable. | ||
# Larger models with detailed piecewise constraints may not solve in a reasonable amount of time. | ||
# |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -977,5 +977,7 @@ Now, all components of our internal math are defined in a readable YAML syntax t | |
|
||
You can add your own math to update the pre-defined math and to represent the physical system in ways we do not cover in our base math, or to apply new modelling methods and problem types (e.g., pathway or stochastic optimisation)! | ||
|
||
When adding your own math, you can add [piecewise linear constraints](user_defined_math/components.md#piecewise-constraints), which is a new type of constraint compared to what could be defined in v0.6. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice-to-have: not sure if this belongs here... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, we need a quick intro to everything you can do in Calliope v0.7, which we don't have at the moment. Hence why some new functionality is described in |
||
|
||
!!! info "See also" | ||
Our [pre-defined](pre_defined_math/index.md) and [user-defined](user_defined_math/index.md) math documentation. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
# title: Piecewise linear costs - economies of scale | ||
# | ||
# description: | | ||
# Add a piecewise cost function that reduces the incremental increase in investment costs with increasing technology rated capacity. | ||
# This emulates "economies of scale", where the more of a technology there is deployed, the less expensive each additional investment in deployment. | ||
# | ||
# A more detailing example can be found in our [dedicated tutorial][defining-piecewise-linear-constraints]. | ||
# | ||
# New indexed parameters: | ||
# | ||
# - `piecewise_cost_investment_x` (defining the new set `breakpoints`) | ||
# - `piecewise_cost_investment_y` (defining the new set `breakpoints`) | ||
# | ||
# --- | ||
|
||
variables: | ||
piecewise_cost_investment: | ||
description: Investment cost that increases monotonically | ||
foreach: [nodes, techs, carriers, costs] | ||
where: any(piecewise_cost_investment_x, over=breakpoints) AND any(piecewise_cost_investment_y, over=breakpoints) | ||
bounds: | ||
min: 0 | ||
max: .inf | ||
|
||
piecewise_constraints: | ||
sos2_piecewise_costs: | ||
description: > | ||
Set investment cost values along a piecewise curve using special ordered sets of type 2 (SOS2). | ||
foreach: [nodes, techs, carriers, costs] | ||
where: any(piecewise_cost_investment_x, over=breakpoints) AND any(piecewise_cost_investment_y, over=breakpoints) | ||
x_expression: flow_cap | ||
x_values: piecewise_cost_investment_x | ||
y_expression: piecewise_cost_investment | ||
y_values: piecewise_cost_investment_y |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice-to-have: at the moment, it's a tad hard to find this if you do not know about this functionality.
This relates to a separate issue (#601), but we should consider improving this in the future, maybe by listing these features in the Home directory.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, as mentioned in another comment, having a key features page is high up on our to-do list. @sjpfenninger is on the case!