stepss 3.59__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.
- stepss/__init__.py +66 -0
- stepss/_bundled.py +13 -0
- stepss/cases.py +555 -0
- stepss/extractor.py +750 -0
- stepss/globals.py +154 -0
- stepss/helios.py +836 -0
- stepss/libs/helios_api.h +305 -0
- stepss/libs/lin/libhelios_api.so +0 -0
- stepss/libs/lin/ramses.so +0 -0
- stepss/libs/mac/libhelios_api.dylib +0 -0
- stepss/libs/mac/ramses.so +0 -0
- stepss/libs/ramses.h +41 -0
- stepss/libs/win/helios_api.dll +0 -0
- stepss/libs/win/ramses.dll +0 -0
- stepss/scripts/__init__.py +2 -0
- stepss/scripts/exec.py +38 -0
- stepss/simulator.py +1072 -0
- stepss-3.59.dist-info/METADATA +296 -0
- stepss-3.59.dist-info/RECORD +23 -0
- stepss-3.59.dist-info/WHEEL +5 -0
- stepss-3.59.dist-info/entry_points.txt +2 -0
- stepss-3.59.dist-info/licenses/LICENSE.rst +190 -0
- stepss-3.59.dist-info/top_level.txt +1 -0
stepss/helios.py
ADDED
|
@@ -0,0 +1,836 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Python interface to the Helios AC power-flow engine.
|
|
4
|
+
|
|
5
|
+
Helios is the STEPSS power-flow calculator (successor of the Fortran PFC).
|
|
6
|
+
This module wraps the bundled ``libhelios_api`` C shared library with an
|
|
7
|
+
explicit ctypes binding and exposes :class:`HeliosSession`, a stateful
|
|
8
|
+
session object with a load → (modify) → solve → query lifecycle.
|
|
9
|
+
|
|
10
|
+
Unlike the rest of stepss (which follows the historical camelCase RAMSES
|
|
11
|
+
conventions), this module deliberately uses PEP 8 snake_case naming.
|
|
12
|
+
|
|
13
|
+
Quick start::
|
|
14
|
+
|
|
15
|
+
from stepss.helios import HeliosSession
|
|
16
|
+
|
|
17
|
+
with HeliosSession() as pf:
|
|
18
|
+
pf.load_file("network.dat")
|
|
19
|
+
pf.solve()
|
|
20
|
+
v, angle = pf.get_bus_voltage("A")
|
|
21
|
+
v_all, angle_all = pf.get_bus_voltages()
|
|
22
|
+
|
|
23
|
+
Each session owns an independent solver instance; sessions may be used
|
|
24
|
+
concurrently from different threads (do not share one session between
|
|
25
|
+
threads without locking).
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
import ctypes
|
|
29
|
+
import enum
|
|
30
|
+
import os
|
|
31
|
+
import sys
|
|
32
|
+
from dataclasses import dataclass, field
|
|
33
|
+
|
|
34
|
+
import numpy
|
|
35
|
+
|
|
36
|
+
from .globals import HeliosError, __platlibdir__
|
|
37
|
+
|
|
38
|
+
_c_int_p = ctypes.POINTER(ctypes.c_int)
|
|
39
|
+
_c_double_p = ctypes.POINTER(ctypes.c_double)
|
|
40
|
+
|
|
41
|
+
#: Buffer size sufficient for any element name (mirrors HELIOS_NAME_MAX).
|
|
42
|
+
NAME_MAX = 64
|
|
43
|
+
#: Buffer size for free-text strings such as violations (HELIOS_TEXT_MAX).
|
|
44
|
+
TEXT_MAX = 256
|
|
45
|
+
|
|
46
|
+
#: Oldest / newest C API major.minor this wrapper is tested against.
|
|
47
|
+
_SUPPORTED_API = ((1, 1), (1, None))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class SolverStatus(enum.IntEnum):
|
|
51
|
+
"""Detailed solver status reported by :attr:`HeliosSession.solver_status`."""
|
|
52
|
+
|
|
53
|
+
NOT_RUN = 0
|
|
54
|
+
CONVERGED = 1
|
|
55
|
+
DIVERGED = 2
|
|
56
|
+
MAX_ITERATIONS = 3
|
|
57
|
+
SINGULAR = 4
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class Option(enum.IntEnum):
|
|
61
|
+
"""Solver options accepted by :meth:`HeliosSession.set_option` (HeliosOption enum)."""
|
|
62
|
+
|
|
63
|
+
SBASE = 0 #: system base, MVA
|
|
64
|
+
TOLAC = 1 #: active power tolerance, MW ($TOLAC)
|
|
65
|
+
TOLREAC = 2 #: reactive power tolerance, Mvar ($TOLREAC)
|
|
66
|
+
MAX_ITER = 3 #: maximum iterations ($NBITMA)
|
|
67
|
+
TH_BLOCK = 4 #: tap-blocking mismatch threshold, MVA ($MISBLOC)
|
|
68
|
+
TH_ADJ = 5 #: tap-adjustment mismatch threshold, MVA ($MISADJ)
|
|
69
|
+
TH_QLIM = 6 #: Q-limit switching mismatch threshold, MVA ($MISQLIM)
|
|
70
|
+
PLIM = 7 #: 0/1 slack P-limit relaxation ($PLIM)
|
|
71
|
+
DIVDET = 8 #: divergence detection threshold ($DIVDET)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class BusType(enum.IntEnum):
|
|
75
|
+
"""Bus type reported by :meth:`HeliosSession.get_bus_info`."""
|
|
76
|
+
|
|
77
|
+
PQ = 0
|
|
78
|
+
PV = 1
|
|
79
|
+
SLACK = 2
|
|
80
|
+
ISOLATED = 3
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass
|
|
84
|
+
class BusInfo:
|
|
85
|
+
"""Static and solved data of one bus."""
|
|
86
|
+
|
|
87
|
+
name: str
|
|
88
|
+
v_pu: float
|
|
89
|
+
angle_rad: float
|
|
90
|
+
bus_type: BusType
|
|
91
|
+
vnom_kv: float
|
|
92
|
+
p_load_mw: float
|
|
93
|
+
q_load_mvar: float
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclass
|
|
97
|
+
class BranchInfo:
|
|
98
|
+
"""Static data and solved flows of one branch."""
|
|
99
|
+
|
|
100
|
+
name: str
|
|
101
|
+
from_bus: str
|
|
102
|
+
to_bus: str
|
|
103
|
+
closed: bool
|
|
104
|
+
is_transformer: bool
|
|
105
|
+
snom_mva: float
|
|
106
|
+
p_from_mw: float
|
|
107
|
+
q_from_mvar: float
|
|
108
|
+
p_to_mw: float
|
|
109
|
+
q_to_mvar: float
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@dataclass
|
|
113
|
+
class GeneratorInfo:
|
|
114
|
+
"""Static data and solved outputs of one generator."""
|
|
115
|
+
|
|
116
|
+
name: str
|
|
117
|
+
bus: str
|
|
118
|
+
in_service: bool
|
|
119
|
+
p_mw: float
|
|
120
|
+
q_mvar: float
|
|
121
|
+
v_setpoint_pu: float
|
|
122
|
+
p_min_mw: float
|
|
123
|
+
p_max_mw: float
|
|
124
|
+
q_min_mvar: float
|
|
125
|
+
q_max_mvar: float
|
|
126
|
+
has_p_limits: bool
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@dataclass
|
|
130
|
+
class ContingencyResult:
|
|
131
|
+
"""Outcome of one contingency run."""
|
|
132
|
+
|
|
133
|
+
name: str
|
|
134
|
+
converged: bool
|
|
135
|
+
accepted: bool
|
|
136
|
+
min_v_pu: float
|
|
137
|
+
max_v_pu: float
|
|
138
|
+
min_v_bus: str
|
|
139
|
+
max_v_bus: str
|
|
140
|
+
max_loading_pct: float
|
|
141
|
+
max_loading_branch: str
|
|
142
|
+
violations: list = field(default_factory=list)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _lib_name():
|
|
146
|
+
"""Return the platform-specific shared-library filename."""
|
|
147
|
+
if sys.platform in ("win32", "cygwin"):
|
|
148
|
+
return "helios_api.dll"
|
|
149
|
+
if sys.platform == "darwin":
|
|
150
|
+
return "libhelios_api.dylib"
|
|
151
|
+
return "libhelios_api.so"
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
# Explicit binding table: {symbol: (restype, [argtypes])}. The bundled
|
|
155
|
+
# libs/helios_api.h is the authoritative reference for these signatures;
|
|
156
|
+
# an explicit table is used (instead of parsing the header, as the RAMSES
|
|
157
|
+
# wrapper does) because helios_api.h contains enums, typedefs, and
|
|
158
|
+
# multi-line prototypes that a line-based parser cannot handle.
|
|
159
|
+
_H = ctypes.c_void_p
|
|
160
|
+
_I = ctypes.c_int
|
|
161
|
+
_D = ctypes.c_double
|
|
162
|
+
_S = ctypes.c_char_p
|
|
163
|
+
_SIGNATURES = {
|
|
164
|
+
"helios_create": (_H, []),
|
|
165
|
+
"helios_destroy": (None, [_H]),
|
|
166
|
+
"helios_get_api_version": (None, [_c_int_p, _c_int_p, _c_int_p]),
|
|
167
|
+
"helios_get_build_info": (_S, []),
|
|
168
|
+
"helios_get_last_error": (_S, [_H]),
|
|
169
|
+
"helios_clear_last_error": (None, [_H]),
|
|
170
|
+
"helios_load_file": (_I, [_H, _S]),
|
|
171
|
+
"helios_solve": (_I, [_H]),
|
|
172
|
+
"helios_get_convergence_status": (_I, [_H]),
|
|
173
|
+
"helios_get_bus_voltage": (_I, [_H, _S, _c_double_p, _c_double_p]),
|
|
174
|
+
"helios_get_branch_flow": (_I, [_H, _S, _c_double_p, _c_double_p]),
|
|
175
|
+
"helios_get_output_text": (_S, [_H]),
|
|
176
|
+
"helios_set_option": (_I, [_H, _I, _D]),
|
|
177
|
+
"helios_get_option": (_I, [_H, _I, _c_double_p]),
|
|
178
|
+
"helios_get_iteration_count": (_I, [_H]),
|
|
179
|
+
"helios_get_max_mismatch": (_I, [_H, _c_double_p, _c_double_p]),
|
|
180
|
+
"helios_get_solver_status": (_I, [_H]),
|
|
181
|
+
"helios_get_bus_count": (_I, [_H]),
|
|
182
|
+
"helios_get_branch_count": (_I, [_H]),
|
|
183
|
+
"helios_get_generator_count": (_I, [_H]),
|
|
184
|
+
"helios_get_svc_count": (_I, [_H]),
|
|
185
|
+
"helios_get_zone_count": (_I, [_H]),
|
|
186
|
+
"helios_get_cut_count": (_I, [_H]),
|
|
187
|
+
"helios_get_bus_name": (_I, [_H, _I, _S, _I]),
|
|
188
|
+
"helios_get_branch_name": (_I, [_H, _I, _S, _I]),
|
|
189
|
+
"helios_get_generator_name": (_I, [_H, _I, _S, _I]),
|
|
190
|
+
"helios_get_svc_name": (_I, [_H, _I, _S, _I]),
|
|
191
|
+
"helios_get_zone_name": (_I, [_H, _I, _S, _I]),
|
|
192
|
+
"helios_get_cut_name": (_I, [_H, _I, _S, _I]),
|
|
193
|
+
"helios_find_bus": (_I, [_H, _S]),
|
|
194
|
+
"helios_find_branch": (_I, [_H, _S]),
|
|
195
|
+
"helios_find_generator": (_I, [_H, _S]),
|
|
196
|
+
"helios_find_svc": (_I, [_H, _S]),
|
|
197
|
+
"helios_find_zone": (_I, [_H, _S]),
|
|
198
|
+
"helios_find_cut": (_I, [_H, _S]),
|
|
199
|
+
"helios_get_bus_info": (_I, [_H, _I, _c_double_p, _c_double_p, _c_int_p, _c_double_p]),
|
|
200
|
+
"helios_get_bus_load": (_I, [_H, _I, _c_double_p, _c_double_p]),
|
|
201
|
+
"helios_get_bus_shunt": (_I, [_H, _I, _c_double_p, _c_double_p]),
|
|
202
|
+
"helios_get_bus_zone": (_I, [_H, _I]),
|
|
203
|
+
"helios_get_branch_info": (_I, [_H, _I, _c_int_p, _c_int_p, _c_int_p, _c_int_p, _c_double_p]),
|
|
204
|
+
"helios_get_branch_flow_by_index": (_I, [_H, _I, _c_double_p, _c_double_p, _c_double_p, _c_double_p]),
|
|
205
|
+
"helios_get_generator_info": (_I, [_H, _I, _c_int_p, _c_int_p, _c_double_p, _c_double_p, _c_double_p]),
|
|
206
|
+
"helios_get_generator_limits": (_I, [_H, _I, _c_double_p, _c_double_p, _c_double_p, _c_double_p, _c_int_p]),
|
|
207
|
+
"helios_get_svc_info": (_I, [_H, _I, _c_int_p, _c_int_p, _c_double_p, _c_double_p, _c_int_p]),
|
|
208
|
+
"helios_get_cut_flow": (_I, [_H, _I, _c_double_p, _c_double_p]),
|
|
209
|
+
"helios_copy_bus_voltages": (_I, [_H, _c_double_p, _c_double_p, _I]),
|
|
210
|
+
"helios_copy_bus_loads": (_I, [_H, _c_double_p, _c_double_p, _I]),
|
|
211
|
+
"helios_copy_branch_flows": (_I, [_H, _c_double_p, _c_double_p, _c_double_p, _c_double_p, _I]),
|
|
212
|
+
"helios_copy_branch_status": (_I, [_H, _c_int_p, _I]),
|
|
213
|
+
"helios_copy_generator_outputs": (_I, [_H, _c_double_p, _c_double_p, _c_int_p, _I]),
|
|
214
|
+
"helios_copy_svc_outputs": (_I, [_H, _c_double_p, _c_int_p, _I]),
|
|
215
|
+
"helios_trip_branch": (_I, [_H, _S]),
|
|
216
|
+
"helios_connect_branch": (_I, [_H, _S]),
|
|
217
|
+
"helios_trip_generator": (_I, [_H, _S]),
|
|
218
|
+
"helios_connect_generator": (_I, [_H, _S]),
|
|
219
|
+
"helios_trip_svc": (_I, [_H, _S]),
|
|
220
|
+
"helios_connect_svc": (_I, [_H, _S]),
|
|
221
|
+
"helios_set_generator_voltage": (_I, [_H, _S, _D]),
|
|
222
|
+
"helios_set_generator_pq": (_I, [_H, _S, _D]),
|
|
223
|
+
"helios_change_generation": (_I, [_H, _S, _D]),
|
|
224
|
+
"helios_change_load": (_I, [_H, _S, _D, _D]),
|
|
225
|
+
"helios_change_shunt": (_I, [_H, _S, _D]),
|
|
226
|
+
"helios_change_zone_load": (_I, [_H, _S, _D, _D]),
|
|
227
|
+
"helios_change_zone_generation": (_I, [_H, _S, _D]),
|
|
228
|
+
"helios_apply_changes": (_I, [_H]),
|
|
229
|
+
"helios_reset": (_I, [_H]),
|
|
230
|
+
"helios_write_dump": (_I, [_H, _S]),
|
|
231
|
+
"helios_write_voltrat": (_I, [_H, _S]),
|
|
232
|
+
"helios_write_matlab": (_I, [_H, _S]),
|
|
233
|
+
"helios_write_diagram": (_I, [_H, _S, _S]),
|
|
234
|
+
"helios_run_contingencies_file": (_I, [_H, _S, _D, _D, _D, _D]),
|
|
235
|
+
"helios_run_contingencies_n1": (_I, [_H, _I, _I, _I, _D, _D, _D, _D]),
|
|
236
|
+
"helios_get_contingency_count": (_I, [_H]),
|
|
237
|
+
"helios_get_contingency_name": (_I, [_H, _I, _S, _I]),
|
|
238
|
+
"helios_get_contingency_result": (_I, [_H, _I, _c_int_p, _c_int_p, _c_double_p, _c_double_p, _c_double_p]),
|
|
239
|
+
"helios_get_contingency_extremes": (_I, [_H, _I, _S, _S, _S, _I]),
|
|
240
|
+
"helios_get_contingency_violation_count": (_I, [_H, _I]),
|
|
241
|
+
"helios_get_contingency_violation": (_I, [_H, _I, _I, _S, _I]),
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
_lib_cache = {}
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _load_library(lib_dir=None):
|
|
248
|
+
"""Load (or return the cached) helios shared library and bind signatures.
|
|
249
|
+
|
|
250
|
+
Without *lib_dir* the platform-specific bundled copy is used
|
|
251
|
+
(``libs/win``, ``libs/lin`` or ``libs/mac``); a custom *lib_dir* must
|
|
252
|
+
contain the library file directly (no platform subfolder).
|
|
253
|
+
"""
|
|
254
|
+
directory = os.path.realpath(lib_dir) if lib_dir else __platlibdir__
|
|
255
|
+
path = os.path.join(directory, _lib_name())
|
|
256
|
+
if path in _lib_cache:
|
|
257
|
+
return _lib_cache[path]
|
|
258
|
+
try:
|
|
259
|
+
lib = ctypes.CDLL(path)
|
|
260
|
+
except OSError as exc:
|
|
261
|
+
raise ImportError(
|
|
262
|
+
"Could not load the helios shared library from %r: %s" % (path, exc)
|
|
263
|
+
) from exc
|
|
264
|
+
for name, (restype, argtypes) in _SIGNATURES.items():
|
|
265
|
+
try:
|
|
266
|
+
fn = getattr(lib, name)
|
|
267
|
+
except AttributeError as exc:
|
|
268
|
+
raise ImportError(
|
|
269
|
+
"Symbol %r missing from %r — library too old for this wrapper?" % (name, path)
|
|
270
|
+
) from exc
|
|
271
|
+
fn.restype = restype
|
|
272
|
+
fn.argtypes = argtypes
|
|
273
|
+
|
|
274
|
+
major = ctypes.c_int()
|
|
275
|
+
minor = ctypes.c_int()
|
|
276
|
+
patch = ctypes.c_int()
|
|
277
|
+
lib.helios_get_api_version(ctypes.byref(major), ctypes.byref(minor), ctypes.byref(patch))
|
|
278
|
+
(min_major, min_minor), (max_major, _) = _SUPPORTED_API
|
|
279
|
+
if not (min_major, min_minor) <= (major.value, minor.value) or major.value > max_major:
|
|
280
|
+
raise ImportError(
|
|
281
|
+
"helios library version %d.%d.%d is outside the supported range (>= %d.%d, major <= %d)"
|
|
282
|
+
% (major.value, minor.value, patch.value, min_major, min_minor, max_major)
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
_lib_cache[path] = lib
|
|
286
|
+
return lib
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
class HeliosSession:
|
|
290
|
+
"""A Helios power-flow session: load → (modify) → solve → query.
|
|
291
|
+
|
|
292
|
+
:param str lib_dir: optional directory containing the helios shared
|
|
293
|
+
library, overriding the copy bundled with stepss (useful for
|
|
294
|
+
testing pre-release engine builds).
|
|
295
|
+
|
|
296
|
+
The session is a context manager; on exit the native handle is
|
|
297
|
+
destroyed. Explicit :meth:`close` is idempotent.
|
|
298
|
+
"""
|
|
299
|
+
|
|
300
|
+
def __init__(self, lib_dir=None):
|
|
301
|
+
self._lib = _load_library(lib_dir)
|
|
302
|
+
self._handle = self._lib.helios_create()
|
|
303
|
+
if not self._handle:
|
|
304
|
+
raise HeliosError("helios_create failed (out of memory?)")
|
|
305
|
+
|
|
306
|
+
# -- lifecycle ---------------------------------------------------------
|
|
307
|
+
|
|
308
|
+
def close(self):
|
|
309
|
+
"""Destroy the native handle. Safe to call more than once."""
|
|
310
|
+
if getattr(self, "_handle", None):
|
|
311
|
+
self._lib.helios_destroy(self._handle)
|
|
312
|
+
self._handle = None
|
|
313
|
+
|
|
314
|
+
def __enter__(self):
|
|
315
|
+
return self
|
|
316
|
+
|
|
317
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
318
|
+
self.close()
|
|
319
|
+
return False
|
|
320
|
+
|
|
321
|
+
def __del__(self):
|
|
322
|
+
try:
|
|
323
|
+
self.close()
|
|
324
|
+
except Exception:
|
|
325
|
+
pass
|
|
326
|
+
|
|
327
|
+
def _check(self, ret, what):
|
|
328
|
+
"""Raise :class:`HeliosError` when *ret* is a negative status code."""
|
|
329
|
+
if ret < 0:
|
|
330
|
+
raise HeliosError("%s failed (%d): %s" % (what, ret, self.last_error))
|
|
331
|
+
return ret
|
|
332
|
+
|
|
333
|
+
@property
|
|
334
|
+
def _h(self):
|
|
335
|
+
if not self._handle:
|
|
336
|
+
raise HeliosError("session is closed")
|
|
337
|
+
return self._handle
|
|
338
|
+
|
|
339
|
+
# -- library information ------------------------------------------------
|
|
340
|
+
|
|
341
|
+
def api_version(self):
|
|
342
|
+
"""Return the C API version as a ``(major, minor, patch)`` tuple."""
|
|
343
|
+
major = ctypes.c_int()
|
|
344
|
+
minor = ctypes.c_int()
|
|
345
|
+
patch = ctypes.c_int()
|
|
346
|
+
self._lib.helios_get_api_version(
|
|
347
|
+
ctypes.byref(major), ctypes.byref(minor), ctypes.byref(patch))
|
|
348
|
+
return (major.value, minor.value, patch.value)
|
|
349
|
+
|
|
350
|
+
def build_info(self):
|
|
351
|
+
"""Return the library build description string."""
|
|
352
|
+
return self._lib.helios_get_build_info().decode()
|
|
353
|
+
|
|
354
|
+
@property
|
|
355
|
+
def last_error(self):
|
|
356
|
+
"""Diagnostic text of the most recent failing call (or warning)."""
|
|
357
|
+
return self._lib.helios_get_last_error(self._h).decode()
|
|
358
|
+
|
|
359
|
+
def clear_last_error(self):
|
|
360
|
+
"""Clear the per-session diagnostic text."""
|
|
361
|
+
self._lib.helios_clear_last_error(self._h)
|
|
362
|
+
|
|
363
|
+
# -- load and solve ------------------------------------------------------
|
|
364
|
+
|
|
365
|
+
def load_file(self, path):
|
|
366
|
+
"""Load a STEPSS data file, replacing any previously loaded network."""
|
|
367
|
+
self._check(self._lib.helios_load_file(self._h, str(path).encode("utf-8")),
|
|
368
|
+
"load_file(%s)" % path)
|
|
369
|
+
|
|
370
|
+
def solve(self):
|
|
371
|
+
"""Run the power flow. Returns ``True`` if converged.
|
|
372
|
+
|
|
373
|
+
Raises :class:`HeliosError` on hard errors (no network loaded,
|
|
374
|
+
solver exception); mere non-convergence returns ``False``.
|
|
375
|
+
"""
|
|
376
|
+
ret = self._check(self._lib.helios_solve(self._h), "solve")
|
|
377
|
+
return ret == 0
|
|
378
|
+
|
|
379
|
+
def reset(self):
|
|
380
|
+
"""Restore the state saved at load and drop pending modifications."""
|
|
381
|
+
self._check(self._lib.helios_reset(self._h), "reset")
|
|
382
|
+
|
|
383
|
+
@property
|
|
384
|
+
def converged(self):
|
|
385
|
+
"""``True`` when the last solve converged."""
|
|
386
|
+
return self._check(self._lib.helios_get_convergence_status(self._h),
|
|
387
|
+
"get_convergence_status") == 0
|
|
388
|
+
|
|
389
|
+
@property
|
|
390
|
+
def solver_status(self):
|
|
391
|
+
"""Detailed :class:`SolverStatus` of the last solve."""
|
|
392
|
+
return SolverStatus(self._check(self._lib.helios_get_solver_status(self._h),
|
|
393
|
+
"get_solver_status"))
|
|
394
|
+
|
|
395
|
+
@property
|
|
396
|
+
def iterations(self):
|
|
397
|
+
"""Iterations used by the last solve."""
|
|
398
|
+
return self._check(self._lib.helios_get_iteration_count(self._h),
|
|
399
|
+
"get_iteration_count")
|
|
400
|
+
|
|
401
|
+
@property
|
|
402
|
+
def max_mismatch(self):
|
|
403
|
+
"""Largest remaining ``(mw, mvar)`` mismatches of the last solve."""
|
|
404
|
+
mw = ctypes.c_double()
|
|
405
|
+
mvar = ctypes.c_double()
|
|
406
|
+
self._check(self._lib.helios_get_max_mismatch(
|
|
407
|
+
self._h, ctypes.byref(mw), ctypes.byref(mvar)), "get_max_mismatch")
|
|
408
|
+
return (mw.value, mvar.value)
|
|
409
|
+
|
|
410
|
+
# -- solver options -------------------------------------------------------
|
|
411
|
+
|
|
412
|
+
def set_option(self, option, value):
|
|
413
|
+
"""Set a solver :class:`Option`. Call after :meth:`load_file`
|
|
414
|
+
(loading overwrites options with the file's ``$PARAM`` values)."""
|
|
415
|
+
self._check(self._lib.helios_set_option(self._h, int(option), float(value)),
|
|
416
|
+
"set_option(%s)" % option)
|
|
417
|
+
|
|
418
|
+
def get_option(self, option):
|
|
419
|
+
"""Return the current value of a solver :class:`Option`."""
|
|
420
|
+
value = ctypes.c_double()
|
|
421
|
+
self._check(self._lib.helios_get_option(self._h, int(option), ctypes.byref(value)),
|
|
422
|
+
"get_option(%s)" % option)
|
|
423
|
+
return value.value
|
|
424
|
+
|
|
425
|
+
# -- counts, names, lookup -------------------------------------------------
|
|
426
|
+
|
|
427
|
+
@property
|
|
428
|
+
def bus_count(self):
|
|
429
|
+
return self._check(self._lib.helios_get_bus_count(self._h), "get_bus_count")
|
|
430
|
+
|
|
431
|
+
@property
|
|
432
|
+
def branch_count(self):
|
|
433
|
+
return self._check(self._lib.helios_get_branch_count(self._h), "get_branch_count")
|
|
434
|
+
|
|
435
|
+
@property
|
|
436
|
+
def generator_count(self):
|
|
437
|
+
return self._check(self._lib.helios_get_generator_count(self._h),
|
|
438
|
+
"get_generator_count")
|
|
439
|
+
|
|
440
|
+
@property
|
|
441
|
+
def svc_count(self):
|
|
442
|
+
return self._check(self._lib.helios_get_svc_count(self._h), "get_svc_count")
|
|
443
|
+
|
|
444
|
+
@property
|
|
445
|
+
def zone_count(self):
|
|
446
|
+
return self._check(self._lib.helios_get_zone_count(self._h), "get_zone_count")
|
|
447
|
+
|
|
448
|
+
@property
|
|
449
|
+
def cut_count(self):
|
|
450
|
+
return self._check(self._lib.helios_get_cut_count(self._h), "get_cut_count")
|
|
451
|
+
|
|
452
|
+
def _names(self, count, getter, what):
|
|
453
|
+
buf = ctypes.create_string_buffer(NAME_MAX)
|
|
454
|
+
names = []
|
|
455
|
+
for i in range(count):
|
|
456
|
+
self._check(getter(self._h, i, buf, NAME_MAX), what)
|
|
457
|
+
names.append(buf.value.decode())
|
|
458
|
+
return names
|
|
459
|
+
|
|
460
|
+
def bus_names(self):
|
|
461
|
+
"""Names of all buses, in element-index order."""
|
|
462
|
+
return self._names(self.bus_count, self._lib.helios_get_bus_name, "get_bus_name")
|
|
463
|
+
|
|
464
|
+
def branch_names(self):
|
|
465
|
+
"""Names of all branches, in element-index order."""
|
|
466
|
+
return self._names(self.branch_count, self._lib.helios_get_branch_name,
|
|
467
|
+
"get_branch_name")
|
|
468
|
+
|
|
469
|
+
def generator_names(self):
|
|
470
|
+
"""Names of all generators, in element-index order."""
|
|
471
|
+
return self._names(self.generator_count, self._lib.helios_get_generator_name,
|
|
472
|
+
"get_generator_name")
|
|
473
|
+
|
|
474
|
+
def svc_names(self):
|
|
475
|
+
"""Names of all SVCs, in element-index order."""
|
|
476
|
+
return self._names(self.svc_count, self._lib.helios_get_svc_name, "get_svc_name")
|
|
477
|
+
|
|
478
|
+
def zone_names(self):
|
|
479
|
+
"""Names of all zones, in element-index order."""
|
|
480
|
+
return self._names(self.zone_count, self._lib.helios_get_zone_name, "get_zone_name")
|
|
481
|
+
|
|
482
|
+
def cut_names(self):
|
|
483
|
+
"""Names of all cuts, in element-index order."""
|
|
484
|
+
return self._names(self.cut_count, self._lib.helios_get_cut_name, "get_cut_name")
|
|
485
|
+
|
|
486
|
+
def find_bus(self, name):
|
|
487
|
+
"""Element index of the named bus (raises if unknown)."""
|
|
488
|
+
return self._check(self._lib.helios_find_bus(self._h, name.encode("utf-8")),
|
|
489
|
+
"find_bus(%s)" % name)
|
|
490
|
+
|
|
491
|
+
def find_branch(self, name):
|
|
492
|
+
"""Element index of the named branch (raises if unknown)."""
|
|
493
|
+
return self._check(self._lib.helios_find_branch(self._h, name.encode("utf-8")),
|
|
494
|
+
"find_branch(%s)" % name)
|
|
495
|
+
|
|
496
|
+
def find_generator(self, name):
|
|
497
|
+
"""Element index of the named generator (raises if unknown)."""
|
|
498
|
+
return self._check(self._lib.helios_find_generator(self._h, name.encode("utf-8")),
|
|
499
|
+
"find_generator(%s)" % name)
|
|
500
|
+
|
|
501
|
+
# -- per-element queries ------------------------------------------------------
|
|
502
|
+
|
|
503
|
+
def get_bus_voltage(self, bus):
|
|
504
|
+
"""Solved voltage of the named bus as ``(v_pu, angle_rad)``."""
|
|
505
|
+
v = ctypes.c_double()
|
|
506
|
+
angle = ctypes.c_double()
|
|
507
|
+
self._check(self._lib.helios_get_bus_voltage(
|
|
508
|
+
self._h, bus.encode("utf-8"), ctypes.byref(v), ctypes.byref(angle)),
|
|
509
|
+
"get_bus_voltage(%s)" % bus)
|
|
510
|
+
return (v.value, angle.value)
|
|
511
|
+
|
|
512
|
+
def get_branch_flow(self, branch):
|
|
513
|
+
"""From-end flow of the named branch as ``(p_mw, q_mvar)``."""
|
|
514
|
+
p = ctypes.c_double()
|
|
515
|
+
q = ctypes.c_double()
|
|
516
|
+
self._check(self._lib.helios_get_branch_flow(
|
|
517
|
+
self._h, branch.encode("utf-8"), ctypes.byref(p), ctypes.byref(q)),
|
|
518
|
+
"get_branch_flow(%s)" % branch)
|
|
519
|
+
return (p.value, q.value)
|
|
520
|
+
|
|
521
|
+
def get_bus_info(self, bus):
|
|
522
|
+
"""Full :class:`BusInfo` of the named bus."""
|
|
523
|
+
idx = self.find_bus(bus)
|
|
524
|
+
v = ctypes.c_double()
|
|
525
|
+
angle = ctypes.c_double()
|
|
526
|
+
btype = ctypes.c_int()
|
|
527
|
+
vnom = ctypes.c_double()
|
|
528
|
+
self._check(self._lib.helios_get_bus_info(
|
|
529
|
+
self._h, idx, ctypes.byref(v), ctypes.byref(angle),
|
|
530
|
+
ctypes.byref(btype), ctypes.byref(vnom)), "get_bus_info(%s)" % bus)
|
|
531
|
+
p = ctypes.c_double()
|
|
532
|
+
q = ctypes.c_double()
|
|
533
|
+
self._check(self._lib.helios_get_bus_load(
|
|
534
|
+
self._h, idx, ctypes.byref(p), ctypes.byref(q)), "get_bus_load(%s)" % bus)
|
|
535
|
+
return BusInfo(name=bus, v_pu=v.value, angle_rad=angle.value,
|
|
536
|
+
bus_type=BusType(btype.value), vnom_kv=vnom.value,
|
|
537
|
+
p_load_mw=p.value, q_load_mvar=q.value)
|
|
538
|
+
|
|
539
|
+
def get_branch_info(self, branch):
|
|
540
|
+
"""Full :class:`BranchInfo` of the named branch."""
|
|
541
|
+
idx = self.find_branch(branch)
|
|
542
|
+
frm = ctypes.c_int()
|
|
543
|
+
to = ctypes.c_int()
|
|
544
|
+
status = ctypes.c_int()
|
|
545
|
+
is_trfo = ctypes.c_int()
|
|
546
|
+
snom = ctypes.c_double()
|
|
547
|
+
self._check(self._lib.helios_get_branch_info(
|
|
548
|
+
self._h, idx, ctypes.byref(frm), ctypes.byref(to), ctypes.byref(status),
|
|
549
|
+
ctypes.byref(is_trfo), ctypes.byref(snom)), "get_branch_info(%s)" % branch)
|
|
550
|
+
pf = ctypes.c_double()
|
|
551
|
+
qf = ctypes.c_double()
|
|
552
|
+
pt = ctypes.c_double()
|
|
553
|
+
qt = ctypes.c_double()
|
|
554
|
+
self._check(self._lib.helios_get_branch_flow_by_index(
|
|
555
|
+
self._h, idx, ctypes.byref(pf), ctypes.byref(qf),
|
|
556
|
+
ctypes.byref(pt), ctypes.byref(qt)), "get_branch_flow_by_index(%s)" % branch)
|
|
557
|
+
buses = self.bus_names()
|
|
558
|
+
return BranchInfo(name=branch,
|
|
559
|
+
from_bus=buses[frm.value] if frm.value >= 0 else "",
|
|
560
|
+
to_bus=buses[to.value] if to.value >= 0 else "",
|
|
561
|
+
closed=status.value == 1,
|
|
562
|
+
is_transformer=bool(is_trfo.value),
|
|
563
|
+
snom_mva=snom.value,
|
|
564
|
+
p_from_mw=pf.value, q_from_mvar=qf.value,
|
|
565
|
+
p_to_mw=pt.value, q_to_mvar=qt.value)
|
|
566
|
+
|
|
567
|
+
def get_generator_info(self, generator):
|
|
568
|
+
"""Full :class:`GeneratorInfo` of the named generator."""
|
|
569
|
+
idx = self.find_generator(generator)
|
|
570
|
+
bus = ctypes.c_int()
|
|
571
|
+
status = ctypes.c_int()
|
|
572
|
+
p = ctypes.c_double()
|
|
573
|
+
q = ctypes.c_double()
|
|
574
|
+
v = ctypes.c_double()
|
|
575
|
+
self._check(self._lib.helios_get_generator_info(
|
|
576
|
+
self._h, idx, ctypes.byref(bus), ctypes.byref(status), ctypes.byref(p),
|
|
577
|
+
ctypes.byref(q), ctypes.byref(v)), "get_generator_info(%s)" % generator)
|
|
578
|
+
pmin = ctypes.c_double()
|
|
579
|
+
pmax = ctypes.c_double()
|
|
580
|
+
qmin = ctypes.c_double()
|
|
581
|
+
qmax = ctypes.c_double()
|
|
582
|
+
has_p = ctypes.c_int()
|
|
583
|
+
self._check(self._lib.helios_get_generator_limits(
|
|
584
|
+
self._h, idx, ctypes.byref(pmin), ctypes.byref(pmax), ctypes.byref(qmin),
|
|
585
|
+
ctypes.byref(qmax), ctypes.byref(has_p)), "get_generator_limits(%s)" % generator)
|
|
586
|
+
buses = self.bus_names()
|
|
587
|
+
return GeneratorInfo(name=generator,
|
|
588
|
+
bus=buses[bus.value] if bus.value >= 0 else "",
|
|
589
|
+
in_service=status.value == 1,
|
|
590
|
+
p_mw=p.value, q_mvar=q.value, v_setpoint_pu=v.value,
|
|
591
|
+
p_min_mw=pmin.value, p_max_mw=pmax.value,
|
|
592
|
+
q_min_mvar=qmin.value, q_max_mvar=qmax.value,
|
|
593
|
+
has_p_limits=bool(has_p.value))
|
|
594
|
+
|
|
595
|
+
def get_cut_flow(self, cut):
|
|
596
|
+
"""Signed total ``(p_mw, q_mvar)`` flow through the named cut."""
|
|
597
|
+
idx = self._check(self._lib.helios_find_cut(self._h, cut.encode("utf-8")),
|
|
598
|
+
"find_cut(%s)" % cut)
|
|
599
|
+
p = ctypes.c_double()
|
|
600
|
+
q = ctypes.c_double()
|
|
601
|
+
self._check(self._lib.helios_get_cut_flow(
|
|
602
|
+
self._h, idx, ctypes.byref(p), ctypes.byref(q)), "get_cut_flow(%s)" % cut)
|
|
603
|
+
return (p.value, q.value)
|
|
604
|
+
|
|
605
|
+
# -- bulk NumPy getters ------------------------------------------------------
|
|
606
|
+
|
|
607
|
+
def _copy(self, count, fn, arrays, what):
|
|
608
|
+
"""Call a bulk copy function, filling freshly allocated NumPy arrays."""
|
|
609
|
+
out = []
|
|
610
|
+
args = []
|
|
611
|
+
for kind in arrays:
|
|
612
|
+
arr = numpy.empty(count, dtype=numpy.float64 if kind == "d" else numpy.int32)
|
|
613
|
+
out.append(arr)
|
|
614
|
+
ptr_type = _c_double_p if kind == "d" else _c_int_p
|
|
615
|
+
args.append(arr.ctypes.data_as(ptr_type))
|
|
616
|
+
self._check(fn(self._h, *args, count), what)
|
|
617
|
+
return tuple(out)
|
|
618
|
+
|
|
619
|
+
def get_bus_voltages(self):
|
|
620
|
+
"""All bus voltages as ``(v_pu, angle_rad)`` NumPy arrays."""
|
|
621
|
+
return self._copy(self.bus_count, self._lib.helios_copy_bus_voltages,
|
|
622
|
+
"dd", "copy_bus_voltages")
|
|
623
|
+
|
|
624
|
+
def get_bus_loads(self):
|
|
625
|
+
"""All bus loads as ``(p_mw, q_mvar)`` NumPy arrays."""
|
|
626
|
+
return self._copy(self.bus_count, self._lib.helios_copy_bus_loads,
|
|
627
|
+
"dd", "copy_bus_loads")
|
|
628
|
+
|
|
629
|
+
def get_branch_flows(self):
|
|
630
|
+
"""All branch flows as ``(p_from_mw, q_from_mvar, p_to_mw, q_to_mvar)`` arrays."""
|
|
631
|
+
return self._copy(self.branch_count, self._lib.helios_copy_branch_flows,
|
|
632
|
+
"dddd", "copy_branch_flows")
|
|
633
|
+
|
|
634
|
+
def get_branch_status(self):
|
|
635
|
+
"""All branch statuses (1=closed, 0=open) as an int array."""
|
|
636
|
+
return self._copy(self.branch_count, self._lib.helios_copy_branch_status,
|
|
637
|
+
"i", "copy_branch_status")[0]
|
|
638
|
+
|
|
639
|
+
def get_generator_outputs(self):
|
|
640
|
+
"""All generator outputs as ``(p_mw, q_mvar, status)`` arrays."""
|
|
641
|
+
return self._copy(self.generator_count, self._lib.helios_copy_generator_outputs,
|
|
642
|
+
"ddi", "copy_generator_outputs")
|
|
643
|
+
|
|
644
|
+
def get_svc_outputs(self):
|
|
645
|
+
"""All SVC outputs as ``(q_mvar, status)`` arrays."""
|
|
646
|
+
return self._copy(self.svc_count, self._lib.helios_copy_svc_outputs,
|
|
647
|
+
"di", "copy_svc_outputs")
|
|
648
|
+
|
|
649
|
+
# -- network modification -------------------------------------------------
|
|
650
|
+
|
|
651
|
+
def trip_branch(self, name):
|
|
652
|
+
"""Open the named branch (settle with :meth:`apply_changes`)."""
|
|
653
|
+
self._check(self._lib.helios_trip_branch(self._h, name.encode("utf-8")),
|
|
654
|
+
"trip_branch(%s)" % name)
|
|
655
|
+
|
|
656
|
+
def connect_branch(self, name):
|
|
657
|
+
"""Close the named branch."""
|
|
658
|
+
self._check(self._lib.helios_connect_branch(self._h, name.encode("utf-8")),
|
|
659
|
+
"connect_branch(%s)" % name)
|
|
660
|
+
|
|
661
|
+
def trip_generator(self, name):
|
|
662
|
+
"""Take the named generator out of service (slack refused)."""
|
|
663
|
+
self._check(self._lib.helios_trip_generator(self._h, name.encode("utf-8")),
|
|
664
|
+
"trip_generator(%s)" % name)
|
|
665
|
+
|
|
666
|
+
def connect_generator(self, name):
|
|
667
|
+
"""Return the named generator to service in PV mode."""
|
|
668
|
+
self._check(self._lib.helios_connect_generator(self._h, name.encode("utf-8")),
|
|
669
|
+
"connect_generator(%s)" % name)
|
|
670
|
+
|
|
671
|
+
def trip_svc(self, name):
|
|
672
|
+
"""Take the named SVC out of service."""
|
|
673
|
+
self._check(self._lib.helios_trip_svc(self._h, name.encode("utf-8")),
|
|
674
|
+
"trip_svc(%s)" % name)
|
|
675
|
+
|
|
676
|
+
def connect_svc(self, name):
|
|
677
|
+
"""Return the named SVC to service in voltage-control mode."""
|
|
678
|
+
self._check(self._lib.helios_connect_svc(self._h, name.encode("utf-8")),
|
|
679
|
+
"connect_svc(%s)" % name)
|
|
680
|
+
|
|
681
|
+
def set_generator_voltage(self, name, v_pu):
|
|
682
|
+
"""Set the voltage setpoint of the named generator."""
|
|
683
|
+
self._check(self._lib.helios_set_generator_voltage(
|
|
684
|
+
self._h, name.encode("utf-8"), float(v_pu)),
|
|
685
|
+
"set_generator_voltage(%s)" % name)
|
|
686
|
+
|
|
687
|
+
def set_generator_pq(self, name, q_mvar):
|
|
688
|
+
"""Switch the named generator to PQ mode with the given Q output."""
|
|
689
|
+
self._check(self._lib.helios_set_generator_pq(
|
|
690
|
+
self._h, name.encode("utf-8"), float(q_mvar)),
|
|
691
|
+
"set_generator_pq(%s)" % name)
|
|
692
|
+
|
|
693
|
+
def change_generation(self, generator, delta_p_mw):
|
|
694
|
+
"""Change the named generator's active output by *delta_p_mw* (a delta)."""
|
|
695
|
+
self._check(self._lib.helios_change_generation(
|
|
696
|
+
self._h, generator.encode("utf-8"), float(delta_p_mw)),
|
|
697
|
+
"change_generation(%s)" % generator)
|
|
698
|
+
|
|
699
|
+
def change_load(self, bus, delta_p_mw, delta_q_mvar=0.0):
|
|
700
|
+
"""Change the named bus load by the given deltas."""
|
|
701
|
+
self._check(self._lib.helios_change_load(
|
|
702
|
+
self._h, bus.encode("utf-8"), float(delta_p_mw), float(delta_q_mvar)),
|
|
703
|
+
"change_load(%s)" % bus)
|
|
704
|
+
|
|
705
|
+
def set_load(self, bus, p_mw, q_mvar):
|
|
706
|
+
"""Set the named bus load to absolute values (reads the current load
|
|
707
|
+
and applies the difference)."""
|
|
708
|
+
idx = self.find_bus(bus)
|
|
709
|
+
p0 = ctypes.c_double()
|
|
710
|
+
q0 = ctypes.c_double()
|
|
711
|
+
self._check(self._lib.helios_get_bus_load(
|
|
712
|
+
self._h, idx, ctypes.byref(p0), ctypes.byref(q0)), "get_bus_load(%s)" % bus)
|
|
713
|
+
self.change_load(bus, float(p_mw) - p0.value, float(q_mvar) - q0.value)
|
|
714
|
+
|
|
715
|
+
def change_shunt(self, bus, delta_b_mvar):
|
|
716
|
+
"""Change the named bus shunt susceptance by *delta_b_mvar* (at 1 pu)."""
|
|
717
|
+
self._check(self._lib.helios_change_shunt(
|
|
718
|
+
self._h, bus.encode("utf-8"), float(delta_b_mvar)),
|
|
719
|
+
"change_shunt(%s)" % bus)
|
|
720
|
+
|
|
721
|
+
def change_zone_load(self, zone, delta_p_mw, delta_q_mvar=0.0):
|
|
722
|
+
"""Distribute a load change over the named zone's buses by participation."""
|
|
723
|
+
self._check(self._lib.helios_change_zone_load(
|
|
724
|
+
self._h, zone.encode("utf-8"), float(delta_p_mw), float(delta_q_mvar)),
|
|
725
|
+
"change_zone_load(%s)" % zone)
|
|
726
|
+
|
|
727
|
+
def change_zone_generation(self, zone, delta_p_mw):
|
|
728
|
+
"""Distribute a generation change over the named zone by participation."""
|
|
729
|
+
self._check(self._lib.helios_change_zone_generation(
|
|
730
|
+
self._h, zone.encode("utf-8"), float(delta_p_mw)),
|
|
731
|
+
"change_zone_generation(%s)" % zone)
|
|
732
|
+
|
|
733
|
+
def apply_changes(self):
|
|
734
|
+
"""Settle pending modifications: connectivity check + redispatch +
|
|
735
|
+
re-solve. Returns ``True`` if the re-solve converged. The redispatch
|
|
736
|
+
summary (if any) is available via :attr:`last_error`."""
|
|
737
|
+
ret = self._check(self._lib.helios_apply_changes(self._h), "apply_changes")
|
|
738
|
+
return ret == 0
|
|
739
|
+
|
|
740
|
+
# -- file writers ------------------------------------------------------------
|
|
741
|
+
|
|
742
|
+
def write_dump(self, path):
|
|
743
|
+
"""Write the current network as a re-loadable data file (DF)."""
|
|
744
|
+
self._check(self._lib.helios_write_dump(self._h, str(path).encode("utf-8")),
|
|
745
|
+
"write_dump(%s)" % path)
|
|
746
|
+
|
|
747
|
+
def write_voltrat(self, path):
|
|
748
|
+
"""Write the operating-point voltages / LTC data (VT, LFRESV records)."""
|
|
749
|
+
self._check(self._lib.helios_write_voltrat(self._h, str(path).encode("utf-8")),
|
|
750
|
+
"write_voltrat(%s)" % path)
|
|
751
|
+
|
|
752
|
+
def write_matlab(self, path):
|
|
753
|
+
"""Write the operating point and Y-bus as a MATLAB script (S)."""
|
|
754
|
+
self._check(self._lib.helios_write_matlab(self._h, str(path).encode("utf-8")),
|
|
755
|
+
"write_matlab(%s)" % path)
|
|
756
|
+
|
|
757
|
+
def write_diagram(self, template_svg, output_svg):
|
|
758
|
+
"""Render an SVG one-line diagram from a placeholder template."""
|
|
759
|
+
self._check(self._lib.helios_write_diagram(
|
|
760
|
+
self._h, str(template_svg).encode("utf-8"), str(output_svg).encode("utf-8")),
|
|
761
|
+
"write_diagram(%s)" % template_svg)
|
|
762
|
+
|
|
763
|
+
# -- contingency analysis -------------------------------------------------------
|
|
764
|
+
|
|
765
|
+
def run_contingencies(self, file=None, branches=False, generators=False,
|
|
766
|
+
svcs=False, v_min=0.9, v_max=1.1, overload=100.0,
|
|
767
|
+
dv_max=999.0):
|
|
768
|
+
"""Run contingency analysis and return a list of :class:`ContingencyResult`.
|
|
769
|
+
|
|
770
|
+
Pass either ``file`` (a Fortran-format contingency file) or one or
|
|
771
|
+
more of ``branches``/``generators``/``svcs`` for automatic N-1 over
|
|
772
|
+
those equipment classes. The base network state is unchanged
|
|
773
|
+
afterwards.
|
|
774
|
+
|
|
775
|
+
:param file: contingency file path, or ``None`` for N-1 mode
|
|
776
|
+
:param branches: N-1 mode — include in-service branches
|
|
777
|
+
:param generators: N-1 mode — include in-service generators (except slack)
|
|
778
|
+
:param svcs: N-1 mode — include in-service SVCs
|
|
779
|
+
:param v_min: acceptance band lower voltage, pu
|
|
780
|
+
:param v_max: acceptance band upper voltage, pu
|
|
781
|
+
:param overload: branch loading threshold, % of Snom
|
|
782
|
+
:param dv_max: max allowed voltage change vs base case, pu (999 = off)
|
|
783
|
+
"""
|
|
784
|
+
if file is not None:
|
|
785
|
+
count = self._check(self._lib.helios_run_contingencies_file(
|
|
786
|
+
self._h, str(file).encode("utf-8"),
|
|
787
|
+
float(v_min), float(v_max), float(overload), float(dv_max)),
|
|
788
|
+
"run_contingencies_file(%s)" % file)
|
|
789
|
+
else:
|
|
790
|
+
if not (branches or generators or svcs):
|
|
791
|
+
raise HeliosError(
|
|
792
|
+
"run_contingencies: pass file= or enable at least one of "
|
|
793
|
+
"branches/generators/svcs")
|
|
794
|
+
count = self._check(self._lib.helios_run_contingencies_n1(
|
|
795
|
+
self._h, int(branches), int(generators), int(svcs),
|
|
796
|
+
float(v_min), float(v_max), float(overload), float(dv_max)),
|
|
797
|
+
"run_contingencies_n1")
|
|
798
|
+
|
|
799
|
+
results = []
|
|
800
|
+
name = ctypes.create_string_buffer(TEXT_MAX)
|
|
801
|
+
bmin = ctypes.create_string_buffer(NAME_MAX)
|
|
802
|
+
bmax = ctypes.create_string_buffer(NAME_MAX)
|
|
803
|
+
brload = ctypes.create_string_buffer(NAME_MAX)
|
|
804
|
+
viol = ctypes.create_string_buffer(TEXT_MAX)
|
|
805
|
+
for i in range(count):
|
|
806
|
+
self._check(self._lib.helios_get_contingency_name(
|
|
807
|
+
self._h, i, name, TEXT_MAX), "get_contingency_name")
|
|
808
|
+
converged = ctypes.c_int()
|
|
809
|
+
accepted = ctypes.c_int()
|
|
810
|
+
vmin = ctypes.c_double()
|
|
811
|
+
vmax = ctypes.c_double()
|
|
812
|
+
loading = ctypes.c_double()
|
|
813
|
+
self._check(self._lib.helios_get_contingency_result(
|
|
814
|
+
self._h, i, ctypes.byref(converged), ctypes.byref(accepted),
|
|
815
|
+
ctypes.byref(vmin), ctypes.byref(vmax), ctypes.byref(loading)),
|
|
816
|
+
"get_contingency_result")
|
|
817
|
+
self._check(self._lib.helios_get_contingency_extremes(
|
|
818
|
+
self._h, i, bmin, bmax, brload, NAME_MAX), "get_contingency_extremes")
|
|
819
|
+
violations = []
|
|
820
|
+
nviol = self._check(self._lib.helios_get_contingency_violation_count(self._h, i),
|
|
821
|
+
"get_contingency_violation_count")
|
|
822
|
+
for j in range(nviol):
|
|
823
|
+
self._check(self._lib.helios_get_contingency_violation(
|
|
824
|
+
self._h, i, j, viol, TEXT_MAX), "get_contingency_violation")
|
|
825
|
+
violations.append(viol.value.decode())
|
|
826
|
+
results.append(ContingencyResult(
|
|
827
|
+
name=name.value.decode(),
|
|
828
|
+
converged=bool(converged.value),
|
|
829
|
+
accepted=bool(accepted.value),
|
|
830
|
+
min_v_pu=vmin.value, max_v_pu=vmax.value,
|
|
831
|
+
min_v_bus=bmin.value.decode().strip(),
|
|
832
|
+
max_v_bus=bmax.value.decode().strip(),
|
|
833
|
+
max_loading_pct=loading.value,
|
|
834
|
+
max_loading_branch=brload.value.decode().strip(),
|
|
835
|
+
violations=violations))
|
|
836
|
+
return results
|