Market Mechanisms#

A Market Mechanism is used to execute the clearing, scheduled by the MarketRole in base_market.py

The method signature for the market_mechanism is given as:

def clearing_mechanism_name(
  market_agent: MarketRole,
  market_products: list[MarketProduct],
):
  accepted_orders: Orderbook = []
  rejected_orders: Orderbook = []
  meta: list[Meta] = []
  return accepted_orders, rejected_orders, meta

The market_mechanism is called by the MarketRole, which is the agent that is responsible for the market. It is called with the market_agent and the market_products, which are the products that are traded in the current opening of the market. This gives maximum flexibility as it allows to access properties from the MarketRole directly. The market_mechanism returns a list of accepted orders, a list of rejected orders and a list of meta information (for each tradable market product or trading zone, if needed). The meta information is used to store information about the clearing, e.g. the min and max price, the cleared demand volume and supply volume, as well as the information about the cleared product.

In the Market Mechanism, the MarketRole is available to access the market configuration with market_agent.marketconfig and the available Orders from previous clearings through market_agent.all_orders. In the future, the MarketMechanism will be a class which contains the additional information like grid information without changing the MarketRole.

The available market mechanisms are the following:

  1. assume.markets.clearing_algorithms.simple.PayAsClearRole()

  2. assume.markets.clearing_algorithms.simple.PayAsBidRole()

  3. assume.markets.clearing_algorithms.complex_clearing.ComplexClearingRole()

  4. assume.markets.clearing_algorithms.complex_clearing_dmas.ComplexDmasClearingRole()

  5. assume.markets.clearing_algorithms.redispatch.RedispatchMarketRole()

  6. assume.markets.clearing_algorithms.nodal_clearing.NodalClearingRole()

  7. assume.markets.clearing_algorithms.contracts.PayAsBidContractRole()

The PayAsClearRole performs an electricity market clearing using a pay-as-clear mechanism. This means that the clearing price is the highest price that is still accepted. This price is then valid for all accepted orders. For this, the demand and supply are separated, before the demand is sorted from highest to lowest order price and the supply lowest to highest order price. Where those two curves in a price over power plot meet, the market is cleared for the price at the intersection. All supply orders with a price below and all demand orders above are accepted. Where the price is equal, only partial volume is accepted.

The PayAsBidRole clears the market in the same manner as the pay-as-clear mechanism, but the accepted_price is the price of the supply order for both the demand order and the supply orders that meet this demand.

Complex clearing#

The ComplexClearingRole performs an electricity market clearing using an optimization to clear the market. Here, also profile block and linked orders are supported. The objective function is a social welfare maximization, which is equivalent to a cost minimization:

\[\min \left( {\sum_{b \in \mathcal{B}}\quad{u_b \: C_{b} \: P_{b, t}} \: T} \right),\]

where \(\mathcal{B}\) is the set of all submitted bids, \(C_{b}\) is the bid price, \(P_{b, t}\) is the volume offered (demand is negative) and \(T\) is the clearing horizon of 24 hours. Decision variables are the acceptance ratio \(u_b\) with \(u_b \in [0, 1] \quad \forall \: b \in \mathcal{B}\), and the clearing status \(x_b\) with \(x_b \in \{0, 1\} \: \forall \: b \in \mathcal{B}\).

The optimization problem is subject to the following constraints:

The energy balance constraint: \(\quad \sum_{b \in \mathcal{B}} P_{b, t} \: u_b = 0 \quad \forall \: t \in \mathcal{T}\),

The minimum acceptance ratio constraint: \(\quad u_{b} \geq U_{b} \: x_{b} \quad \mathrm{and} \quad u_{b} \leq x_{b} \quad \forall \: b \in \mathcal{B}\),

with the minimum acceptance ratio \(U_{b}\) defined for each bid b.

