Dispatch and Redispatch Modelling#
Overview#
The GB model implements a two-stage optimization approach to capture realistic electricity market operations and network constraints:
Unconstrained Dispatch - Optimal market dispatch without internal GB transmission constraints
Constrained Redispatch - Adjustments to respect internal GB transmission constraints with bid/offer costs
This approach reflects real GB electricity market operations where the initial, day-ahead market dispatch is followed by a balancing market to resolve transmission constraints.
![digraph dispatch_flow {
rankdir=TB;
node [shape=box, style="rounded,filled"];
// Input nodes
fes_data [label="FES outputs\n(asset & flex capacities,\nannual demand, marginal costs)", fillcolor="#E8F4F8", shape=box];
network_data [label="Network Data\n(transmission lines)", fillcolor="#E8F4F8", shape=box];
timeseries_data [label="Profiles\n(renewables capacity factors,\ndemand profiles)", fillcolor="#E8F4F8"];
strike_prices [label="CfD Strike Prices\n(Low carbon contracts)", fillcolor="#E8F4F8"];
multipliers [label="Bid/Offer multipliers", fillcolor="#E8F4F8"];
boundary_caps [label="ETYS Boundary\nCapabilities", fillcolor="#E8F4F8"];
// Process nodes
compose [label="Compose Network\n(attach generators, loads,\ninterconnectors, CHP)", fillcolor="#D4E6F1", fontsize=11];
unconstrained [label="Unconstrained Dispatch\n(minimize system cost,\nno boundary constraints)", fillcolor="#AED6F1", fontsize=11];
calc_bids [label="Calculate Bid/Offer\nProfiles\n(interconnectors,\ngenerators)", fillcolor="#D4E6F1", fontsize=11];
prepare_constrained [label="Prepare Constrained\nNetwork\n(apply bid/offer costs)", fillcolor="#D4E6F1", fontsize=11];
constrained [label="Constrained Redispatch\n(minimize redispatch cost,\nrespect ETYS boundaries)", fillcolor="#AED6F1", fontsize=11];
calc_cost [label="Calculate Constraint\nCosts\n(difference in dispatch)", fillcolor="#D4E6F1", fontsize=11];
// Output nodes
unconstrained_result [label="Unconstrained Results\n(optimal dispatch,\nprices)", fillcolor="#C8E6C9"];
constrained_result [label="Constrained Results\n(feasible dispatch,\nredispatch costs)", fillcolor="#C8E6C9"];
constraint_cost [label="Total Constraint\nCost", fillcolor="#C8E6C9"];
// Flow
fes_data -> compose;
network_data -> compose;
timeseries_data -> compose;
compose -> unconstrained [label=" no boundary\n constraints", fontsize=10];
unconstrained -> unconstrained_result;
unconstrained_result -> calc_bids;
strike_prices -> calc_bids;
multipliers -> calc_bids;
calc_bids -> prepare_constrained;
compose -> prepare_constrained [label=" base network", fontsize=10];
unconstrained_result -> prepare_constrained;
prepare_constrained -> constrained [label=" with bid/offer\n costs", fontsize=10];
boundary_caps -> constrained [label=" ETYS\n constraints", fontsize=10];
constrained -> constrained_result;
constrained_result -> calc_cost;
calc_cost -> constraint_cost;
}](../_images/graphviz-060347d28739d0262c2ae28fb2afd19fdd685ce4.png)
Dispatch and Redispatch Process Flow#
Process#
Stage 1: Unconstrained (day-ahead) dispatch#
The unconstrained dispatch represents the economically optimal dispatch without considering internal GB transmission constraints. By default, we optimise with a perfect foresight at an hourly resolution for individual years.
Objective: Minimize total system cost
Where:
\(MC_g\) - Marginal cost of generator g (GBP/MWh)
\(MC_s\) - Marginal cost of storage unit s (GBP/MWh)
\(p_{g,t}\) - Power output of generator g at time t
\(p_{s,t}\) - Power output of storage unit s at time t
Constraints:
Supply-demand balance at each bus
Generator capacity limits
Minimum generation levels (e.g., heat-led CHP constraints)
Interconnector flow limits
Storage state of charge limits
Two changes to the default PyPSA-Eur constraints are made during the unconstrained dispatch stage:
We remove all line and link limits within the GB network and we remove Kirchhoff-Voltage-Law (linearised power flow) constraints throughout the entire network. We apply the first to “copperplate” the GB bidding zone (no transmission constraints between intra-GB regions). We apply the second to ensure all intra-GB regional market prices align in each time period.
We set upper and lower bounds on annual nuclear power capacity factors, to limit their dispatchability. The bounds can be found within the configuration file.
Output:
Optimal dispatch schedule
Nodal electricity prices
Interconnector flows
Stage 2: Constrained (balancing market) redispatch#
The constrained redispatch modifies the unconstrained dispatch to respect ETYS (Electricity Ten Year Statement) boundary capabilities, as well as individual line limits of the GB high-voltage transmission network. By default, we optimise with a perfect foresight at an hourly resolution for individual years.
Key Modifications:
Fixed dispatch & redispatch generators
The initial dispatch profiles for generators, GB → neighbour interconnectors, and storage units are all fixed to their optimal values from stage 1. We also impose limits on intra-GB transmission using GB Electricity Ten-Year Statement (ETYS) boundary constraints, defined below. Generators (except nuclear power), interconnectors, and storage units can deviate from their optimal dispatch via virtual
upanddowngenerators that we create for each asset.downgenerators can only remove energy from the system,upgenerators can only add energy to it. To these virtual generators we then apply redispatch (bid/offer) costs.Note
This process requires updates to core PyPSA-Eur storage constraints to (a) fix their optimal dispatch correctly and (b) include the virtual generators in the storage energy balance constraint.
Bid/Offer Costs Applied
Generators that deviate from unconstrained dispatch incur bid (decrease) or offer (increase) costs based on technology-specific multipliers. The modified marginal cost becomes:
\[\begin{split}MC'_g = \begin{cases} MC_g \times offer\_multiplier & \text{if increase from unconstrained} \\ MC_g \times bid\_multiplier & \text{if decrease from unconstrained} \\ \end{cases}\end{split}\]ETYS Boundary Constraints
Transmission boundaries between ETYS regions are constrained to their capabilities:
\[\sum_{line \in boundary} flow_{line} \leq capability_{boundary}\]Several transmission lines cross each boundary. Some lines cross several boundaries, such as offshore HVDC lines that connect northern Scotland with central England. The boundary capabilities are scaled in shoulder and summer seasons to reflect the impact of thermal limits on the lines crossing those boundaries. This scaling is configurable and based initially on values defined by NESO in table 2.3 of their network options assessment methodology.
By default, transmission lines are not otherwise constrained to their individual physical capacities. This behaviour can be changed in the configuration so that individual transmission line capacities, as calculated by the PyPSA-Eur workflow, can also constraint intra-GB flows.
Rest of Europe
At this stage, the operation of assets in the rest of Europe is fixed, with no scope for deviation. In fact, we don’t care about distinguishing between European countries, we only care about the bid/offer costs on each interconnector with GB. We assume that GB can fully deviate from the use of these interconnectors as defined in the initial dispatch stage. That is, if it is exporting at full capacity in the optimal dispatch, it can feasibly redispatch to importing at full capacity. We assume that the rest of Europe can absorb this change without a change of redispatch costs along the interconnectors.
Since we only care about the redispatch costs on the interconnectors, we simplify the rest of Europe at this stage to a single node with an infinite store that can inject/remove any quantity of energy from the system at zero additional cost.
Objective: Minimize redispatch cost
Where:
\(RP\) - The redispatch penalty cost.
Bid/Offer Profile Calculation#
See also
For details on the different system components defined here, see: System Overview.
Conventional Generators
Multipliers are based on historical bids and offers.
For conventional assets (e.g., fossil-fuelled power plants), the bid cost is a premium minus the asset’s short-run marginal cost. This premium reflects a penalty paid by the system operator in lieu of profit lost by the asset operator. The offer cost is the asset’s short-run marginal cost plus a premium, to reflect additional profits gained in the balancing market.
Low-Carbon Generators with Contracts for Difference
For low-carbon generators with Contracts for Difference (CfD), the bid/offer costs are based on the strike price (the subsidy received by the operator) minus the asset short-run marginal cost. This value is applied as a cost to the system whether the asset operator is increasing or decreasing output in redispatch. It reflects a cost to the system operator to account for lost subsidy (bids) and to pay for additional generation (offers).
In rare cases, the strike price we have may be lower than our modelled short-run marginal costs. For instance, if using wood pellet fuel prices for waste plants, their marginal costs exceed the strike price. This highlights the problem of matching real, historical CfD data with modelled future costs. In these cases, to mitigate strange model behaviour, we set the bid/offer costs to zero rather than allowing them to become negative.
Other Generators & Storage Units
Some system components have no historical bid/offer data, nor do we have subsidy information (e.g., geothermal plants, run-of-river hydropower). For these plants, we do one of two things:
Directly map their short run marginal costs to their bid/offer costs. Therefore, when bidding, the system receives a revenue equal to the cost of generating that power.
Directly map a multiplier from another component
For storage units (pumped hydro and batteries), we could apply a multiplier (based on historical battery bid/offer costs). However, storage redispatch costs are based on the opportunity cost of dispatching / storing energy in future time periods. This opportunity cost is already reflected in the optimisation problem since it has perfect foresight for the model year. Therefore, we set the storage redispatch costs to their short-run marginal costs (which are generally negligible).
Demand-side response
Demand-side response (DSR) can be redispatched and zero bid/offer costs are applied. As with storage units, we assume DSR aggregators would be receiving revenue from capacity markets and so would not need additional payments in balancing markets.
Interconnectors:
Interconnector bid/offer costs are given as an hourly timeseries profile, derived from the unconstrained dispatch. There are six options available to interconnectors during redispatch, as outlined in the National Grid Long-term Market and Constraint Modelling methodology document.
Currently importing into GB and offering to import more.
Currently importing into GB and bidding to import less or switch to exporting.
Currently not in use (“floating”) and offering to import.
Currently not in use (“floating”) and bidding to export.
Currently exporting from GB and offering to export less or switch to importing.
Currently exporting from GB and bidding to export more.
These can be collapsed into four profiles as we consider (1) & (3) and (4) & (6) to be equivalent.
Offering to increase imports: Treated as generator offering power
\[Cost_{offer}^{import+} = |\lambda_{GB} - \lambda_{neighbor}| + offer_{neighbor} \cdot (1 + \eta_{loss})\]Bidding to decrease imports: Treated as generator bidding to reduce
\[Cost_{bid}^{import-} = \lambda_{GB} - \lambda_{neighbor} \cdot (1 + \eta_{loss}) - bid_{neighbor} \cdot (1 + \eta_{loss})\]Offering to decrease exports: Treated as generator offering power
\[Cost_{offer}^{export-} = \lambda_{neighbor} \cdot (1 - \eta_{loss}) - \lambda_{GB} - offer_{neighbor} \cdot (1 - \eta_{loss})\]Bidding to increase exports: Treated as generator bidding to reduce
\[Cost_{bid}^{export+} = |\lambda_{GB} - \lambda_{neighbor}| - bid_{neighbor} \cdot (1 - \eta_{loss})\]
Where:
\(\lambda_{GB}\) - GB marginal price (GBP/MWh)
\(\lambda_{neighbor}\) - Neighbor’s marginal price (GBP/MWh)
\(offer_{neighbor}\) - Neighbor’s marginal plant offer cost (GBP/MWh)
\(bid_{neighbor}\) - Neighbor’s marginal plant bid cost (GBP/MWh)
\(\eta_{loss}\) - Interconnector loss rate (fraction)
The neighbours marginal plant is selected as the plant anywhere in Europe with marginal cost closest to the neighbour’s marginal price. This is because any plant could be setting the marginal price, via intra-European interconnectors. The bid/offer costs of this plant are calculated as in GB, using the same multipliers. However, since we do not have information on renewables subsidy mechanisms in each European country, if a low-carbon plant is setting the price, we do not set bids/offers to a strike price but rather to the plant’s marginal cost. This can mean that there are times with negligible bid/offer costs on the European side of the interconnector.
Redispatch penalty#
The redispatch costs are sometimes formed in such a way that the system benefits financially from bidding off with one generator and bidding on with another an equal amount. This happens in particular with interconnectors, in periods where is is cheaper to import more electricity than, say, ramp down a CCGT plant. This is an optimisation quirk rather than reflection of how the balancing market operates. In reality, overall redispatching is always kept to an absolute minimum.
To ensure we do not have excess redispatching in the optimisation, both bids and offers are penalised in the objective function. This is a non-monetary penalisation that is added directly to the optimisation problem and is only used to mitigate this optimisation quirk; PyPSA network statistics (e.g., opex, revenue, market_value) will not include this penalty.
The value of the penalty is configurable, to allow for it to be tweaked to be just large enough to mitigate excess redispatching but small enough not to cause numerical instability in the optimisation.
Analysing results#
Result files can be found in the results directory.
Each year in the horizon of interest is optimised separately and a file for each year is stored in the following directory structure:
results/{run}/networks/
├── unconstrained_clustered/ # Stage 1 results
│ └── {year}.nc
└── constrained_clustered/ # Stage 2 results
└── {year}.nc
In addition, once all runs have completed, a results/{run}/constraint_costs.csv will be available, which gives a single value constraint cost for the entire modelling horizon, plus the final year’s cost duplicated N times to simulate costs until the end of current infrastructure asset lifetimes.
Configuration#
The workflow is configured under redispatch in the model configuration.
redispatch:
year_range_incl: [2025, 2044] # 20-year period over which we will run the simulation
constraint_cost_extra_years: 20
unconstrain_lines_and_links: true
no_redispatch_carriers:
- nuclear
# Scaling based on thermal limits defined in table 2.3 of the networks options assessment methodology
# https://www.neso.energy/document/285321/download
monthly_boundary_capability_scaling:
1: 1.0
2: 1.0
3: 0.85
4: 0.85
5: 0.85
6: 0.8
7: 0.8
8: 0.8
9: 0.85
10: 0.85
11: 0.85
12: 1.0
redispatch_profit_mitigation_penalty: 250
elexon: # Elexon API data
technology_mapping: # ignoring renewables in mapping as they come from CfD data
coal: coal
CCGT: CCGT
OCGT: OCGT
# We do not collect battery data by default since we do not want to calculate a bid/offer multiplier for storage devices.
# BATTERY: battery
BIOMASS: biomass
DIESEL: oil
GAS: engine
NUCLEAR: nuclear
years: [2020, 2021, 2022, 2023, 2024]
api_bmu_fuel_map: false # Boolean to decide source of Elexon BMU unit - fuel map, If True - data is fetched using API else uses data uploaded to the repo
max_concurrent_requests: 4
bid_offer_multiplier_mapping:
purchased H2: CCGT
Implementation Notes#
Data Processing Workflow#
The redispatch workflow is built through rules/gb-model/redispatch.smk.
Note
The graph above was generated using:
pixi run filtered_rulegraph \
"results/GB/networks/HT/constrained_clustered/2040.nc \
-w fes_scenario -w year \
-c compose_network \
-f rules/gb-model/redispatch.smk -s 11,8" \
"doc/gb-model/img/redispatch_workflow.svg"
The filtered_rulegraph task allows us to trim the full DAG to the redispatch-related workflow slice while retaining the upstream steps that build the PyPSA network.
Key Assumptions#
Storage is redispatched at the short-run marginal cost since the storage opportunity cost is reflected in the overall optimisation.
Historical redispatch is suitable for defining bid/offer multipliers of conventional generators
Historical low carbon contracts (e.g., CfDs) are suitable for defining future renewable energy bids/offers.
Inverted bid/offer costs, caused by short-run marginal prices being higher than contracts subsidies for some low carbon generators, are not allowed; costs are instead set to zero.
European renewable energy subsidy schemes are ignored when considering marginal generator bid/offer costs in interconnector redispatch cost calculations.
Electrolyser and demand-side response load profiles can be adapted at zero cost in response to redispatched generation.
Nuclear power is not able to redispatch due to ramping limitations.
Renewable generators that were curtailed in the dispatch run may _increase_ there generation (bid on) during redispatch.