How to impose renewable energy targets
This how-to shows how variable renewable energy is modelled in SpineOpt and how renewable targets and carbon policies can be imposed on a model: a lower limit on renewable generation, an upper limit on carbon emissions and a carbon tax. Each method is illustrated with a small example system, which is available as example files.
Modelling variable renewable energy
A variable renewable energy (VRE) source, such as a wind farm or a solar park, is a unit with only an output flow:
- Add the unit, e.g.
wind_farm, and a unit__to_node entity from the unit to the electricity node. The unit needs no input flow, because its 'fuel' is free. - Give the unit__to_node entity a capacity_per_unit, i.e. the installed capacity.
- Give the unit an availability_factor as a time series of values between 0 and 1, i.e. the generation profile of the weather year. The available capacity in each time step is the capacity_per_unit multiplied by the availability_factor.
The unit can produce any amount up to the available capacity, so the model curtails the VRE generation automatically when it is not needed. To penalise curtailment, give the unit a curtailment_cost: it adds the cost of the unused available capacity to the objective function. A vom_cost on the unit__to_node entity can be used for the operation and maintenance costs that depend on the generation.
Since VRE generation has no fuel cost, the model always uses it before other units. Therefore, a lower limit on renewable generation only affects the results when the model has to choose between renewable sources with a cost, e.g. biomass, or when it can invest in new renewable capacity.
Example system
The example system has an electricity node with a demand of 150, 160, 170 and 140 MW in four hourly time steps, and three units:
| unit | capacity (MW) | input | efficiency | cost (EUR/MWh of electricity) | CO2 (t/MWh of electricity) |
|---|---|---|---|---|---|
| wind_farm | 100 | - | - | 0 | 0 |
| gas_plant | 200 | gas_node | 0.5 | 50 | 0.4 |
| bio_plant | 30 | biomass_node | 0.4 | 80 | 0 |
The system is available as the example file renewable_targets_base.json. You can import it into an empty input database, or build it yourself: set up the workflow and import the templates as in the Simple system tutorial, and then add the entities and values below.
| class | entities |
|---|---|
| node | electricity_node, gas_node, biomass_node, co2_node |
| unit | wind_farm, gas_plant, bio_plant |
| node__to_unit | gas_node ǀ gas_plant, biomass_node ǀ bio_plant |
| unit__to_node | wind_farm ǀ electricity_node, gas_plant ǀ electricity_node, bio_plant ǀ electricity_node, gas_plant ǀ co2_node |
| unit_flow__unit_flow | gas_plant ǀ electricity_node ǀ gas_node ǀ gas_plant, bio_plant ǀ electricity_node ǀ biomass_node ǀ bio_plant, gas_plant ǀ co2_node ǀ gas_node ǀ gas_plant |
| report__output | report1 ǀ unit_flow |
The emissions are modelled as a flow from gas_plant to co2_node. Its flow_ratio_equality_coefficient is the CO2 emitted per MWh of gas, 0.2 t/MWh, which is 0.4 t per MWh of electricity with the efficiency of 0.5. The fuel nodes and the CO2 node have the balance_type none, so they can supply or receive any amount.
| class | entity byname | parameter name | value |
|---|---|---|---|
| model | simple | model_start | 2000-01-01T00:00:00 |
| model | simple | model_end | 2000-01-01T04:00:00 |
| temporal_block | flat | resolution | 1h |
| node | electricity_node | demand | time series, see below |
| node | gas_node | balance_type | none |
| node | biomass_node | balance_type | none |
| node | co2_node | balance_type | none |
| unit | wind_farm | availability_factor | time series, see below |
| node__to_unit | gas_node ǀ gas_plant | vom_cost | 25.0 |
| node__to_unit | biomass_node ǀ bio_plant | vom_cost | 32.0 |
| unit__to_node | wind_farm ǀ electricity_node | capacity_per_unit | 100.0 |
| unit__to_node | gas_plant ǀ electricity_node | capacity_per_unit | 200.0 |
| unit__to_node | bio_plant ǀ electricity_node | capacity_per_unit | 30.0 |
| unit_flow__unit_flow | gas_plant ǀ electricity_node ǀ gas_node ǀ gas_plant | flow_ratio_equality_coefficient | 0.5 |
| unit_flow__unit_flow | bio_plant ǀ electricity_node ǀ biomass_node ǀ bio_plant | flow_ratio_equality_coefficient | 0.4 |
| unit_flow__unit_flow | gas_plant ǀ co2_node ǀ gas_node ǀ gas_plant | flow_ratio_equality_coefficient | 0.2 |
The vom_cost is paid for the fuel input, so with the efficiencies, the costs per MWh of electricity are 25 / 0.5 = 50 EUR for gas_plant and 32 / 0.4 = 80 EUR for bio_plant. The two time series have an hourly resolution:
| t | demand of electricity_node | availability_factor of wind_farm |
|---|---|---|
| 2000-01-01T00:00:00 | 150.0 | 0.8 |
| 2000-01-01T01:00:00 | 160.0 | 0.5 |
| 2000-01-01T02:00:00 | 170.0 | 0.2 |
| 2000-01-01T03:00:00 | 140.0 | 0.9 |
The unit_flow results to electricity_node are (MW):
| unit | 00:00 | 01:00 | 02:00 | 03:00 | total (MWh) |
|---|---|---|---|---|---|
| wind_farm | 80 | 50 | 20 | 90 | 240 |
| gas_plant | 70 | 110 | 150 | 50 | 380 |
| bio_plant | 0 | 0 | 0 | 0 | 0 |
wind_farm produces all it can, gas_plant covers the rest of the demand and the more expensive bio_plant is not used. The CO2 emissions are 0.4 × 380 = 152 t. The objective value is 19000 EUR.
Each of the following methods is a change to this system. With each of them, bio_plant replaces 30 MW of gas_plant in every hour, so the results are:
| unit | 00:00 | 01:00 | 02:00 | 03:00 | total (MWh) |
|---|---|---|---|---|---|
| wind_farm | 80 | 50 | 20 | 90 | 240 |
| gas_plant | 40 | 80 | 120 | 20 | 260 |
| bio_plant | 30 | 30 | 30 | 30 | 120 |
Imposing a lower limit on renewable production
In energy system models, especially in investment models, it is common to impose a lower limit on the amount of electricity generated by renewable sources. In SpineOpt, this is done with the flow_limits_min_cumulative parameter, which triggers the constraint_total_cumulated_unit_flow. The parameter is defined on a unit__to_node entity, and it sets a lower bound on the sum of the flows from the unit to the node over the whole optimisation horizon.
The value is an absolute value, so make sure that its units match the ones of the unit_flow variable, e.g. MWh when the flows are in MW and the time steps in hours. Defining the target as a share of the demand is not yet implemented, so the share has to be converted to an absolute value by the user.
The parameter can also be defined for unit groups and node groups. With a unit group, e.g. of all renewable units, the target covers the total generation of the group. With a node group, the target covers the flows to several nodes, e.g. the electricity nodes of a region.
When you define a parameter for a unit group and a node, SpineOpt also creates a flow from the group itself to the node, as if the group were an extra unit. This flow has no capacity and no cost, so the group would supply energy for free. To prevent this, set flow_limits_fix = 0 on the same entity. The target itself is not affected, as it sums the flows of the group members.
In the example, the renewable units wind_farm and bio_plant have to produce at least 360 MWh together. They form the unit group renewables: right-click the unit class, select Add entity group..., enter the name, move bio_plant and wind_farm to the members with >> and press OK. Then add the unit__to_node entity renewables ǀ electricity_node with the following values:
| class | entity byname | parameter name | value |
|---|---|---|---|
| unit__to_node | renewables ǀ electricity_node | flow_limits_min_cumulative | 360.0 |
| unit__to_node | renewables ǀ electricity_node | flow_limits_fix | 0.0 |
wind_farm already produces all it can (240 MWh), so bio_plant has to produce the missing 120 MWh at its full capacity. The objective value is 22600 EUR. The model is available as the example file renewable_targets_min_renewable_flow.json.
Imposing an upper limit on carbon emissions
Over the entire optimisation horizon
To impose a limit on the total carbon emissions over the entire optimisation horizon, i.e. a carbon budget, the emissions need to be modelled as flows:
- node: a node that represents the carbon emissions, e.g. the atmosphere as a fictive CO2 node. To distinguish e.g. between regions, there can also be more than one carbon node.
- unit__to_node: a flow from each emitting unit to the carbon node.
- unit_flow__unit_flow and flow_ratio_equality_coefficient: the ratio of the carbon flow to another flow of the unit, e.g. the fuel input as in the example system, or the electricity output. For a full list of possible ratios, see unit_flow__unit_flow and its parameters.
- flow_limits_max_cumulative: an upper limit on the sum of the flows from a unit to a node over the whole optimisation horizon, see constraint_total_cumulated_unit_flow. It can be defined for the unit__to_node entity of one unit and the carbon node, or for a unit group of e.g. all conventional units and the carbon node.
In the example, the CO2 emissions of gas_plant are limited to 104 t:
| class | entity byname | parameter name | value |
|---|---|---|---|
| unit__to_node | gas_plant ǀ co2_node | flow_limits_max_cumulative | 104.0 |
This allows 104 / 0.4 = 260 MWh of gas generation, so bio_plant has to produce the missing 120 MWh at its full capacity. The result is the same as with the renewable target, and the objective value is 22600 EUR. The model is available as the example file renewable_targets_carbon_cap.json.
For a specific period of time (advanced method)
If the emissions should not be capped over the entire optimisation horizon, but for specific periods of time, e.g. to impose decreasing carbon caps over time, an alternative method can be used.
To illustrate this, we assume a fictitious cap of 100 for the period 2025-2030 and a cap of 50 for the period 2030-2035. In this simple example, one carbon-emitting unit carbon_unit has two outgoing flows, electricity and carbon.
Three nodes are required to represent this system: an electricity node, a carbon_cap_1 node (with storage_active = true and storage_state_max = 100), and a carbon_cap_2 node (with storage_active = true and storage_state_max = 50).
Further, we introduce the unit_flow__unit_flow entities carbon_unit ǀ carbon_cap_1 ǀ carbon_unit ǀ electricity and carbon_unit ǀ carbon_cap_2 ǀ carbon_unit ǀ electricity. On these entities, we define the ratio between emissions and electricity production. In this fictitious example, we assume 0.5 units of emissions per unit of electricity.
The flow_ratio_equality_coefficient parameter is defined as a time-varying parameter in the following way (simplified representation of the time series):
- flow_ratio_equality_coefficient of
carbon_unit ǀ carbon_cap_1 ǀ carbon_unit ǀ electricity= [2025: 0.5; 2030: 0] - flow_ratio_equality_coefficient of
carbon_unit ǀ carbon_cap_2 ǀ carbon_unit ǀ electricity= [2025: 0; 2030: 0.5]
This way, the first emission cap node carbon_cap_1 can only be "filled" during 2025-2030, while carbon_cap_2 can only be "filled" during the second period 2030-2035.
Note that it would also be possible to have, e.g., one node with a time-varying storage_state_max. However, in this case, "unused" carbon emissions in the first period of time would be available for the second period of time.
Imposing a carbon tax
To include carbon pricing in a model, the emissions are modelled as flows to a carbon node, as for the carbon cap above. The carbon tax is then defined with the tax_in_unit_flow parameter of the carbon node: it adds the tax for all unit_flows into the node to the objective function. To distinguish e.g. between regions, there can also be more than one carbon node.
In the example, the carbon tax is 100 EUR/t:
| class | entity byname | parameter name | value |
|---|---|---|---|
| node | co2_node | tax_in_unit_flow | 100.0 |
This raises the cost of gas_plant from 50 to 50 + 0.4 × 100 = 90 EUR/MWh of electricity, which is more than the 80 EUR/MWh of bio_plant, so bio_plant produces at its full capacity. The objective value is 33000 EUR, of which 10400 EUR is the tax on the remaining 104 t of CO2. The model is available as the example file renewable_targets_carbon_tax.json.