The linked bid constraint, ensuring that the acceptance of child bids c is below the acceptance of their parent bids p is given by: \(\mathbf{a}_{c, p} \: u_c \leq u_{p} \quad \forall \: c, p \in \mathcal{B}\),

with the incidence matrix \(\mathbf{a}_{c, p}\) defining the links between bids as 1, if c is linked as child to p, 0 else.

Flows in the network are limited by the Net Transfer Capacity (‘s_nom’ * ‘s_max_pu’) of each line l: \(\quad -NTC_{l} \leq F_{l, t} \leq NTC_{l} \quad \forall \: l \in \mathcal{L}, t \in \mathcal{T}\),

Because with this algorithm, paradoxically accepted bids (PABs) can occur, the objective is solved in an iterative manner:

  1. The optimization problem is solved with the objective function and all constraints.

  2. The binary variables \(x_b\) are fixed to the current solution.

  3. The optimization problem is solved again without the minimum acceptance ratio constraint.

  4. The market clearing prices are given as the dual variables of the energy balance constraint.

  5. The surplus of each bid is calculated as the difference between the bid price and the market clearing price.

  6. If the surplus for one or more bids is negative, the clearing status \(x_b\) for those bids is set to 0 and the algorithm starts again with step 1.

If you want a hands-on use-case of the complex clearing check out the prepared tutorial in Colab: https://colab.research.google.com/github/assume-framework/assume

Nodal clearing#

The NodalClearingRole performs an electricity market clearing of the bids submitted by market participants using an optimal power flow (OPF) approach. Profile, block and linked orders are not supported. The algorithm utilizes PyPSA to solve the OPF problem, allowing for a physics based representation of network constraints.

Modeling Redispatch in ASSUME#

This section demonstrates the modeling and simulation of the redispatch mechanism using PyPSA as a plug-and-play module within the ASSUME framework. Modeling redispatch in ASSUME using PyPSA consists of two main steps: first, identifying network congestion based on the cleared Energy-Only Market (EOM) dispatch, and second, resolving congestion by optimizing upward and downward redispatch actions.

Overview of Network Modeling in PyPSA#

The PyPSA network model can be created to visualize line flows using EOM clearing outcomes of generation and loads at different nodes (locations).

PyPSA uses the following terminology to define grid infrastructure:

Attributes#

  1. Bus: Nodes (locations) where power plants and loads are connected
    • name: Unique identifier of the bus

    • v_nom: Nominal voltage of the node

    • carrier: Energy carrier, which can be “AC” or “DC” for electricity buses

    • x: Longitude coordinate

    • y: Latitude coordinate

  2. Generator: Power plants that generate electricity
    • name: Unique identifier of the generator

    • p_nom: Nominal power capacity, used as a limit in optimization

    • p_max_pu: Maximum output for each snapshot as a fraction of p_nom

    • p_min_pu: Minimum output for each snapshot as a fraction of p_nom

    • p_set: Active power set point (for power flow analysis)

    • marginal_cost: Marginal cost of producing 1 MWh

    • carrier: Energy carrier, such as “AC” or “DC”

    • x: Longitude coordinate

    • y: Latitude coordinate

    • sign: Power sign (positive by default)

  3. Load: Demand units that consume electricity
    • name: Unique identifier of the load

    • p_set: Active power set point (for inflexible demand)

    • marginal_cost: Marginal cost of producing 1 MWh

    • x: Longitude coordinate

    • y: Latitude coordinate

    • sign: Power sign (negative by default)

  4. Line: Transmission grids that transmit electricity
    • name: Unique identifier of the line

    • s_nom: Nominal transmission capacity of apparent power in MVA

    • s_max_pu: Maximum loading as a fraction of s_nom (value between 0 and 1)

    • capital_cost: Capital cost for extending lines by 1 MVA

    • r: Resistance in Ohms

    • bus0: First node the line is connected to

    • bus1: Second node the line is connected to

    • x: Series reactance

    • s_nom_extendable: Flag to enable s_nom expansion

  5. Network: The PyPSA network to which all the components are integrated
    • name: Unique identifier of the network

    • snapshots: List of timesteps (e.g., hourly, quarter-hourly, daily, etc.)

