forked from PyPSA/PyPSA
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_lopf_basic_constraints.py
More file actions
220 lines (187 loc) · 6.37 KB
/
test_lopf_basic_constraints.py
File metadata and controls
220 lines (187 loc) · 6.37 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Tue Feb 1 15:20:12 2022.
@author: fabian
"""
import pandas as pd
import pytest
from conftest import SUPPORTED_APIS, optimize
from pypsa.descriptors import expand_series
from pypsa.descriptors import get_switchable_as_dense as get_as_dense
from pypsa.descriptors import nominal_attrs
TOLERANCE = 1e-2
def describe_storage_unit_contraints(n):
"""
Checks whether all storage units are balanced over time.
This function requires the network to contain the separate variables
p_store and p_dispatch, since they cannot be reconstructed from p.
The latter results from times tau where p_store(tau) > 0 **and**
p_dispatch(tau) > 0, which is allowed (even though not economic).
Therefor p_store is necessarily equal to negative entries of p, vice
versa for p_dispatch.
"""
sus = n.storage_units
sus_i = sus.index
if sus_i.empty:
return
sns = n.snapshots
c = "StorageUnit"
pnl = n.pnl(c)
description = {}
eh = expand_series(n.snapshot_weightings.stores, sus_i)
stand_eff = expand_series(1 - n.df(c).standing_loss, sns).T.pow(eh)
dispatch_eff = expand_series(n.df(c).efficiency_dispatch, sns).T
store_eff = expand_series(n.df(c).efficiency_store, sns).T
inflow = get_as_dense(n, c, "inflow") * eh
spill = eh[pnl.spill.columns] * pnl.spill
description["Spillage Limit"] = pd.Series(
{"min": (inflow[spill.columns] - spill).min().min()}
)
if "p_store" in pnl:
soc = pnl.state_of_charge
store = store_eff * eh * pnl.p_store # .clip(upper=0)
dispatch = 1 / dispatch_eff * eh * pnl.p_dispatch # (lower=0)
start = soc.iloc[-1].where(
sus.cyclic_state_of_charge, sus.state_of_charge_initial
)
previous_soc = stand_eff * soc.shift().fillna(start)
reconstructed = (
previous_soc.add(store, fill_value=0)
.add(inflow, fill_value=0)
.add(-dispatch, fill_value=0)
.add(-spill, fill_value=0)
)
description["SOC Balance StorageUnit"] = (
(reconstructed - soc).unstack().describe()
)
return pd.concat(description, axis=1, sort=False)
def describe_nodal_balance_constraint(n):
"""
Helper function to double check whether network flow is balanced.
"""
network_injection = (
pd.concat(
[
n.pnl(c)[f"p{inout}"].rename(columns=n.df(c)[f"bus{inout}"])
for inout in (0, 1)
for c in ("Line", "Transformer")
],
axis=1,
)
.groupby(level=0, axis=1)
.sum()
)
return (
(n.buses_t.p - network_injection)
.unstack()
.describe()
.to_frame("Nodal Balance Constr.")
)
def describe_upper_dispatch_constraints(n):
"""
Recalculates the minimum gap between operational status and nominal
capacity.
"""
description = {}
key = " Upper Limit"
for c, attr in nominal_attrs.items():
dispatch_attr = "p0" if c in ["Line", "Transformer", "Link"] else attr[0]
description[c + key] = pd.Series(
{
"min": (
n.df(c)[attr + "_opt"] * get_as_dense(n, c, attr[0] + "_max_pu")
- n.pnl(c)[dispatch_attr]
)
.min()
.min()
}
)
return pd.concat(description, axis=1)
def describe_lower_dispatch_constraints(n):
description = {}
key = " Lower Limit"
for c, attr in nominal_attrs.items():
if c in ["Line", "Transformer", "Link"]:
dispatch_attr = "p0"
description[c] = pd.Series(
{
"min": (
n.df(c)[attr + "_opt"] * get_as_dense(n, c, attr[0] + "_max_pu")
+ n.pnl(c)[dispatch_attr]
)
.min()
.min()
}
)
else:
dispatch_attr = attr[0]
description[c + key] = pd.Series(
{
"min": (
-n.df(c)[attr + "_opt"]
* get_as_dense(n, c, attr[0] + "_min_pu")
+ n.pnl(c)[dispatch_attr]
)
.min()
.min()
}
)
return pd.concat(description, axis=1)
def describe_store_contraints(n):
"""
Checks whether all stores are balanced over time.
"""
stores = n.stores
stores_i = stores.index
if stores_i.empty:
return
sns = n.snapshots
c = "Store"
pnl = n.pnl(c)
eh = expand_series(n.snapshot_weightings.stores, stores_i)
stand_eff = expand_series(1 - n.df(c).standing_loss, sns).T.pow(eh)
start = pnl.e.iloc[-1].where(stores.e_cyclic, stores.e_initial)
previous_e = stand_eff * pnl.e.shift().fillna(start)
return (
(previous_e - pnl.p - pnl.e).unstack().describe().to_frame("SOC Balance Store")
)
def describe_cycle_constraints(n):
weightings = n.lines.x_pu_eff.where(n.lines.carrier == "AC", n.lines.r_pu_eff)
def cycle_flow(sub):
C = pd.DataFrame(sub.C.todense(), index=sub.lines_i())
if C.empty:
return None
C_weighted = 1e5 * C.mul(weightings[sub.lines_i()], axis=0)
return C_weighted.apply(lambda ds: ds @ n.lines_t.p0[ds.index].T)
return (
pd.concat([cycle_flow(sub) for sub in n.sub_networks.obj], axis=0)
.unstack()
.describe()
.to_frame("Cycle Constr.")
)
funcs = (
[
describe_cycle_constraints,
# describe_store_contraints,
# describe_storage_unit_contraints,
describe_nodal_balance_constraint,
describe_lower_dispatch_constraints,
describe_upper_dispatch_constraints,
],
)
@pytest.fixture
def solved_network(ac_dc_network, api):
n = ac_dc_network
optimize(n, api)
n.lines["carrier"] = n.lines.bus0.map(n.buses.carrier)
return n
@pytest.mark.parametrize("func", *funcs)
@pytest.mark.parametrize("api", SUPPORTED_APIS)
def test_tolerance(solved_network, api, func):
n = solved_network
description = func(n).fillna(0)
for col in description:
assert abs(description[col]["min"]) < TOLERANCE
if "max" in description:
assert description[col]["max"] < TOLERANCE