Advanced constraints¶
This section, as the title suggests, contains more info and more details, and in particular, information on some of Calliope’s more advanced functionality.
We suggest you read the Building a model, Running a model and Analysing a model sections first.
The supply_plus
tech¶
The plus
tech groups offer complex functionality, for technologies which cannot be described easily. Supply_plus
allows a supply technology with internal storage of resource before conversion to the carrier happens. This could be emulated with dummy carriers and a combination of supply, storage, and conversion techs, but the supply_plus
tech allows for concise and mathematically more efficient formulation.
Representation of the supply_plus
technology¶
An example use of supply_plus
is to define a concentrating solar power (CSP) technology which consumes a solar resource, has built-in thermal storage, and produces electricity. See the national-scale built-in example model for an application of this.
See the listing of supply_plus configuration in the abstract base tech group definitions for the additional constraints that are possible.
Warning
When analysing results from supply_plus, care must be taken to correctly account for the losses along the transformation from resource to carrier. For example, charging of storage from the resource may have a resource_eff
-associated loss with it, while discharging storage to produce the carrier may have a different loss resulting from a combination of energy_eff
and parasitic_eff
. Such intermediate conversion losses need to be kept in mind when comparing discharge from storage with carrier_prod
in the same time step.
The conversion_plus
tech¶
The plus
tech groups offer complex functionality, for technologies which cannot be described easily. Conversion_plus
allows several carriers to be converted to several other carriers. Describing such a technology requires that the user understands the carrier_ratios
, i.e. the interactions and relative efficiencies of carrier inputs and outputs.
Representation of the most complex conversion_plus
technology available¶
The conversion_plus
technologies allows for up to three carrier groups as inputs (carrier_in
, carrier_in_2
and carrier_in_3
) and up to three carrier groups as outputs (carrier_out
, carrier_out_2
and carrier_out_3
). A carrier group can contain any number of carriers.
The efficiency of a conversion_plus
tech dictates how many units of carrier_out are produced per unit of consumed carrier_in. A unit of carrier_out_2 and of carrier_out_3 is produced each time a unit of carrier_out is produced. Similarly, a unit of Carrier_in_2 and of carrier_in_3 is consumed each time a unit of carrier_in is consumed. Within a given carrier group (e.g. carrier_out_2) any number of carriers can meet this one unit. The carrier_ratio
of any carrier compares it either to the production of one unit of carrier_out or to the consumption of one unit of carrier_in.
In this section, we give examples of a few conversion_plus
technologies alongside the YAML formulation required to construct them:
Combined heat and power¶
A combined heat and power plant produces electricity, in this case from natural gas. Waste heat that is produced can be used to meet nearby heat demand (e.g. via district heating network). For every unit of electricity produced, 0.8 units of heat are always produced. This is analogous to the heat to power ratio (HTP). Here, the HTP is 0.8.
chp:
essentials:
name: Combined heat and power
carrier_in: gas
carrier_out: electricity
carrier_out_2: heat
primary_carrier_out: electricity
constraints:
energy_eff: 0.45
energy_cap_max: 100
carrier_ratios.carrier_out_2.heat: 0.8
Air source heat pump¶
The output energy from the heat pump can be either heat or cooling, simulating a heat pump that can be useful in both summer and winter. For each unit of electricity input, one unit of output is produced. Within this one unit of carrier_out
, there can be a combination of heat and cooling. Heat is produced with a COP of 5, cooling with a COP of 3. If only heat were produced in a timestep, 5 units of it would be available in carrier_out; similarly 3 units for cooling. In another timestep, both heat and cooling might be produced with e.g. 2.5 units heat + 1.5 units cooling = 1 unit of carrier_out.
ahp:
essentials:
name: Air source heat pump
carrier_in: electricity
carrier_out: [heat, cooling]
primary_carrier_out: heat
constraints:
energy_eff: 1
energy_cap_max: 100
carrier_ratios:
carrier_out:
heat: 5
cooling: 3
Combined cooling, heat and power (CCHP)¶
A CCHP plant can use generated heat to produce cooling via an absorption chiller. As with the CHP plant, electricity is produced at 45% efficiency. For every unit of electricity produced, 1 unit of carrier_out_2
must be produced, which can be a combination of 0.8 units of heat and 0.5 units of cooling. Some example ways in which the model could decide to operate this unit in a given time step are:
1 unit of gas (
carrier_in
) is converted to 0.45 units of electricity (carrier_out
) and (0.8 * 0.45) units of heat (carrier_out_2
)1 unit of gas is converted to 0.45 units electricity and (0.5 * 0.45) units of cooling
1 unit of gas is converted to 0.45 units electricity, (0.3 * 0.8 * 0.45) units of heat, and (0.7 * 0.5 * 0.45) units of cooling
cchp:
essentials:
name: Combined cooling, heat and power
carrier_in: gas
carrier_out: electricity
carrier_out_2: [heat, cooling]
primary_carrier_out: electricity
constraints:
energy_eff: 0.45
energy_cap_max: 100
carrier_ratios.carrier_out_2: {heat: 0.8, cooling: 0.5}
Advanced gas turbine¶
This technology can choose to burn methane (CH:sub:4) or send hydrogen (H:sub:2) through a fuel cell to produce electricity. One unit of carrier_in can be met by any combination of methane and hydrogen. If all methane, 0.5 units of carrier_out would be produced for 1 unit of carrier_in (energy_eff). If all hydrogen, 0.25 units of carrier_out would be produced for the same amount of carrier_in (energy_eff * hydrogen carrier ratio).
gt:
essentials:
name: Advanced gas turbine
carrier_in: [methane, hydrogen]
carrier_out: electricity
constraints:
energy_eff: 0.5
energy_cap_max: 100
carrier_ratios:
carrier_in: {methane: 1, hydrogen: 0.5}
Complex fictional technology¶
There are few instances where using the full capacity of a conversion_plus tech is physically possible. Here, we have a fictional technology that combines fossil fuels with biomass/waste to produce heat, cooling, and electricity. Different ‘grades’ of heat can be produced, the higher grades having an alternative. High grade heat (high_T_heat
) is produced and can be used directly, or used to produce electricity (via e.g. organic rankine cycle). carrier_out
is thus a combination of these two. carrier_out_2 can be 0.3 units mid grade heat for every unit carrier_out or 0.2 units cooling. Finally, 0.1 units carrier_out_3
, low grade heat, is produced for every unit of carrier_out.
complex:
essentials:
name: Complex fictional technology
carrier_in: [coal, gas, oil]
carrier_in_2: [biomass, waste]
carrier_out: [high_T_heat, electricity]
carrier_out_2: [mid_T_heat, cooling]
carrier_out_3: low_T_heat
primary_carrier_out: electricity
constraints:
energy_eff: 1
energy_cap_max: 100
carrier_ratios:
carrier_in: {coal: 1.2, gas: 1, oil: 1.6}
carrier_in_2: {biomass: 1, waste: 1.25}
carrier_out: {high_T_heat: 0.8, electricity: 0.6}
carrier_out_2: {mid_T_heat: 0.3, cooling: 0.2}
carrier_out_3.low_T_heat: 0.15
A primary_carrier_out
must be defined when there are multiple carrier_out
values defined, similarly primary_carrier_in
can be defined for carrier_in
. primary_carriers can be defined as any carrier in a technology’s input/output carriers (including secondary and tertiary carriers). The chosen output carrier will be the one to which production costs are applied (reciprocally, input carrier for consumption costs).
Note
Conversion_plus
technologies can also export any one of their output carriers, by specifying that carrier as carrier_export
.
Resource area constraints¶
Several optional constraints can be used to specify area-related restrictions on technology use.
To make use of these constraints, one should set resource_unit: energy_per_area
for the given technologies. This scales the available resource at a given location for a given technology with its resource_area
decision variable.
The following related settings are available:
resource_area_equals
,resource_area_max
,resource_area_min
: Set uppper or lower bounds on resource_area or force it to a specific valueresource_area_per_energy_cap
: False by default, but if set to true, it forcesresource_area
to followenergy_cap
with the given numerical ratio (e.g. setting to 1.5 means thatresource_area == 1.5 * energy_cap
)
By default, resource_area_max
is infinite and resource_area_min
is 0 (zero).
Group constraints¶
Group constraints are applied to named sets of locations and techs, called “constraint groups”, specified through a top-level group_constraints
key (sitting alongside other top-level keys like model
and run
).
The below example shows two such named groups. The first does not specify a subset of techs or locations and is thus applied across the entire model. In the example, we use cost_max
with the co2
cost class to specify a model-wide emissions limit (assuming the technologies in the model have co2
costs associated with them). We also use the demand_share_min
constraint to force wind and PV to supply at least 40% of electricity demand in Germany, which is modelled as two locations (North and South):
run:
...
model:
...
group_constraints:
# A constraint group to apply a systemwide CO2 cap
systemwide_co2_cap:
cost_max:
co2: 100000
# A constraint group to enforce renewable generation in Germany
renewable_minimum_share_in_germany:
techs: ['wind', 'pv']
locs: ['germany_north', 'germany_south']
demand_share_min:
electricity: 0.4
When specifying group constraints, a named group must give at least one constraint, but can list an arbitrary amount of constraints, and optionally give a subset of techs and locations:
group_constraints:
group_name:
techs: [] # Optional, can be left out if empty
locs: [] # Optional, can be left out if empty
# Any number of constraints can be specified for the given group
constraint_1: ...
constraint_2: ...
...
The below table lists all available group constraints.
Note that when computing the share for demand_share
constraints, only demand
technologies are counted, and that when computing the share for supply_share
constraints, supply
and supply_plus
technologies are counted.
Constraint |
Dimensions |
Description |
---|---|---|
|
carriers |
Minimum share of carrier demand met from a set of technologies across a set of locations, on average over the entire model period. |
|
carriers |
Maximum share of carrier demand met from a set of technologies across a set of locations, on average over the entire model period. |
|
carriers |
Share of carrier demand met from a set of technologies across a set of locations, on average over the entire model period. |
|
carriers |
Minimum share of carrier demand met from a set of technologies across a set of locations, in each individual timestep. |
|
carriers |
Maximum share of carrier demand met from a set of technologies across a set of locations, in each individual timestep. |
|
carriers |
Share of carrier demand met from a set of technologies across a set of locations, in each individual timestep. |
|
carriers |
Turns the per-timestep share of carrier demand met from a set of technologies across a set of locations into a model decision variable. |
|
carriers |
Minimum share of carrier production met from a set of technologies across a set of locations, on average over the entire model period. |
|
carriers |
Maximum share of carrier production met from a set of technologies across a set of locations, on average over the entire model period. |
|
carriers |
Share of carrier production met from a set of technologies across a set of locations, on average over the entire model period. |
|
carriers |
Minimum share of carrier production met from a set of technologies across a set of locations, in each individual timestep. |
|
carriers |
Maximum share of carrier production met from a set of technologies across a set of locations, in each individual timestep. |
|
carriers |
Share of carrier production met from a set of technologies across a set of locations, in each individual timestep. |
|
carriers |
Minimum share of demand met from transmission technologies into a set of locations, on average over the entire model period. All transmission technologies of the chosen carrier are added automatically and technologies must thus not be defined explicitly. |
|
carriers |
Maximum share of demand met from transmission technologies into a set of locations, on average over the entire model period. All transmission technologies of the chosen carrier are added automatically and technologies must thus not be defined explicitly. |
|
carriers |
Share of demand met from transmission technologies into a set of locations, on average over the entire model. All transmission technologies of the chosen carrier are added automatically and technologies must thus not be defined explicitly. period. |
|
carriers |
Minimum absolute sum of supplied energy (carrier_prod) over all timesteps for a set of technologies across a set of locations. |
|
carriers |
Maximum absolute sum of supplied energy (carrier_prod) over all timesteps for a set of technologies across a set of locations. |
|
carriers |
Exact absolute sum of supplied energy (carrier_prod) over all timesteps for a set of technologies across a set of locations. |
|
carriers |
Minimum sum of consumed energy (carrier_con) over all timesteps for a set of conversion/demand technologies across a set of locations. Values are negative and are relative to zero, i.e. a minimum value of -1 means that consumed energy must be < -1 |
|
carriers |
Maximum sum of consumed energy (carrier_con) over all timesteps for a set of conversion/demand technologies across a set of locations. Values are negative and are relative to zero, i.e. a maximum value of -1 means that consumed energy must be > -1 |
|
carriers |
Exact sum of consumed energy (carrier_con) over all timesteps for a set of conversion/demand technologies across a set of locations. Values are negative. |
|
costs |
Maximum total cost from a set of technologies across a set of locations. |
|
costs |
Minimum total cost from a set of technologies across a set of locations. |
|
costs |
Total cost from a set of technologies across a set of locations must equal given value. |
|
costs |
Maximum variable cost from a set of technologies across a set of locations. |
|
costs |
Minimum variable cost from a set of technologies across a set of locations. |
|
costs |
Variable cost from a set of technologies across a set of locations must equal given value. |
|
costs |
Maximum investment cost from a set of technologies across a set of locations. |
|
costs |
Minimum investment cost from a set of technologies across a set of locations. |
|
costs |
Investment cost from a set of technologies across a set of locations must equal given value. |
|
– |
Minimum share of installed capacity from a set of technologies across a set of locations. |
|
– |
Maximum share of installed capacity from a set of technologies across a set of locations. |
|
– |
Exact share of installed capacity from a set of technologies across a set of locations. |
|
– |
Minimum installed capacity from a set of technologies across a set of locations. |
|
– |
Maximum installed capacity from a set of technologies across a set of locations. |
|
– |
Exact installed capacity from a set of technologies across a set of locations. |
|
– |
Minimum resource area used by a set of technologies across a set of locations. |
|
– |
Maximum resource area used by a set of technologies across a set of locations. |
|
– |
Exact resource area used by a set of technologies across a set of locations. |
For specifics of the mathematical formulation of the available group constraints, see Group constraints in the mathematical formulation section.
See also
The built-in national-scale example’s scenarios.yaml
shows two example uses of group constraints: limiting shared capacity with energy_cap_max
and enforcing a minimum shared power generation with carrier_prod_share_min
.
Per-distance constraints and costs¶
Transmission technologies can additionally specify per-distance efficiency (loss) with energy_eff_per_distance
and per-distance costs with energy_cap_per_distance
:
techs:
my_transmission_tech:
essentials:
...
constraints:
# "efficiency" (1-loss) per unit of distance
energy_eff_per_distance: 0.99
costs:
monetary:
# cost per unit of distance
energy_cap_per_distance: 10
The distance is specified in transmission links:
links:
location1,location2:
my_transmission_tech:
distance: 500
constraints:
energy_cap.max: 10000
If no distance is given, but the locations have been given lat and lon coordinates, Calliope will compute distances automatically (based on the length of a straight line connecting the locations).
One-way transmission links¶
Transmission links are bidirectional by default. To force unidirectionality for a given technology along a given link, you have to set the one_way
constraint in the constraint definition of that technology, for that link:
links:
location1,location2:
transmission-tech:
constraints:
one_way: true
This will only allow transmission from location1
to location2
. To swap the direction, the link name must be inverted, i.e. location2,location1
.
Cyclic storage¶
With storage
and supply_plus
techs, it is possible to link the storage at either end of the timeseries, using cyclic storage. This allows the user to better represent multiple years by just modelling one year. Cyclic storage is activated by default (to deactivate: run.cyclic_storage: false
). As a result, a technology’s initial stored energy at a given location will be equal to its stored energy at the end of the model’s last timestep.
For example, for a model running over a full year at hourly resolution, the initial storage at Jan 1st 00:00:00 will be forced equal to the storage at the end of the timestep Dec 31st 23:00:00. By setting storage_initial
for a technology, it is also possible to fix the value in the last timestep. For instance, with run.cyclic_storage: true
and a storage_initial
of zero, the stored energy must be zero by the end of the time horizon.
Without cyclic storage in place (as was the case prior to v0.6.2), the storage tech can have any amount of stored energy by the end of the timeseries. This may prove useful in some cases, but has less physical meaning than assuming cyclic storage.
Note
Cyclic storage also functions when time clustering, if allowing storage to be tracked between clusters (see Time resolution adjustment). However, it cannot be used in operate
run mode.
Revenue and export¶
It is possible to specify revenues for technologies simply by setting a negative cost value. For example, to consider a feed-in tariff for PV generation, it could be given a negative operational cost equal to the real operational cost minus the level of feed-in tariff received.
Export is an extension of this, allowing an energy carrier to be removed from the system without meeting demand. This is analogous to e.g. domestic PV technologies being able to export excess electricity to the national grid. A cost (or negative cost: revenue) can then be applied to export.
Note
Negative costs can be applied to capacity costs, but the user must an ensure a capacity limit has been set. Otherwise, optimisation will be unbounded.
Binary and mixed-integer constraints¶
Calliope models are purely linear by default. However, several constraints can turn a model into a binary or mixed-integer model. Because solving problems with binary or integer variables takes considerably longer than solving purely linear models, it usually makes sense to carefully consider whether the research question really necessitates going beyond a purely linear model.
By applying a purchase
cost to a technology, that technology will have a binary variable associated with it, describing whether or not it has been “purchased”.
By applying units.max
, units.min
, or units.equals
to a technology, that technology will have a integer variable associated with it, describing how many of that technology have been “purchased”. If a purchase
cost has been applied to this same technology, the purchasing cost will be applied per unit.
Warning
Integer and binary variables are a recent addition to Calliope and may not cover all edge cases as intended. Please raise an issue on GitHub if you see unexpected behavior.
Asynchronous energy production/consumption¶
The asynchronous_prod_con
binary constraint ensures that only one of carrier_prod
and carrier_con
can be non-zero in a given timestep.
This constraint can be applied to storage or transmission technologies. This example shows use with a heat transmission technology:
heat_pipes:
constraints:
force_asynchronous_prod_con: true
In the above example, heat pipes which distribute thermal energy in the network may be prone to dissipating heat in an unphysical way. I.e. given that they have distribution losses associated with them, in any given timestep, a link could produce and consume energy in the same timestep, losing energy to the atmosphere in both instances, but having a net energy transmission of zero. This might allow e.g. a CHP facility to overproduce heat to produce more cheap electricity, and have some way of dumping that heat. Enabling the asynchronous_prod_con
constraint ensures that this does not happen.
User-defined custom constraints¶
It is possible to pass custom constraints to the Pyomo backend, using the backend interface. This requires an understanding of the structure of Pyomo constraints. As an example, the following code reproduces the constraint which limits the maximum carrier consumption to less than or equal to the technology capacity:
model = calliope.Model(...)
model.run() # or `model.run(build_only=True)` if you don't want the model to be optimised before adding the new constraint
constraint_name = 'max_capacity_90_constraint'
constraint_sets = ['loc_techs_supply']
def max_capacity_90_constraint_rule(backend_model, loc_tech):
return backend_model.energy_cap[loc_tech] <= (
backend_model.energy_cap_max[loc_tech] * 0.9
)
# Add the constraint
model.backend.add_constraint(constraint_name, constraint_sets, max_capacity_90_constraint_rule)
# Rerun the model with new constraint.
new_model = model.backend.rerun() # `new_model` is a calliope model *without* a backend, it is only useful for saving the results to file
Note
We like the convention that constraint names end with ‘constraint’ and constraint rules have the same text, with an appended ‘_rule’, but you are not required to follow this convention to have a working constraint.
model.run(force_rerun=True)
will not implement the new constraint,model.backend.rerun()
is required. If you runmodel.run(force_rerun=True)
, the backend model will be rebuilt, killing any changes you’ve made.
Previous: Tutorial 3: Mixed Integer Linear Programming | Next: Advanced features