Congestion Identification#

The first step is to check whether the cleared EOM dispatch leads to congestion in the network. The cleared EOM generation is assigned to the corresponding PyPSA generators via generators_t.p_set. Demand is assigned as PyPSA loads via loads_t.p_set. Since demand bids in ASSUME are commonly represented with negative volumes, these values are converted to positive load values before being passed to PyPSA.

network.generators_t.p_set = gen_p_set
network.loads_t.p_set = load_p_set.abs()

ASSUME uses PyPSA’s linear power-flow method network.lpf() for the identification of congestion. The method is computationally efficient and suitable for congestion checks after market simulations of large-scale systems. The resulting line flows are retrieved from network.lines_t.p0 and compared with the available transmission capacity:

line_loading = network.lines_t.p0.abs() / ( network.lines.s_nom * network.lines.s_max_pu )

If the line loading exceeds 1, the corresponding line is congested and redispatch optimization is triggered.

Redispatch of Power Plants#

Once congestion is detected, ASSUME optimizes redispatch actions to relieve overloaded lines. In the redispatch formulation, cleared EOM generator dispatch is represented directly on the generator side. The cleared generator schedule is assigned through generators_t.p_set for linear power-flow analysis and is also reflected in the generator bounds using p_min_pu = p_max_pu. This ensures that the pre-redispatch dispatch is treated as fixed while redispatch flexibility is represented separately through additional upward and downward redispatch generators.

Steps for Redispatch#

  1. Fixing cleared EOM generator dispatch

The cleared EOM dispatch of each generator is fixed using two complementary representations. First, generators_t.p_set is used for the linear power-flow calculation. Second, the same dispatch is converted into per-unit values and assigned as equal lower and upper bounds:

p_set_pu = cleared_dispatch / p_nom
generators_t.p_min_pu = p_set_pu
generators_t.p_max_pu = p_set_pu

This prevents the base generator dispatch from being freely changed during redispatch optimization.

  1. Representing upward redispatch

Upward redispatch is modeled through additional generator components with positive sign. The available upward redispatch capacity is limited by the difference between the availability-adjusted maximum power and the cleared EOM dispatch:

p_max_pu_up = (max_power - gen_p_set) / p_nom

Here, max_power represents the available maximum generation capacity in the respective timestep, gen_p_set is market cleared capacity and p_nom is the nominal power of the generator.

  1. Representing downward redispatch

Downward redispatch is modeled through additional generator components with negative sign. The available downward redispatch capacity is limited by the difference between the cleared EOM dispatch and the minimum generation level:

p_max_pu_down = (gen_p_set - min_power) / p_nom

A negative generator sign means that dispatching this component reduces the physical generation of the corresponding unit.

  1. Backup redispatch

Additional upward and downward backup generators are added at each node. These backup units have high marginal costs and ensure that the optimization remains feasible if market-based redispatch bids are insufficient to resolve congestion.

Objective#

The redispatch optimization minimizes the net cost of redispatch actions while satisfying network constraints. Upward redispatch is assigned a positive marginal cost. Downward redispatch is assigned the negative of the submitted redispatch price, because reducing generation avoids the corresponding generation cost.

Therefore, the net redispatch cost is evaluated using signed accepted redispatch volumes multiplied by their accepted prices:

\[C_{redispatch} = \sum_{o \in O} V^{accepted}_{o} \cdot P^{accepted}_{o}\]

where \(V^{accepted}_{o}\) is the signed accepted redispatch volume and \(P^{accepted}_{o}\) is the corresponding accepted redispatch price.

Positive accepted volumes correspond to upward redispatch, while negative accepted volumes correspond to downward redispatch.