Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Operational reserve constraint with energy storage #1158

Open
pz-max opened this issue Jul 17, 2024 · 2 comments
Open

Operational reserve constraint with energy storage #1158

pz-max opened this issue Jul 17, 2024 · 2 comments

Comments

@pz-max
Copy link
Collaborator

pz-max commented Jul 17, 2024

  • 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
@fneum
Copy link
Member

fneum commented Jul 17, 2024

related #643

@pz-max
Copy link
Collaborator Author

pz-max commented Jul 31, 2024

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants