scr-screen 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- scr_screen/__init__.py +3 -0
- scr_screen/__main__.py +7 -0
- scr_screen/check.py +138 -0
- scr_screen/classical.py +256 -0
- scr_screen/cli.py +368 -0
- scr_screen/cost.py +225 -0
- scr_screen/data/ieee39_assumed.json +2367 -0
- scr_screen/data/ieee39_sites_example.csv +5 -0
- scr_screen/data/plants_template.csv +4 -0
- scr_screen/examples.py +54 -0
- scr_screen/io.py +144 -0
- scr_screen/loaders.py +91 -0
- scr_screen/metrics.py +189 -0
- scr_screen/netmap.py +176 -0
- scr_screen/network.py +177 -0
- scr_screen/report.py +742 -0
- scr_screen/screening.py +108 -0
- scr_screen/settings.py +46 -0
- scr_screen-0.1.0.dist-info/METADATA +273 -0
- scr_screen-0.1.0.dist-info/RECORD +24 -0
- scr_screen-0.1.0.dist-info/WHEEL +5 -0
- scr_screen-0.1.0.dist-info/entry_points.txt +2 -0
- scr_screen-0.1.0.dist-info/licenses/LICENSE +21 -0
- scr_screen-0.1.0.dist-info/top_level.txt +1 -0
scr_screen/__init__.py
ADDED
scr_screen/__main__.py
ADDED
scr_screen/check.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""Check whether a network is ready for SCR-Screen, before scanning.
|
|
2
|
+
|
|
3
|
+
Lists, in plain language, anything missing or unsupported (for example a
|
|
4
|
+
generator without sub-transient reactance), and says which short-circuit
|
|
5
|
+
methods can run.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import math
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
|
|
13
|
+
from .classical import UNSUPPORTED_TABLES, connected_to_source
|
|
14
|
+
from .netmap import MAX_MAP_BUSES, bus_label
|
|
15
|
+
|
|
16
|
+
ERROR, WARNING, INFO = "error", "warning", "info"
|
|
17
|
+
CLASSICAL_SLOW_ABOVE = 5000
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class Finding:
|
|
22
|
+
level: str # error, warning, info
|
|
23
|
+
applies_to: str # "both", "classical", "iec60909", or "report"
|
|
24
|
+
message: str
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class CheckReport:
|
|
29
|
+
n_buses: int
|
|
30
|
+
findings: list[Finding] = field(default_factory=list)
|
|
31
|
+
|
|
32
|
+
def ready(self, method: str) -> bool:
|
|
33
|
+
return not any(f.level == ERROR and f.applies_to in ("both", method) for f in self.findings)
|
|
34
|
+
|
|
35
|
+
def add(self, level, applies_to, message):
|
|
36
|
+
self.findings.append(Finding(level, applies_to, message))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def check_network(net) -> CheckReport:
|
|
40
|
+
live = [int(b) for b in net.bus.index[net.bus["in_service"]]] if len(net.bus) else []
|
|
41
|
+
rep = CheckReport(n_buses=len(live))
|
|
42
|
+
if not live:
|
|
43
|
+
rep.add(ERROR, "both", "The network has no in-service buses.")
|
|
44
|
+
return rep
|
|
45
|
+
|
|
46
|
+
# --- Sources --------------------------------------------------------------
|
|
47
|
+
eg = net.ext_grid[net.ext_grid["in_service"]] if "ext_grid" in net else []
|
|
48
|
+
gens = net.gen[net.gen["in_service"]] if "gen" in net else []
|
|
49
|
+
if len(eg) == 0 and len(gens) == 0:
|
|
50
|
+
rep.add(ERROR, "both", "No in-service external grid or synchronous generator: "
|
|
51
|
+
"there is no source of short-circuit current.")
|
|
52
|
+
|
|
53
|
+
for idx, row in (eg.iterrows() if len(eg) else []):
|
|
54
|
+
name = _name(row, f"external grid {idx}")
|
|
55
|
+
if not _pos(row.get("s_sc_max_mva")) or not _nonneg(row.get("rx_max")):
|
|
56
|
+
rep.add(ERROR, "both", f"{name} (bus {bus_label(net, int(row['bus']))}) needs "
|
|
57
|
+
"s_sc_max_mva and rx_max (short-circuit MVA and R/X of the grid source).")
|
|
58
|
+
|
|
59
|
+
missing_classical, missing_iec = [], []
|
|
60
|
+
for idx, row in (gens.iterrows() if len(gens) else []):
|
|
61
|
+
name = f"{_name(row, f'generator {idx}')} (bus {bus_label(net, int(row['bus']))})"
|
|
62
|
+
if not _pos(row.get("xdss_pu")) or not _pos(row.get("sn_mva")):
|
|
63
|
+
missing_classical.append(name)
|
|
64
|
+
elif not (_pos(row.get("vn_kv")) and _nonneg(row.get("rdss_ohm")) and _pos(row.get("cos_phi"))):
|
|
65
|
+
missing_iec.append(name)
|
|
66
|
+
if missing_classical:
|
|
67
|
+
rep.add(ERROR, "both", f"{len(missing_classical)} generator(s) lack sub-transient data "
|
|
68
|
+
f"(xdss_pu and sn_mva): {_short(missing_classical)}.")
|
|
69
|
+
if missing_iec:
|
|
70
|
+
rep.add(ERROR, "iec60909", f"{len(missing_iec)} generator(s) lack vn_kv, rdss_ohm or cos_phi, "
|
|
71
|
+
f"which IEC 60909 needs: {_short(missing_iec)}.")
|
|
72
|
+
|
|
73
|
+
# --- Elements the classical method does not model in v0.1 ---------------
|
|
74
|
+
for table in UNSUPPORTED_TABLES:
|
|
75
|
+
if table in net and len(net[table]) and "in_service" in net[table]:
|
|
76
|
+
n = int(net[table]["in_service"].sum())
|
|
77
|
+
if n:
|
|
78
|
+
rep.add(ERROR, "classical", f"{n} in-service '{table}' element(s): not modeled by "
|
|
79
|
+
"the classical method in v0.1 (try --method iec60909).")
|
|
80
|
+
if "switch" in net and len(net.switch):
|
|
81
|
+
n = int(((net.switch["et"] == "b") & net.switch["closed"]).sum())
|
|
82
|
+
if n:
|
|
83
|
+
rep.add(ERROR, "classical", f"{n} closed bus-bus switch(es): not supported by the "
|
|
84
|
+
"classical method in v0.1 (merge those buses, or try --method iec60909).")
|
|
85
|
+
|
|
86
|
+
# --- Connectivity ---------------------------------------------------------
|
|
87
|
+
if len(eg) or len(gens):
|
|
88
|
+
reach = connected_to_source(net, set(live))
|
|
89
|
+
isolated = [b for b in live if b not in reach]
|
|
90
|
+
if isolated:
|
|
91
|
+
rep.add(WARNING, "both", f"{len(isolated)} bus(es) have no path to any source and will "
|
|
92
|
+
f"be skipped in scans: {_short([bus_label(net, b) for b in isolated])}.")
|
|
93
|
+
|
|
94
|
+
# --- Information ------------------------------------------------------------
|
|
95
|
+
n_ibr = sum(int(net[t]["in_service"].sum()) for t in ("sgen", "storage") if t in net and len(net[t]))
|
|
96
|
+
if n_ibr:
|
|
97
|
+
rep.add(INFO, "both", f"{n_ibr} inverter-based resource(s) (sgen/storage) found; their "
|
|
98
|
+
"contribution is excluded from short-circuit MVA, as NERC's SCR definition requires.")
|
|
99
|
+
labels = [bus_label(net, b) for b in live]
|
|
100
|
+
if len(set(labels)) != len(labels):
|
|
101
|
+
rep.add(INFO, "report", "Bus names are not unique, so reports label buses by pandapower index.")
|
|
102
|
+
if "geo" in net.bus.columns and net.bus.loc[live, "geo"].notna().all():
|
|
103
|
+
rep.add(INFO, "report", "All buses have coordinates: the map will use them.")
|
|
104
|
+
else:
|
|
105
|
+
rep.add(INFO, "report", "Not all buses have coordinates: the map will use an automatic layout.")
|
|
106
|
+
if len(live) > MAX_MAP_BUSES:
|
|
107
|
+
rep.add(INFO, "report", f"{len(live)} buses: above the {MAX_MAP_BUSES}-bus limit, so the "
|
|
108
|
+
"report will omit the network map.")
|
|
109
|
+
if len(live) > CLASSICAL_SLOW_ABOVE:
|
|
110
|
+
rep.add(WARNING, "classical", f"{len(live)} buses: the classical method may be slow or run "
|
|
111
|
+
"out of memory at this size in v0.1; IEC 60909 may be faster.")
|
|
112
|
+
return rep
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _name(row, default):
|
|
116
|
+
n = row.get("name")
|
|
117
|
+
if n is None or (isinstance(n, float) and math.isnan(n)) or str(n).strip() == "":
|
|
118
|
+
return default
|
|
119
|
+
return str(n)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _pos(v):
|
|
123
|
+
try:
|
|
124
|
+
return v is not None and math.isfinite(float(v)) and float(v) > 0
|
|
125
|
+
except (TypeError, ValueError):
|
|
126
|
+
return False
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _nonneg(v):
|
|
130
|
+
try:
|
|
131
|
+
return v is not None and math.isfinite(float(v)) and float(v) >= 0
|
|
132
|
+
except (TypeError, ValueError):
|
|
133
|
+
return False
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _short(items, n=8):
|
|
137
|
+
items = [str(i) for i in items]
|
|
138
|
+
return ", ".join(items[:n]) + (f" and {len(items) - n} more" if len(items) > n else "")
|
scr_screen/classical.py
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"""Classical flat-start short-circuit MVA (Option B).
|
|
2
|
+
|
|
3
|
+
Method (common U.S. planning practice for SCR screening):
|
|
4
|
+
1. Every bus starts at 1.0 pu voltage ("flat start").
|
|
5
|
+
2. Sources are modeled as impedances to ground:
|
|
6
|
+
- external grids: Z = 1 / S_sc (pu), split by R/X, with NO IEC c-factor;
|
|
7
|
+
- synchronous generators: sub-transient reactance X''d (plus R).
|
|
8
|
+
Inverter-based resources (sgen, storage) are not sources, so their
|
|
9
|
+
contribution is excluded automatically.
|
|
10
|
+
3. Lines and transformers are series impedances. Loads, shunts and line
|
|
11
|
+
charging are ignored; transformers are at nominal ratio.
|
|
12
|
+
4. Build the bus admittance matrix Y, then for each bus i:
|
|
13
|
+
Z_th,i = [Y^-1]_ii and SCMVA_i = S_base / |Z_th,i| (V = 1.0 pu)
|
|
14
|
+
|
|
15
|
+
v0.1 supports: bus, ext_grid, gen, line, trafo (two-winding), and
|
|
16
|
+
line/transformer switches. Other in-service elements raise a clear error
|
|
17
|
+
rather than being silently ignored.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import math
|
|
23
|
+
|
|
24
|
+
import numpy as np
|
|
25
|
+
|
|
26
|
+
from .metrics import InputError
|
|
27
|
+
|
|
28
|
+
S_BASE_MVA = 100.0
|
|
29
|
+
|
|
30
|
+
# Element tables that would change the result but aren't modeled in v0.1.
|
|
31
|
+
UNSUPPORTED_TABLES = ("trafo3w", "impedance", "xward", "ward", "dcline", "tcsc", "ssc", "vsc")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def scmva_classical(net, bus_list: list[int], skip_isolated: bool = False
|
|
35
|
+
) -> tuple[dict[int, float], list[str]]:
|
|
36
|
+
"""Return {bus: SCMVA} using the classical flat-start method, plus notes.
|
|
37
|
+
|
|
38
|
+
Buses with no electrical path to any source cannot have a short-circuit
|
|
39
|
+
level. With skip_isolated=True (used by full-network scans) they are left
|
|
40
|
+
out and listed in the notes; otherwise requesting one raises InputError.
|
|
41
|
+
"""
|
|
42
|
+
_check_supported(net)
|
|
43
|
+
|
|
44
|
+
live = [int(b) for b in net.bus.index[net.bus["in_service"]]]
|
|
45
|
+
reachable = connected_to_source(net, set(live))
|
|
46
|
+
isolated = [b for b in live if b not in reachable]
|
|
47
|
+
buses = [b for b in live if b in reachable]
|
|
48
|
+
pos = {b: k for k, b in enumerate(buses)}
|
|
49
|
+
n = len(buses)
|
|
50
|
+
y = np.zeros((n, n), dtype=complex)
|
|
51
|
+
notes = [
|
|
52
|
+
"Classical flat-start: 1.0 pu pre-fault voltage, no IEC c-factor or "
|
|
53
|
+
"correction factors; loads, shunts and line charging ignored; "
|
|
54
|
+
"transformers at nominal ratio.",
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
open_lines, open_trafos = _open_switches(net)
|
|
58
|
+
|
|
59
|
+
# --- Series branches: lines -------------------------------------------
|
|
60
|
+
for idx, ln in net.line.iterrows():
|
|
61
|
+
if not ln["in_service"] or idx in open_lines:
|
|
62
|
+
continue
|
|
63
|
+
f, t = int(ln["from_bus"]), int(ln["to_bus"])
|
|
64
|
+
if f not in pos or t not in pos:
|
|
65
|
+
continue
|
|
66
|
+
vn = float(net.bus.at[f, "vn_kv"])
|
|
67
|
+
par = float(ln.get("parallel", 1) or 1)
|
|
68
|
+
z_ohm = complex(ln["r_ohm_per_km"], ln["x_ohm_per_km"]) * ln["length_km"] / par
|
|
69
|
+
z_pu = z_ohm * S_BASE_MVA / vn**2
|
|
70
|
+
_add_branch(y, pos[f], pos[t], z_pu, f"line {idx}")
|
|
71
|
+
|
|
72
|
+
# --- Series branches: two-winding transformers ------------------------
|
|
73
|
+
for idx, tr in net.trafo.iterrows():
|
|
74
|
+
if not tr["in_service"] or idx in open_trafos:
|
|
75
|
+
continue
|
|
76
|
+
hv, lv = int(tr["hv_bus"]), int(tr["lv_bus"])
|
|
77
|
+
if hv not in pos or lv not in pos:
|
|
78
|
+
continue
|
|
79
|
+
zk = tr["vk_percent"] / 100.0
|
|
80
|
+
rk = tr["vkr_percent"] / 100.0
|
|
81
|
+
if zk <= 0 or rk < 0 or rk > zk:
|
|
82
|
+
raise InputError(f"Transformer {idx} has invalid vk_percent/vkr_percent.")
|
|
83
|
+
xk = math.sqrt(zk**2 - rk**2)
|
|
84
|
+
par = float(tr.get("parallel", 1) or 1)
|
|
85
|
+
z_pu = complex(rk, xk) * (S_BASE_MVA / tr["sn_mva"]) / par
|
|
86
|
+
_add_branch(y, pos[hv], pos[lv], z_pu, f"transformer {idx}")
|
|
87
|
+
|
|
88
|
+
# --- Sources: external grids ------------------------------------------
|
|
89
|
+
n_sources = 0
|
|
90
|
+
for idx, eg in net.ext_grid.iterrows():
|
|
91
|
+
if not eg["in_service"]:
|
|
92
|
+
continue
|
|
93
|
+
b = int(eg["bus"])
|
|
94
|
+
s_sc = eg.get("s_sc_max_mva", np.nan)
|
|
95
|
+
rx = eg.get("rx_max", np.nan)
|
|
96
|
+
if not _is_pos(s_sc) or not _is_nonneg(rx):
|
|
97
|
+
raise InputError(
|
|
98
|
+
f"External grid {idx} needs s_sc_max_mva and rx_max for the "
|
|
99
|
+
"classical method."
|
|
100
|
+
)
|
|
101
|
+
z_mag = S_BASE_MVA / s_sc
|
|
102
|
+
x = z_mag / math.sqrt(1 + rx**2)
|
|
103
|
+
_add_shunt(y, pos[b], complex(rx * x, x), f"external grid {idx}")
|
|
104
|
+
n_sources += 1
|
|
105
|
+
|
|
106
|
+
# --- Sources: synchronous generators -----------------------------------
|
|
107
|
+
for idx, g in net.gen.iterrows():
|
|
108
|
+
if not g["in_service"]:
|
|
109
|
+
continue
|
|
110
|
+
b = int(g["bus"])
|
|
111
|
+
xdss, sn = g.get("xdss_pu", np.nan), g.get("sn_mva", np.nan)
|
|
112
|
+
if not _is_pos(xdss) or not _is_pos(sn):
|
|
113
|
+
raise InputError(
|
|
114
|
+
f"Generator {idx} needs xdss_pu and sn_mva (sub-transient data) "
|
|
115
|
+
"for the classical method."
|
|
116
|
+
)
|
|
117
|
+
vn = float(net.bus.at[b, "vn_kv"])
|
|
118
|
+
rdss_ohm = g.get("rdss_ohm", 0.0)
|
|
119
|
+
r_pu = 0.0 if not _is_nonneg(rdss_ohm) else rdss_ohm * S_BASE_MVA / vn**2
|
|
120
|
+
x_pu = xdss * S_BASE_MVA / sn
|
|
121
|
+
_add_shunt(y, pos[b], complex(r_pu, x_pu), f"generator {idx}")
|
|
122
|
+
n_sources += 1
|
|
123
|
+
|
|
124
|
+
if n_sources == 0:
|
|
125
|
+
raise InputError("The network has no in-service external grid or generator.")
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
z_bus = np.linalg.inv(y)
|
|
129
|
+
except np.linalg.LinAlgError:
|
|
130
|
+
raise InputError(
|
|
131
|
+
"The network matrix could not be inverted. Part of the network is "
|
|
132
|
+
"probably isolated from every source."
|
|
133
|
+
) from None
|
|
134
|
+
|
|
135
|
+
if isolated:
|
|
136
|
+
notes.append(f"{len(isolated)} bus(es) have no path to any source and were "
|
|
137
|
+
"excluded: " + ", ".join(str(b) for b in isolated[:20])
|
|
138
|
+
+ (" ..." if len(isolated) > 20 else "") + ".")
|
|
139
|
+
values = {}
|
|
140
|
+
for b in bus_list:
|
|
141
|
+
if b in isolated:
|
|
142
|
+
if skip_isolated:
|
|
143
|
+
continue
|
|
144
|
+
raise InputError(f"Bus {b} has no path to any source (isolated part of the network).")
|
|
145
|
+
if b not in pos:
|
|
146
|
+
raise InputError(f"Bus {b} is out of service.")
|
|
147
|
+
z_th = abs(z_bus[pos[b], pos[b]])
|
|
148
|
+
if not math.isfinite(z_th) or z_th <= 0:
|
|
149
|
+
raise InputError(f"Bus {b} has no valid path to a source.")
|
|
150
|
+
values[b] = S_BASE_MVA / z_th
|
|
151
|
+
return values, notes
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
# ---------------------------------------------------------------------------
|
|
155
|
+
|
|
156
|
+
def connected_to_source(net, live: set[int]) -> set[int]:
|
|
157
|
+
"""Buses connected (through in-service lines/transformers) to a grid source or generator."""
|
|
158
|
+
open_lines, open_trafos = _open_switches(net, allow_bus_bus=True)
|
|
159
|
+
adj: dict[int, set[int]] = {b: set() for b in live}
|
|
160
|
+
|
|
161
|
+
def link(a, b):
|
|
162
|
+
if a in adj and b in adj:
|
|
163
|
+
adj[a].add(b)
|
|
164
|
+
adj[b].add(a)
|
|
165
|
+
|
|
166
|
+
for idx, ln in net.line.iterrows():
|
|
167
|
+
if ln["in_service"] and idx not in open_lines:
|
|
168
|
+
link(int(ln["from_bus"]), int(ln["to_bus"]))
|
|
169
|
+
for idx, tr in net.trafo.iterrows():
|
|
170
|
+
if tr["in_service"] and idx not in open_trafos:
|
|
171
|
+
link(int(tr["hv_bus"]), int(tr["lv_bus"]))
|
|
172
|
+
if "trafo3w" in net and len(net.trafo3w):
|
|
173
|
+
for _, t3 in net.trafo3w.iterrows():
|
|
174
|
+
if t3["in_service"]:
|
|
175
|
+
link(int(t3["hv_bus"]), int(t3["mv_bus"]))
|
|
176
|
+
link(int(t3["hv_bus"]), int(t3["lv_bus"]))
|
|
177
|
+
if "impedance" in net and len(net.impedance):
|
|
178
|
+
for _, im in net.impedance.iterrows():
|
|
179
|
+
if im["in_service"]:
|
|
180
|
+
link(int(im["from_bus"]), int(im["to_bus"]))
|
|
181
|
+
if "switch" in net and len(net.switch):
|
|
182
|
+
for _, sw in net.switch.iterrows():
|
|
183
|
+
if sw["et"] == "b" and sw["closed"]:
|
|
184
|
+
link(int(sw["bus"]), int(sw["element"]))
|
|
185
|
+
|
|
186
|
+
stack = []
|
|
187
|
+
for table in ("ext_grid", "gen"):
|
|
188
|
+
if table in net and len(net[table]):
|
|
189
|
+
stack += [int(b) for b in net[table].loc[net[table]["in_service"], "bus"] if int(b) in adj]
|
|
190
|
+
seen = set(stack)
|
|
191
|
+
while stack:
|
|
192
|
+
b = stack.pop()
|
|
193
|
+
for n in adj[b]:
|
|
194
|
+
if n not in seen:
|
|
195
|
+
seen.add(n)
|
|
196
|
+
stack.append(n)
|
|
197
|
+
return seen
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _add_branch(y, i, j, z_pu, label):
|
|
201
|
+
if abs(z_pu) == 0:
|
|
202
|
+
raise InputError(f"{label} has zero impedance; merge the buses instead.")
|
|
203
|
+
adm = 1.0 / z_pu
|
|
204
|
+
y[i, i] += adm
|
|
205
|
+
y[j, j] += adm
|
|
206
|
+
y[i, j] -= adm
|
|
207
|
+
y[j, i] -= adm
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _add_shunt(y, i, z_pu, label):
|
|
211
|
+
if abs(z_pu) == 0:
|
|
212
|
+
raise InputError(f"{label} has zero impedance.")
|
|
213
|
+
y[i, i] += 1.0 / z_pu
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _open_switches(net, allow_bus_bus: bool = False):
|
|
217
|
+
"""Lines and transformers disconnected by an open switch."""
|
|
218
|
+
open_lines, open_trafos = set(), set()
|
|
219
|
+
if "switch" not in net or len(net.switch) == 0:
|
|
220
|
+
return open_lines, open_trafos
|
|
221
|
+
for _, sw in net.switch.iterrows():
|
|
222
|
+
if sw["et"] == "b" and sw["closed"] and not allow_bus_bus:
|
|
223
|
+
raise InputError(
|
|
224
|
+
"Closed bus-bus switches are not supported by the classical "
|
|
225
|
+
"method in v0.1. Merge those buses in the model."
|
|
226
|
+
)
|
|
227
|
+
if not sw["closed"]:
|
|
228
|
+
if sw["et"] == "l":
|
|
229
|
+
open_lines.add(int(sw["element"]))
|
|
230
|
+
elif sw["et"] == "t":
|
|
231
|
+
open_trafos.add(int(sw["element"]))
|
|
232
|
+
return open_lines, open_trafos
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _check_supported(net):
|
|
236
|
+
for table in UNSUPPORTED_TABLES:
|
|
237
|
+
if table in net and len(net[table]) > 0 and "in_service" in net[table]:
|
|
238
|
+
if bool(net[table]["in_service"].any()):
|
|
239
|
+
raise InputError(
|
|
240
|
+
f"The network contains in-service '{table}' elements, which "
|
|
241
|
+
"the classical method does not model in v0.1."
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _is_pos(v) -> bool:
|
|
246
|
+
try:
|
|
247
|
+
return v is not None and math.isfinite(float(v)) and float(v) > 0
|
|
248
|
+
except (TypeError, ValueError):
|
|
249
|
+
return False
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _is_nonneg(v) -> bool:
|
|
253
|
+
try:
|
|
254
|
+
return v is not None and math.isfinite(float(v)) and float(v) >= 0
|
|
255
|
+
except (TypeError, ValueError):
|
|
256
|
+
return False
|