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

Add operational reserve margin constraint analogous to GenX #358

Merged
merged 2 commits into from Apr 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions config.default.yaml
Expand Up @@ -44,6 +44,12 @@ electricity:
co2base: 1.487e+9
agg_p_nom_limits: data/agg_p_nom_minmax.csv

operational_reserve: # like https://genxproject.github.io/GenX/dev/core/#Reserves
activate: true
epsilon_load: 0.02 # share of total load
epsilon_vres: 0.02 # share of total renewable supply
contingency: 4000 # fixed capacity in MW

extendable_carriers:
Generator: []
StorageUnit: [] # battery, H2
Expand Down
3 changes: 3 additions & 0 deletions doc/release_notes.rst
Expand Up @@ -22,6 +22,9 @@ Energy Security Release (April 2022)
* old: ``estimate_renewable_capacities_from_capacity_stats``
* new: ``estimate_renewable_capacities``

* Add operational reserve margin constraint analogous to `GenX implementation <https://genxproject.github.io/GenX/dev/core/#Reserves>`_.
Can be activated with config setting ``electricity: operational_reserve:``.

* Add function to add global constraint on use of gas in :mod:`prepare_network`. This can be activated with `electricity: gaslimit:` given in MWh.

* The powerplants that have been shut down before 2021 are filtered out.
Expand Down
75 changes: 73 additions & 2 deletions scripts/solve_network.py
Expand Up @@ -84,8 +84,9 @@
import re

import pypsa
from pypsa.linopf import (get_var, define_constraints, linexpr, join_exprs,
network_lopf, ilopf)
from pypsa.linopf import (get_var, define_constraints, define_variables,
linexpr, join_exprs, network_lopf, ilopf)
from pypsa.descriptors import get_switchable_as_dense as get_as_dense

from pathlib import Path
from vresutils.benchmark import memory_logger
Expand Down Expand Up @@ -211,6 +212,73 @@ def add_SAFE_constraints(n, config):
define_constraints(n, lhs, '>=', rhs, 'Safe', 'mintotalcap')


def add_operational_reserve_margin_constraint(n, config):

reserve_config = config["electricity"]["operational_reserve"]
EPSILON_LOAD = reserve_config["epsilon_load"]
EPSILON_VRES = reserve_config["epsilon_vres"]
CONTINGENCY = reserve_config["contingency"]

# Reserve Variables
reserve = get_var(n, 'Generator', 'r')

# Share of extendable renewable capacities
ext_i = n.generators.query('p_nom_extendable').index
vres_i = n.generators_t.p_max_pu.columns
capacity_factor = n.generators_t.p_max_pu[vres_i.intersection(ext_i)]
renewable_capacity_variables = get_var(n, 'Generator', 'p_nom')[vres_i.intersection(ext_i)]

# Left-hand-side
lhs = (
linexpr((1, reserve)).sum(1) +
linexpr((-EPSILON_VRES * capacity_factor, renewable_capacity_variables)).sum(1)
)


# Total demand at t
demand = n.loads_t.p.sum(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(1)

# Right-hand-side
rhs = EPSILON_LOAD * demand + EPSILON_VRES * potential + CONTINGENCY

define_constraints(n, lhs, '>=', rhs, "Reserve margin")


def update_capacity_constraint(n):
gen_i = n.generators.index
ext_i = n.generators.query('p_nom_extendable').index
fix_i = n.generators.query('not p_nom_extendable').index

dispatch = get_var(n, 'Generator', 'p')
reserve = get_var(n, 'Generator', 'r')

capacity_variable = get_var(n, 'Generator', 'p_nom')
capacity_fixed = n.generators.p_nom[fix_i]

p_max_pu = get_as_dense(n, 'Generator', 'p_max_pu')

lhs = linexpr((1, dispatch), (1, reserve))
lhs += linexpr((-p_max_pu[ext_i], capacity_variable)).reindex(columns=gen_i, fill_value='')

rhs = (p_max_pu[fix_i] * capacity_fixed).reindex(columns=gen_i, fill_value=0)

define_constraints(n, lhs, '<=', rhs, 'Generators', 'updated_capacity_constraint')


def add_operational_reserve_margin(n, sns, config):

define_variables(n, 0, np.inf, 'Generator', 'r', axes=[sns, n.generators.index])

add_operational_reserve_margin_constraint(n, config)

update_capacity_constraint(n)


def add_battery_constraints(n):
nodes = n.buses.index[n.buses.carrier == "battery"]
if nodes.empty or ('Link', 'p_nom') not in n.variables.index:
Expand All @@ -236,6 +304,9 @@ def extra_functionality(n, snapshots):
add_SAFE_constraints(n, config)
if 'CCL' in opts and n.generators.p_nom_extendable.any():
add_CCL_constraints(n, config)
reserve = config["electricity"].get("operational_reserve", {})
if reserve.get("activate"):
add_operational_reserve_margin(n, snapshots, config)
for o in opts:
if "EQ" in o:
add_EQ_constraints(n, o)
Expand Down