PyPSA/pypsa-eur

Operational reserve constraint with energy storage

pz-max opened this issue · 2 comments

  • Here the GenX publication. Page 31 last & page 32 first paragraph talks about energy storage play a role for reserves (incl hydro etc).

  • Our current PyPSA-Eur implementation does not consider energy storage

    def add_operational_reserve_margin(n, sns, config):
    """
    Build reserve margin constraints based on the formulation given in
    https://genxproject.github.io/GenX/dev/core/#Reserves.
    Parameters
    ----------
    n : pypsa.Network
    sns: pd.DatetimeIndex
    config : dict
    Example:
    --------
    config.yaml requires to specify operational_reserve:
    operational_reserve: # like https://genxproject.github.io/GenX/dev/core/#Reserves
    activate: true
    epsilon_load: 0.02 # percentage of load at each snapshot
    epsilon_vres: 0.02 # percentage of VRES at each snapshot
    contingency: 400000 # MW
    """
    reserve_config = config["electricity"]["operational_reserve"]
    EPSILON_LOAD = reserve_config["epsilon_load"]
    EPSILON_VRES = reserve_config["epsilon_vres"]
    CONTINGENCY = reserve_config["contingency"]
    # Reserve Variables
    n.model.add_variables(
    0, np.inf, coords=[sns, n.generators.index], name="Generator-r"
    )
    reserve = n.model["Generator-r"]
    summed_reserve = reserve.sum("Generator")
    # Share of extendable renewable capacities
    ext_i = n.generators.query("p_nom_extendable").index
    vres_i = n.generators_t.p_max_pu.columns
    if not ext_i.empty and not vres_i.empty:
    capacity_factor = n.generators_t.p_max_pu[vres_i.intersection(ext_i)]
    p_nom_vres = (
    n.model["Generator-p_nom"]
    .loc[vres_i.intersection(ext_i)]
    .rename({"Generator-ext": "Generator"})
    )
    lhs = summed_reserve + (
    p_nom_vres * (-EPSILON_VRES * xr.DataArray(capacity_factor))
    ).sum("Generator")
    # Total demand per t
    demand = get_as_dense(n, "Load", "p_set").sum(axis=1)
    # VRES potential of non extendable generators
    capacity_factor = n.generators_t.p_max_pu[vres_i.difference(ext_i)]
    renewable_capacity = n.generators.p_nom[vres_i.difference(ext_i)]
    potential = (capacity_factor * renewable_capacity).sum(axis=1)
    # Right-hand-side
    rhs = EPSILON_LOAD * demand + EPSILON_VRES * potential + CONTINGENCY
    n.model.add_constraints(lhs >= rhs, name="reserve_margin")

Energy storage should be considered. However, because not all markets might consider storage as a reserve contributor, it should be implemented as an option.

Implementation idea:

  • Avoid introducing if conditions
  • Write in config a boolean (True/False aka. 1/0) for storage addition in the reserve constraint
  • Add terms to the lhs and rhs for the constraint (see here). rhs= old terms + new term * boolean
  • One challenge will be to properly understand what term to add on the lhs and rhs side... Good documentation for the new term will be essential

related #643

Status. This feature request is of interest to the maintainers.
Now, someone just needs to work on adding a good PR to address this.