Math components¶
Here, we will briefly introduce each of the math components you will need to build an optimisation problem. A more detailed description of the math YAML syntax is provided on the math syntax page and in the [math formulation schema][math-formulation-schema].
Decision variables¶
Decision variables (called variables
in Calliope) are the unknown quantities whose values can be chosen by the optimisation algorithm while optimising for the chosen objective (e.g. cost minimisation) under the bounds set by the constraints.
These include the output capacity of technologies, the per-timestep flow of carriers into and out of technologies or along transmission lines, and storage content in each timestep.
A decision variable in Calliope math looks like this:
variables:
storage_cap:
title: Stored carrier capacity
description: >-
The upper limit on a carrier that can
be stored by a technology in any timestep.
default: 0
unit: energy
foreach: [nodes, techs]
where: "include_storage=True OR base_tech=storage"
domain: real # optional; defaults to real.
bounds:
min: 0 # set in a distinct constraint to handle the integer purchase variable
max: storage_cap_max
active: true # optional; defaults to true.
- It needs a unique name (
storage_cap
in the example above). - Ideally, it has a long-form name (
title
), adescription
and aunit
added. These are not required, but are useful metadata for later reference. - It can have a top-level
foreach
list andwhere
string. Without aforeach
, it becomes an un-indexed variable. Without awhere
string, all valid members (according to thedefinition_matrix
) based onforeach
will be included in this decision variable. - It can define a domain to turn it into a binary or integer variable (in either of those cases, domain becomes
integer
). - It requires a minimum and maximum bound, which can be:
- a numeric value:
- a reference to an input parameter, where each valid member of the variable (i.e. each value of the variable for a specific combination of indices) will get a different value based on the values of the referenced parameters (see example above). If a value for a valid variable member is undefined in the referenced parameter, the decision variable will be unbounded for this member.
- It can be deactivated so that it does not appear in the built optimisation problem by setting
active: false
. - It can take on a
default
value that will be used in math operations to avoidNaN
values creeping in. The default value should be set such that it has no impact on the optimisation problem if it is included (most of the time, this meansNaN
).
Global Expressions¶
Global expressions are those combinations of decision variables and input parameters that you want access to in multiple constraints / objectives in the model. You will also receive the result of the global expression as a numeric value in your optimisation results, without having to do any additional post-processing.
For instance, total costs are global expressions as the cost associated with a technology is not a constraint, but rather a linear combination of decision variables and parameters (e.g., storage_cap * cost_storage_cap
).
To not clutter the objective function with all combinations of variables and parameters, we define a separate global expression:
global_expressions:
cost:
title: Total costs
description: >-
The total annualised costs of a technology,
including installation and operation costs.
default: 0
unit: cost
foreach: [nodes, techs, costs]
where: "cost_investment_annualised OR cost_operation_variable OR cost_operation_fixed"
equations:
- expression: >-
default_if_empty(cost_investment_annualised, 0) +
$cost_operation_sum +
default_if_empty(cost_operation_fixed, 0)
sub_expressions:
cost_operation_sum:
- where: "cost_operation_variable"
expression: sum(cost_operation_variable, over=timesteps)
- where: "NOT cost_operation_variable"
expression: "0"
active: true # optional; defaults to true.
Global expressions are by no means necessary to include, but can make more complex linear expressions easier to keep track of and can reduce post-processing requirements.
- It needs a unique name (
cost
in the above example). - Ideally, it has a long-form name (
title
), adescription
and aunit
added. These are not required, but are useful metadata for later reference. - It can have a top-level
foreach
list andwhere
string. Without aforeach
, it becomes an un-indexed expression. Without awhere
string, all valid members (according to thedefinition_matrix
) based onforeach
will be included in this expression. - It has equations (and, optionally, sub-expressions and slices) with corresponding lists of
where
+expression
dictionaries. The equation expressions do not have comparison operators; those are reserved for constraints - It can be deactivated so that it does not appear in the built optimisation problem by setting
active: false
. - It can take on a
default
value that will be used in math operations to avoidNaN
values creeping in. The default value should be set such that it has no impact on the optimisation problem if it is included (most of the time, this meansNaN
).
Constraints¶
Decision variables / global expressions need to be constrained or included in the model objective. Constraining these math components is where you introduce the realities of the system you are modelling. This includes limits on things like the maximum area use of tech (there's only so much rooftop available for roof-mounted solar PV), and links between in/outflows such as how much carrier is consumed by a technology to produce each unit of output carrier. Here is an example:
constraints:
set_storage_initial:
description: >-
Fix the relationship between carrier stored in a `storage` technology at
the start and end of the whole model period.
foreach: [nodes, techs]
where: "storage AND storage_initial AND cyclic_storage=True"
equations:
- expression: >-
storage[timesteps=$final_step] * (
(1 - storage_loss) ** timestep_resolution[timesteps=$final_step]
) == storage_initial * storage_cap
slices:
final_step:
- expression: get_val_at_index(timesteps=-1)
active: true # optional; defaults to true.
- It needs a unique name (
set_storage_initial
in the above example). - Ideally, it has a long-form
description
added. These are not required, but are useful metadata for later reference. - It can have a top-level
foreach
list andwhere
string. Without aforeach
, it becomes an un-indexed constraint. Without awhere
string, all valid members (according to thedefinition_matrix
) based onforeach
will be included in this constraint. - It has equations (and, optionally, sub-expressions and slices) with corresponding lists of
where
+expression
dictionaries. The equation expressions must have comparison operators. - It can be deactivated so that it does not appear in the built optimisation problem by setting
active: false
.
Piecewise constraints¶
If you have non-linear relationships between two decision variables, you may want to represent them as a piecewise linear function. The most common form of a piecewise function involves creating special ordered sets of type 2 (SOS2), set of binary variables that are linked together with specific constraints.
Note
You can find a fully worked-out example in our piecewise linear tutorial.
Because the formulation of piecewise constraints is so specific, the math syntax differs from all other modelling components by having x
and y
attributes that need to be specified:
piecewise_constraints:
sos2_piecewise_flow_out:
description: Set outflow to follow capacity according to a piecewise curve.
foreach: [nodes, techs, carriers]
where: piecewise_x AND piecewise_y
x_expression: flow_cap
x_values: piecewise_x
y_expression: flow_out
y_values: piecewise_y
active: true
- It needs a unique name (
sos2_piecewise_flow_out
in the above example). - Ideally, it has a long-form
description
added. This is not required, but is useful metadata for later reference. - It can have a top-level
foreach
list andwhere
string. Without aforeach
, it becomes an un-indexed constraint. Without awhere
string, all valid members (according to thedefinition_matrix
) based onforeach
will be included in this constraint. - It has
x
andy
expression strings (x_expression
,y_expression
). - It has
x
andy
parameter references (x_values
,y_values
). This should be a string name referencing an input parameter that contains thebreakpoints
dimension. The values given by this parameter will be used to set the respective (x
/y
) expression at each breakpoint. - It can be deactivated so that it does not appear in the built optimisation problem by setting
active: false
.
The component attributes combine to describe a piecewise curve that links the x_expression
and y_expression
according to their respective values in x_values
and y_values
at each breakpoint.
Note
If the non-linear function you want to represent is convex, you may be able to avoid SOS2 variables, and instead represent it using constraint components. You can find an example of this in our piecewise linear costs custom math example.
Warning
This approximation of a non-linear relationship may improve the representation of whatever real system you are modelling, but it will come at the cost of a more difficult model to solve. Indeed, introducing piecewise constraints may mean your model can no longer reach a solution with the computational resources you have available.
Objectives¶
With your constrained decision variables and a global expression that binds these variables to costs, you need an objective to minimise/maximise. The default, pre-defined objective is min_cost_optimisation
and looks as follows:
objectives:
min_cost_optimisation:
description: >-
Minimise the total cost of installing and operating
all technologies in the system.
If multiple cost classes are present (e.g., monetary and co2 emissions),
the weighted sum of total costs is minimised.
Cost class weights can be defined in the indexed parameter
`objective_cost_weights`.
equations:
- where: "any(cost, over=[nodes, techs, costs])"
expression: >-
sum(
sum(cost, over=[nodes, techs])
* objective_cost_weights,
over=costs
) + $unmet_demand
- where: "NOT any(cost, over=[nodes, techs, costs])"
expression: $unmet_demand
sub_expressions:
unmet_demand:
- where: "config.ensure_feasibility=True"
expression: >-
sum(
sum(unmet_demand - unused_supply, over=[carriers, nodes])
* timestep_weights,
over=timesteps
) * bigM
- where: "NOT config.ensure_feasibility=True"
expression: "0"
sense: minimise
active: true # optional; defaults to true.
- It needs a unique name.
- Ideally, it has a long-form
description
and aunit
added. These are not required, but are useful metadata for later reference. - It can have a top-level
where
string, but noforeach
(it is a single value you need to minimise/maximise). Without awhere
string, the objective will be activated. - It has equations (and, optionally, sub-expressions and slices) with corresponding lists of
where
+expression
dictionaries. These expressions do not have comparison operators. - It can be deactivated so that it does not appear in the built optimisation problem by setting
active: false
.
Warning
You can only have one objective function activated in your math.
If you have defined multiple objective functions, you can deactivate unwanted ones using active: false
, or you can set your top-level where
string on each that leads to only one being valid for your particular problem.