Skip to content

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.

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:
    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: storage_cap_min
      max: storage_cap_max
    active: true  # optional; defaults to true.
  1. It needs a unique name (storage_cap in the example above).
  2. Ideally, it has a long-form description and a unit added. These are not required, but are useful metadata for later reference.
  3. It can have a top-level foreach list and where string. Without a foreach, it becomes an un-indexed variable. Without a where string, all valid members (according to the definition_matrix) based on foreach will be included in this decision variable.
  4. It can define a domain to turn it into a binary or integer variable (in either of those cases, domain becomes integer).
  5. It requires a minimum and maximum bound, which can be:
    1. a numeric value:
      variables:
        flow_out:
        ...
          bounds:
            min: 0
            max: .inf
      
    2. 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.
  6. It can be deactivated so that it does not appear in the built optimisation problem by setting active: false.
  7. It can take on a default value that will be used in math operations to avoid NaN 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 means setting it to zero).

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:
    description: >-
      The total annualised costs of a technology,
      including installation and operation costs.
    default: 0
    unit: cost
    foreach: [nodes, techs, costs]
    where: "cost_investment OR cost_var"
    equations:
      - expression: $cost_investment + $cost_var_sum
    sub_expressions:
      cost_investment:
        - where: "cost_investment"
          expression: cost_investment
        - where: "NOT cost_investment"
          expression: "0"
      cost_var_sum:
        - where: "cost_var"
          expression: sum(cost_var, over=timesteps)
        - where: "NOT cost_var"
          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.

  1. It needs a unique name (cost in the above example).
  2. Ideally, it has a long-form description and a unit added. These are not required, but are useful metadata for later reference.
  3. It can have a top-level foreach list and where string. Without a foreach, it becomes an un-indexed expression. Without a where string, all valid members (according to the definition_matrix) based on foreach will be included in this expression.
  4. 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
  5. It can be deactivated so that it does not appear in the built optimisation problem by setting active: false.
  6. It can take on a default value that will be used in math operations to avoid NaN 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 means setting it to zero).

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.
  1. It needs a unique name (set_storage_initial in the above example).
  2. Ideally, it has a long-form description and a unit added. These are not required, but are useful metadata for later reference.
  3. It can have a top-level foreach list and where string. Without a foreach, it becomes an un-indexed constraint. Without a where string, all valid members (according to the definition_matrix) based on foreach will be included in this constraint.
  4. It has equations (and, optionally, sub-expressions and slices) with corresponding lists of where+expression dictionaries. The equation expressions must have comparison operators.
  5. It can be deactivated so that it does not appear in the built optimisation problem by setting active: false.

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.
  1. It needs a unique name.
  2. Ideally, it has a long-form description and a unit added. These are not required, but are useful metadata for later reference.
  3. It can have a top-level where string, but no foreach (it is a single value you need to minimise/maximise). Without a where string, the objective will be activated.
  4. It has equations (and, optionally, sub-expressions and slices) with corresponding lists of where+expression dictionaries. These expressions do not have comparison operators.
  5. 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.