Skip to content

Commit 11a2423

Browse files
author
Michael Lindner
authored
apply power limit to gross imports/exports (#123)
* apply power limit to gross imports/exports * use query to hopefully avoid fragmentation * fix query * improve linopy syntax * improve auxiliary variables, solve fragmentation issue
1 parent 98a27b5 commit 11a2423

File tree

4 files changed

+92
-47
lines changed

4 files changed

+92
-47
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ cutouts
8585

8686
# custom local files
8787
local
88+
figures
8889
# private dev folder
8990
dev/*
9091

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Changelog
2-
2+
- Bugfix: Enforce stricter power import limit to avoid that import from one country compensate from exports to another
33
- Added the IIASA database to the repository and disabled re-downloading it by default.
44
- Simplified IIASA database download, rename `iiasa_database` config section to `pypsa-de`
55
- Updated technology-data to v0.13.4

config/config.de.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
# docs in https://pypsa-eur.readthedocs.io/en/latest/configuration.html#run
66
run:
7-
prefix: 20251015_improve_database
7+
prefix: 20251017_improve_power_limits
88
name:
99
# - ExPol
1010
- KN2045_Mix

scripts/pypsa-de/additional_functionality.py

Lines changed: 89 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -102,60 +102,104 @@ def add_power_limits(n, investment_year, limits_power_max):
102102
"""
103103
" Restricts the maximum inflow/outflow of electricity from/to a country.
104104
"""
105+
106+
def add_pos_neg_aux_variables(n, idx, infix):
107+
"""
108+
For every snapshot in the network `n` this functions adds auxiliary variables corresponding to the positive and negative parts of the dynamical variables of the network components specified in the index `idx`. The `infix` parameter is used to create unique names for the auxiliary variables and constraints.
109+
110+
Parameters
111+
----------
112+
n : pypsa.Network
113+
The PyPSA network object containing the model.
114+
idx : pandas.Index
115+
The index of the network component (e.g., lines or links) for which to create auxiliary variables.
116+
infix : str
117+
A string used to create unique names for the auxiliary variables and constraints.
118+
"""
119+
120+
var_key = f"{idx.name}-{'s' if idx.name == 'Line' else 'p'}"
121+
var = n.model[var_key].sel({idx.name: idx})
122+
aux_pos = n.model.add_variables(
123+
name=f"{var_key}-{infix}-aux-pos",
124+
lower=0,
125+
coords=[n.snapshots, idx],
126+
)
127+
aux_neg = n.model.add_variables(
128+
name=f"{var_key}-{infix}-aux-neg",
129+
upper=0,
130+
coords=[n.snapshots, idx],
131+
)
132+
n.model.add_constraints(
133+
aux_pos >= var,
134+
name=f"{var_key}-{infix}-aux-pos-constr",
135+
)
136+
n.model.add_constraints(
137+
aux_neg <= var,
138+
name=f"{var_key}-{infix}-aux-neg-constr",
139+
)
140+
return aux_pos, aux_neg
141+
105142
for ct in limits_power_max:
106143
if investment_year not in limits_power_max[ct].keys():
107144
continue
108145

109-
limit = 1e3 * limits_power_max[ct][investment_year] / 10
146+
lim = 1e3 * limits_power_max[ct][investment_year] # in MW
110147

111148
logger.info(
112-
f"Adding constraint on electricity import/export from/to {ct} to be < {limit} MW"
149+
f"Adding constraint on electricity import/export from/to {ct} to be < {lim} MW"
113150
)
114-
incoming_line = n.lines.index[
115-
(n.lines.carrier == "AC")
116-
& (n.lines.bus0.str[:2] != ct)
117-
& (n.lines.bus1.str[:2] == ct)
118-
]
119-
outgoing_line = n.lines.index[
120-
(n.lines.carrier == "AC")
121-
& (n.lines.bus0.str[:2] == ct)
122-
& (n.lines.bus1.str[:2] != ct)
123-
]
151+
# identify interconnectors
124152

125-
incoming_link = n.links.index[
126-
(n.links.carrier == "DC")
127-
& (n.links.bus0.str[:2] != ct)
128-
& (n.links.bus1.str[:2] == ct)
129-
]
130-
outgoing_link = n.links.index[
131-
(n.links.carrier == "DC")
132-
& (n.links.bus0.str[:2] == ct)
133-
& (n.links.bus1.str[:2] != ct)
134-
]
153+
incoming_lines = n.lines.query(
154+
f"not bus0.str.startswith('{ct}') and bus1.str.startswith('{ct}') and active"
155+
)
156+
outgoing_lines = n.lines.query(
157+
f"bus0.str.startswith('{ct}') and not bus1.str.startswith('{ct}') and active"
158+
)
159+
incoming_links = n.links.query(
160+
f"not bus0.str.startswith('{ct}') and bus1.str.startswith('{ct}') and carrier == 'DC' and active"
161+
)
162+
outgoing_links = n.links.query(
163+
f"bus0.str.startswith('{ct}') and not bus1.str.startswith('{ct}') and carrier == 'DC' and active"
164+
)
165+
166+
# define auxiliary variables for positive and negative parts of line and link flows
167+
168+
incoming_lines_aux_pos, incoming_lines_aux_neg = add_pos_neg_aux_variables(
169+
n, incoming_lines.index, f"incoming-{ct}"
170+
)
171+
172+
outgoing_lines_aux_pos, outgoing_lines_aux_neg = add_pos_neg_aux_variables(
173+
n, outgoing_lines.index, f"outgoing-{ct}"
174+
)
175+
176+
incoming_links_aux_pos, incoming_links_aux_neg = add_pos_neg_aux_variables(
177+
n, incoming_links.index, f"incoming-{ct}"
178+
)
179+
180+
outgoing_links_aux_pos, outgoing_links_aux_neg = add_pos_neg_aux_variables(
181+
n, outgoing_links.index, f"outgoing-{ct}"
182+
)
135183

136-
# iterate over snapshots - otherwise exporting of postnetwork fails since
137-
# the constraints are time dependent
138-
for t in n.snapshots:
139-
incoming_line_p = n.model["Line-s"].loc[t, incoming_line]
140-
outgoing_line_p = n.model["Line-s"].loc[t, outgoing_line]
141-
incoming_link_p = n.model["Link-p"].loc[t, incoming_link]
142-
outgoing_link_p = n.model["Link-p"].loc[t, outgoing_link]
143-
144-
lhs = (
145-
incoming_link_p.sum()
146-
- outgoing_link_p.sum()
147-
+ incoming_line_p.sum()
148-
- outgoing_line_p.sum()
149-
) / 10
150-
# divide by 10 to avoid numerical issues
151-
152-
cname_upper = f"Power-import-limit-{ct}-{t}"
153-
cname_lower = f"Power-export-limit-{ct}-{t}"
154-
155-
n.model.add_constraints(lhs <= limit, name=cname_upper)
156-
n.model.add_constraints(lhs >= -limit, name=cname_lower)
157-
158-
# not adding to network as the shadow prices are not needed
184+
# To constraint the absolute values of imports and exports, we have to sum the
185+
# corresponding positive and negative flows separately, using the auxiliary variables
186+
187+
import_lhs = (
188+
incoming_links_aux_pos.sum(dim="Link")
189+
+ incoming_lines_aux_pos.sum(dim="Line")
190+
- outgoing_links_aux_neg.sum(dim="Link")
191+
- outgoing_lines_aux_neg.sum(dim="Line")
192+
) / 10
193+
194+
export_lhs = (
195+
outgoing_links_aux_pos.sum(dim="Link")
196+
+ outgoing_lines_aux_pos.sum(dim="Line")
197+
- incoming_links_aux_neg.sum(dim="Link")
198+
- incoming_lines_aux_neg.sum(dim="Line")
199+
) / 10
200+
201+
n.model.add_constraints(import_lhs <= lim / 10, name=f"Power-import-limit-{ct}")
202+
n.model.add_constraints(export_lhs <= lim / 10, name=f"Power-export-limit-{ct}")
159203

160204

161205
def h2_import_limits(n, investment_year, limits_volume_max):

0 commit comments

Comments
 (0)