stepss 3.77__tar.gz → 3.78__tar.gz
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-3.77/stepss.egg-info → stepss-3.78}/PKG-INFO +3 -2
- {stepss-3.77 → stepss-3.78}/stepss/__init__.py +4 -2
- {stepss-3.77 → stepss-3.78}/stepss/_bundled.py +1 -1
- {stepss-3.77 → stepss-3.78}/stepss/cases.py +60 -6
- {stepss-3.77 → stepss-3.78}/stepss/globals.py +0 -26
- {stepss-3.77 → stepss-3.78}/stepss/libs/lin/ramses.so +0 -0
- {stepss-3.77 → stepss-3.78}/stepss/libs/mac/ramses.so +0 -0
- {stepss-3.77 → stepss-3.78}/stepss/libs/win/ramses.dll +0 -0
- stepss-3.78/stepss/live.py +477 -0
- {stepss-3.77 → stepss-3.78/stepss.egg-info}/PKG-INFO +3 -2
- {stepss-3.77 → stepss-3.78}/stepss.egg-info/SOURCES.txt +1 -0
- {stepss-3.77 → stepss-3.78}/LICENSE.rst +0 -0
- {stepss-3.77 → stepss-3.78}/MANIFEST.in +0 -0
- {stepss-3.77 → stepss-3.78}/README.rst +0 -0
- {stepss-3.77 → stepss-3.78}/setup.cfg +0 -0
- {stepss-3.77 → stepss-3.78}/setup.py +0 -0
- {stepss-3.77 → stepss-3.78}/stepss/extractor.py +0 -0
- {stepss-3.77 → stepss-3.78}/stepss/helios.py +0 -0
- {stepss-3.77 → stepss-3.78}/stepss/libs/helios_api.h +0 -0
- {stepss-3.77 → stepss-3.78}/stepss/libs/lin/libhelios_api.so +0 -0
- {stepss-3.77 → stepss-3.78}/stepss/libs/mac/libhelios_api.dylib +0 -0
- {stepss-3.77 → stepss-3.78}/stepss/libs/ramses.h +0 -0
- {stepss-3.77 → stepss-3.78}/stepss/libs/win/helios_api.dll +0 -0
- {stepss-3.77 → stepss-3.78}/stepss/scripts/__init__.py +0 -0
- {stepss-3.77 → stepss-3.78}/stepss/scripts/exec.py +0 -0
- {stepss-3.77 → stepss-3.78}/stepss/simulator.py +0 -0
- {stepss-3.77 → stepss-3.78}/stepss.egg-info/dependency_links.txt +0 -0
- {stepss-3.77 → stepss-3.78}/stepss.egg-info/entry_points.txt +0 -0
- {stepss-3.77 → stepss-3.78}/stepss.egg-info/requires.txt +0 -0
- {stepss-3.77 → stepss-3.78}/stepss.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: stepss
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.78
|
|
4
4
|
Summary: Python interface to the STEPSS power-system simulation platform.
|
|
5
5
|
Home-page: https://stepss.sps-lab.org
|
|
6
6
|
Author: Petros Aristidou
|
|
@@ -84,6 +84,7 @@ Key Features
|
|
|
84
84
|
- **Complete simulation workflow** - define cases, run simulations, pause/continue, and extract results, all from Python
|
|
85
85
|
- **Runtime interaction** - query bus voltages, branch flows, and component observables while paused; inject disturbances on-the-fly
|
|
86
86
|
- **Trajectory post-processing** - extract and plot time-series results from Fortran binary trajectory files
|
|
87
|
+
- **Live monitoring** - ``stepss.monitor`` steps a running simulation and plots the quantities you choose as they are computed
|
|
87
88
|
- **Parameter sweeps** - script multiple simulations with varying parameters or disturbances
|
|
88
89
|
- **Small-signal stability analysis** - compute eigenvalues, damping ratios, participation factors and mode shapes in the engine, or export the system Jacobian and drive your own solver. See ``examples/eigenanalysis/`` for an annotated walkthrough
|
|
89
90
|
- **AC power flow** - the ``stepss.helios`` module runs Helios power flows: solve, modify with redispatch, N-1 contingency screening, and file exports
|
|
@@ -103,7 +104,7 @@ Minimal installation (no plotting or notebook support)::
|
|
|
103
104
|
|
|
104
105
|
pip install stepss
|
|
105
106
|
|
|
106
|
-
**
|
|
107
|
+
**Live plots:** ``stepss.monitor`` draws chosen quantities while a simulation runs. It uses matplotlib, which pip installs above, so nothing further is needed.
|
|
107
108
|
|
|
108
109
|
Linux System Prerequisites
|
|
109
110
|
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
@@ -12,6 +12,7 @@ Public API exported by this package:
|
|
|
12
12
|
- :class:`~stepss.extractor.extractor` - parse Fortran binary trajectory files post-simulation.
|
|
13
13
|
- :class:`~stepss.extractor.cur` - lightweight NamedTuple holding a (time, value, msg) timeseries.
|
|
14
14
|
- :func:`~stepss.extractor.curplot` - plot one or more :class:`cur` objects on a single axes.
|
|
15
|
+
- :class:`~stepss.live.monitor` - plot chosen quantities while a simulation runs.
|
|
15
16
|
- :class:`~stepss.helios.HeliosSession` - run AC power flows with the Helios engine.
|
|
16
17
|
- :class:`~stepss.globals.HeliosError` - exception raised by Helios calls.
|
|
17
18
|
|
|
@@ -23,7 +24,7 @@ Module-level flags set at import time:
|
|
|
23
24
|
"""
|
|
24
25
|
|
|
25
26
|
__package_name__ = "stepss"
|
|
26
|
-
__version__ = '3.
|
|
27
|
+
__version__ = '3.78'
|
|
27
28
|
__author__ = "Petros Aristidou"
|
|
28
29
|
__copyright__ = "Petros Aristidou"
|
|
29
30
|
__license__ = "Apache-2.0"
|
|
@@ -37,12 +38,13 @@ from .cases import cfg
|
|
|
37
38
|
from .globals import HeliosError
|
|
38
39
|
from .simulator import sim
|
|
39
40
|
from .extractor import extractor, curplot, cur
|
|
41
|
+
from .live import monitor
|
|
40
42
|
from . import helios
|
|
41
43
|
from .helios import HeliosSession
|
|
42
44
|
from ._bundled import RAMSES_VERSION as __ramses_version__
|
|
43
45
|
from ._bundled import HELIOS_VERSION as __helios_version__
|
|
44
46
|
|
|
45
|
-
__all__ = ["cfg", "sim", "extractor", "cur", "curplot", "helios",
|
|
47
|
+
__all__ = ["cfg", "sim", "extractor", "cur", "curplot", "monitor", "helios",
|
|
46
48
|
"HeliosSession", "HeliosError"]
|
|
47
49
|
|
|
48
50
|
# No gnuplot probe. This package has always plotted with matplotlib, through
|
|
@@ -15,6 +15,48 @@ from .globals import RAMSESError, CustomWarning
|
|
|
15
15
|
warnings.showwarning = CustomWarning
|
|
16
16
|
|
|
17
17
|
|
|
18
|
+
def _dstProblem(dstfile):
|
|
19
|
+
"""Return None if *dstfile* can drive a run, or a sentence saying what is wrong.
|
|
20
|
+
|
|
21
|
+
Mirrors what the engine itself accepts. A blank line, or one opening with
|
|
22
|
+
``#`` or ``!``, is a comment. Every other line is a record whose first field
|
|
23
|
+
is a time and whose remainder is the description, and the run ends at the
|
|
24
|
+
first record whose description carries ``STOP``.
|
|
25
|
+
|
|
26
|
+
:param str dstfile: path to the disturbance file to check.
|
|
27
|
+
:returns: None when the file is usable, otherwise the reason it is not.
|
|
28
|
+
:rtype: str or None
|
|
29
|
+
"""
|
|
30
|
+
records = 0
|
|
31
|
+
untimed = False
|
|
32
|
+
try:
|
|
33
|
+
with open(dstfile, 'r', encoding='utf-8', errors='replace') as records_file:
|
|
34
|
+
for line in records_file:
|
|
35
|
+
text = line.strip()
|
|
36
|
+
if not text or text[0] in '#!':
|
|
37
|
+
continue
|
|
38
|
+
_, space, description = text.partition(' ')
|
|
39
|
+
if not space:
|
|
40
|
+
# The engine refuses a record with fewer than two fields.
|
|
41
|
+
# Noting a bare STOP separately turns "no STOP record" into
|
|
42
|
+
# a sentence the reader can act on, with STOP on screen.
|
|
43
|
+
untimed = untimed or 'STOP' in text
|
|
44
|
+
continue
|
|
45
|
+
records += 1
|
|
46
|
+
if 'STOP' in description:
|
|
47
|
+
return None
|
|
48
|
+
except IOError as e:
|
|
49
|
+
return 'RAMSES: The disturbance file %s could not be read: %s' % (dstfile, e)
|
|
50
|
+
|
|
51
|
+
if untimed:
|
|
52
|
+
return ('RAMSES: The disturbance file %s has a STOP without a time. Every '
|
|
53
|
+
'record starts with its time, as in "240.0 STOP".' % dstfile)
|
|
54
|
+
if records == 0:
|
|
55
|
+
return 'RAMSES: The disturbance file %s has no disturbance records.' % dstfile
|
|
56
|
+
return ('RAMSES: The disturbance file %s has no STOP record. A dynamic simulation '
|
|
57
|
+
'ends at its STOP time, so add a line such as "240.0 STOP".' % dstfile)
|
|
58
|
+
|
|
59
|
+
|
|
18
60
|
class cfg(object):
|
|
19
61
|
"""Simulation case configuration.
|
|
20
62
|
|
|
@@ -174,8 +216,16 @@ class cfg(object):
|
|
|
174
216
|
|
|
175
217
|
.. warning:: If *userFile* already exists it will be overwritten.
|
|
176
218
|
"""
|
|
177
|
-
if not self._dataset
|
|
178
|
-
raise RAMSESError('
|
|
219
|
+
if not self._dataset:
|
|
220
|
+
raise RAMSESError('RAMSES: No data file is set. A case needs at least one.')
|
|
221
|
+
if not self._dstset:
|
|
222
|
+
raise RAMSESError('RAMSES: No disturbance file is set. A dynamic simulation '
|
|
223
|
+
'needs one, ending in a STOP record.')
|
|
224
|
+
# Re-checked here rather than trusted from addDst: the file can be
|
|
225
|
+
# edited, or replaced, between being attached and being run.
|
|
226
|
+
problem = _dstProblem(self._dstset[0])
|
|
227
|
+
if problem:
|
|
228
|
+
raise RAMSESError(problem)
|
|
179
229
|
|
|
180
230
|
cmdFile = ""
|
|
181
231
|
|
|
@@ -534,11 +584,15 @@ class cfg(object):
|
|
|
534
584
|
|
|
535
585
|
.. warning:: A disturbance file must be provided before the simulation can start.
|
|
536
586
|
"""
|
|
537
|
-
if os.path.isfile(dstfile):
|
|
538
|
-
del self._dstset[:]
|
|
539
|
-
self._dstset.append(dstfile)
|
|
540
|
-
else:
|
|
587
|
+
if not os.path.isfile(dstfile):
|
|
541
588
|
raise IOError('RAMSES: The dstfile %s does not exist or is not valid.' % (dstfile))
|
|
589
|
+
# Checked before the case is touched, so a rejected file never replaces
|
|
590
|
+
# a working one.
|
|
591
|
+
problem = _dstProblem(dstfile)
|
|
592
|
+
if problem:
|
|
593
|
+
raise RAMSESError(problem)
|
|
594
|
+
del self._dstset[:]
|
|
595
|
+
self._dstset.append(dstfile)
|
|
542
596
|
|
|
543
597
|
def getDst(self):
|
|
544
598
|
"""Get disturbance
|
|
@@ -95,32 +95,6 @@ class HeliosError(Exception):
|
|
|
95
95
|
pass
|
|
96
96
|
|
|
97
97
|
|
|
98
|
-
def __which(program):
|
|
99
|
-
"""Locate a program on the system PATH, similar to the Unix ``which`` command.
|
|
100
|
-
|
|
101
|
-
:param str program: executable name (with or without full path)
|
|
102
|
-
:returns: full path to the executable if found and executable, otherwise ``None``
|
|
103
|
-
:rtype: str or None
|
|
104
|
-
"""
|
|
105
|
-
|
|
106
|
-
def is_exe(fpath):
|
|
107
|
-
"""Return True if *fpath* is a regular file and is executable."""
|
|
108
|
-
return os.path.isfile(fpath) and os.access(fpath, os.X_OK)
|
|
109
|
-
|
|
110
|
-
fpath, fname = os.path.split(program)
|
|
111
|
-
if fpath:
|
|
112
|
-
if is_exe(program):
|
|
113
|
-
return program
|
|
114
|
-
else:
|
|
115
|
-
for path in os.environ.get("PATH", "").split(os.pathsep):
|
|
116
|
-
path = path.strip('"')
|
|
117
|
-
exe_file = os.path.join(path, program)
|
|
118
|
-
if is_exe(exe_file):
|
|
119
|
-
return exe_file
|
|
120
|
-
|
|
121
|
-
return None
|
|
122
|
-
|
|
123
|
-
|
|
124
98
|
def silentremove(filename):
|
|
125
99
|
"""Delete a file, silently ignoring the case where it does not exist.
|
|
126
100
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Live plotting of a running RAMSES simulation.
|
|
4
|
+
|
|
5
|
+
Provides :class:`monitor`, which steps a simulation forward in slices, reads
|
|
6
|
+
the chosen quantities out of the engine at every pause, and redraws a stacked
|
|
7
|
+
matplotlib figure as the run proceeds.
|
|
8
|
+
|
|
9
|
+
The engine is polled in process, through the same accessors a script would
|
|
10
|
+
call by hand (:meth:`~stepss.simulator.sim.getBusVolt`,
|
|
11
|
+
:meth:`~stepss.simulator.sim.getObs` and the rest): no file is read and
|
|
12
|
+
nothing is piped anywhere. Run-time observables declared with
|
|
13
|
+
:meth:`~stepss.cases.cfg.addRunObs` are a separate mechanism, in which the
|
|
14
|
+
engine writes a curve file of its own; the two are independent and may be used
|
|
15
|
+
together.
|
|
16
|
+
|
|
17
|
+
One panel per observable, stacked and sharing the time axis.
|
|
18
|
+
|
|
19
|
+
:Example:
|
|
20
|
+
|
|
21
|
+
>>> import stepss
|
|
22
|
+
>>> ram = stepss.sim()
|
|
23
|
+
>>> case = stepss.cfg('cmd.txt')
|
|
24
|
+
>>> ram.execSim(case, 0.0) # initialise, pause at t = 0
|
|
25
|
+
>>> mon = stepss.monitor(ram, ['BV 4044', 'MS g6'])
|
|
26
|
+
>>> curves = mon.run(step=0.5) # run to the end of the scenario
|
|
27
|
+
>>> stepss.curplot(curves) # replot the same data afterwards
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
import time
|
|
31
|
+
|
|
32
|
+
import matplotlib
|
|
33
|
+
import matplotlib.pyplot as plt
|
|
34
|
+
import numpy as np
|
|
35
|
+
|
|
36
|
+
from .extractor import cur
|
|
37
|
+
|
|
38
|
+
# Backends that render to a file and have no window to update. Live redraw is
|
|
39
|
+
# skipped on these; sampling and the final figure are unaffected, so
|
|
40
|
+
# monitor.savefig() produces the same chart a window would have shown. The list
|
|
41
|
+
# is matplotlib's own set of non-interactive built-ins, copied rather than
|
|
42
|
+
# imported: matplotlib.rcsetup.non_interactive_bk is deprecated since 3.9 and
|
|
43
|
+
# removed in 3.11, and this package supports matplotlib 3.0 upwards. A backend
|
|
44
|
+
# missing from the list is treated as interactive, which costs a draw call that
|
|
45
|
+
# does nothing.
|
|
46
|
+
_STATIC_BACKENDS = frozenset(['agg', 'cairo', 'pdf', 'pgf', 'ps', 'svg', 'template'])
|
|
47
|
+
|
|
48
|
+
# Sim-time comparisons. RAMSES pauses at the first internal step at or after
|
|
49
|
+
# the requested time, so equality is never exact.
|
|
50
|
+
_EPS = 1e-9
|
|
51
|
+
|
|
52
|
+
# Branch quantity -> (index into getBranchPow, description, unit).
|
|
53
|
+
_BRANCH_POW = {
|
|
54
|
+
'BPO': (0, 'active power at the origin', 'MW'),
|
|
55
|
+
'BQO': (1, 'reactive power at the origin', 'Mvar'),
|
|
56
|
+
'BPE': (2, 'active power at the extremity', 'MW'),
|
|
57
|
+
'BQE': (3, 'reactive power at the extremity', 'Mvar'),
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _canDraw():
|
|
62
|
+
"""Return True if the active matplotlib backend can update a live window.
|
|
63
|
+
|
|
64
|
+
:returns: False for the file-only backends, True otherwise.
|
|
65
|
+
:rtype: bool
|
|
66
|
+
"""
|
|
67
|
+
name = matplotlib.get_backend().lower()
|
|
68
|
+
if name.startswith('module://'):
|
|
69
|
+
# Jupyter's inline backend emits a static image per draw and cannot
|
|
70
|
+
# animate one; ipympl and the nbagg family can.
|
|
71
|
+
return 'inline' not in name
|
|
72
|
+
return name not in _STATIC_BACKENDS
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class _Clock(object):
|
|
76
|
+
"""Wall-clock seconds elapsed since the first sample.
|
|
77
|
+
|
|
78
|
+
Backs the ``RT`` descriptor: plotted against simulated time it shows how
|
|
79
|
+
far the simulation runs behind, or ahead of, real time.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
def __init__(self):
|
|
83
|
+
self._start = None
|
|
84
|
+
|
|
85
|
+
def __call__(self, ram):
|
|
86
|
+
"""Return seconds elapsed, starting the clock on the first call.
|
|
87
|
+
|
|
88
|
+
:param ram: the simulator instance (unused).
|
|
89
|
+
:returns: elapsed wall-clock time in seconds.
|
|
90
|
+
:rtype: float
|
|
91
|
+
"""
|
|
92
|
+
now = time.monotonic()
|
|
93
|
+
if self._start is None:
|
|
94
|
+
self._start = now
|
|
95
|
+
return now - self._start
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _namedObs(comp_type, comp_name, obs_name):
|
|
99
|
+
"""Return a reader for one named engine observable.
|
|
100
|
+
|
|
101
|
+
:param str comp_type: component type accepted by :meth:`~stepss.simulator.sim.getObs`.
|
|
102
|
+
:param str comp_name: component name.
|
|
103
|
+
:param str obs_name: observable name.
|
|
104
|
+
:returns: a callable taking the simulator and returning a float.
|
|
105
|
+
:rtype: callable
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
def read(ram):
|
|
109
|
+
return ram.getObs(comp_type, comp_name, obs_name)[0]
|
|
110
|
+
|
|
111
|
+
return read
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _branchPow(branch_name, index):
|
|
115
|
+
"""Return a reader for one field of :meth:`~stepss.simulator.sim.getBranchPow`.
|
|
116
|
+
|
|
117
|
+
:param str branch_name: branch name.
|
|
118
|
+
:param int index: 0 to 3, selecting p_orig, q_orig, p_extr or q_extr.
|
|
119
|
+
:returns: a callable taking the simulator and returning a float.
|
|
120
|
+
:rtype: callable
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
def read(ram):
|
|
124
|
+
return ram.getBranchPow([branch_name])[0][index]
|
|
125
|
+
|
|
126
|
+
return read
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _expect(descriptor, args, count):
|
|
130
|
+
"""Raise unless *args* holds exactly *count* items.
|
|
131
|
+
|
|
132
|
+
:param str descriptor: the descriptor being parsed, for the message.
|
|
133
|
+
:param list args: the fields following the descriptor code.
|
|
134
|
+
:param int count: the number of fields the code takes.
|
|
135
|
+
:raises ValueError: if the count does not match.
|
|
136
|
+
"""
|
|
137
|
+
if len(args) != count:
|
|
138
|
+
raise ValueError(
|
|
139
|
+
"RAMSES: the observable descriptor '%s' takes %i field(s) after the code, got %i"
|
|
140
|
+
% (descriptor, count, len(args)))
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _parse(descriptor):
|
|
144
|
+
"""Turn one descriptor string into a label, a unit and a reader.
|
|
145
|
+
|
|
146
|
+
The vocabulary is tabulated in :class:`monitor`.
|
|
147
|
+
|
|
148
|
+
:param str descriptor: the descriptor to parse.
|
|
149
|
+
:returns: ``(label, unit, reader)``; *unit* is empty when the engine does
|
|
150
|
+
not define one.
|
|
151
|
+
:rtype: tuple
|
|
152
|
+
:raises ValueError: if the descriptor is empty, unknown, or carries the
|
|
153
|
+
wrong number of fields.
|
|
154
|
+
"""
|
|
155
|
+
fields = descriptor.split()
|
|
156
|
+
if not fields:
|
|
157
|
+
raise ValueError('RAMSES: the observable descriptor is empty')
|
|
158
|
+
code = fields[0].upper()
|
|
159
|
+
args = fields[1:]
|
|
160
|
+
|
|
161
|
+
if code == 'RT':
|
|
162
|
+
return 'elapsed real time', 's', _Clock()
|
|
163
|
+
|
|
164
|
+
if code == 'BV':
|
|
165
|
+
_expect(descriptor, args, 1)
|
|
166
|
+
name = args[0]
|
|
167
|
+
return '%s: voltage magnitude' % name, 'pu', lambda ram: ram.getBusVolt([name])[0]
|
|
168
|
+
|
|
169
|
+
if code == 'BA':
|
|
170
|
+
_expect(descriptor, args, 1)
|
|
171
|
+
name = args[0]
|
|
172
|
+
return '%s: voltage phase' % name, 'deg', lambda ram: ram.getBusPha([name])[0]
|
|
173
|
+
|
|
174
|
+
if code == 'MS':
|
|
175
|
+
_expect(descriptor, args, 1)
|
|
176
|
+
name = args[0]
|
|
177
|
+
return '%s: machine speed' % name, 'pu', _namedObs('SYN', name, 'Omega')
|
|
178
|
+
|
|
179
|
+
if code in _BRANCH_POW:
|
|
180
|
+
_expect(descriptor, args, 1)
|
|
181
|
+
name = args[0]
|
|
182
|
+
index, what, unit = _BRANCH_POW[code]
|
|
183
|
+
return '%s: %s' % (name, what), unit, _branchPow(name, index)
|
|
184
|
+
|
|
185
|
+
if code in ('ON', 'TO'):
|
|
186
|
+
_expect(descriptor, args, 2)
|
|
187
|
+
name, obs_name = args
|
|
188
|
+
comp_type = 'INJ' if code == 'ON' else 'TWOP'
|
|
189
|
+
return '%s: %s' % (name, obs_name), '', _namedObs(comp_type, name, obs_name)
|
|
190
|
+
|
|
191
|
+
if code == 'OBS':
|
|
192
|
+
_expect(descriptor, args, 3)
|
|
193
|
+
comp_type, name, obs_name = args
|
|
194
|
+
return '%s: %s' % (name, obs_name), '', _namedObs(comp_type.upper(), name, obs_name)
|
|
195
|
+
|
|
196
|
+
raise ValueError(
|
|
197
|
+
"RAMSES: unknown observable descriptor '%s'. Expected one of "
|
|
198
|
+
"BV, BA, MS, BPO, BQO, BPE, BQE, ON, TO, OBS or RT." % descriptor)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _isPair(item):
|
|
202
|
+
"""Return True if *item* is a ``(label, callable)`` observable spec."""
|
|
203
|
+
return (isinstance(item, (tuple, list)) and len(item) == 2
|
|
204
|
+
and isinstance(item[0], str) and callable(item[1]))
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _reader(item):
|
|
208
|
+
"""Turn one observable spec into a label, a unit and a reader.
|
|
209
|
+
|
|
210
|
+
:param item: a descriptor string, a ``(label, callable)`` pair, or a bare
|
|
211
|
+
callable taking the simulator and returning a float.
|
|
212
|
+
:returns: ``(label, unit, reader)``.
|
|
213
|
+
:rtype: tuple
|
|
214
|
+
:raises ValueError: if the spec is of none of those forms.
|
|
215
|
+
"""
|
|
216
|
+
if isinstance(item, str):
|
|
217
|
+
return _parse(item)
|
|
218
|
+
if _isPair(item):
|
|
219
|
+
return item[0], '', item[1]
|
|
220
|
+
if callable(item):
|
|
221
|
+
return getattr(item, '__name__', 'observable'), '', item
|
|
222
|
+
raise ValueError(
|
|
223
|
+
'RAMSES: an observable must be a descriptor string, a (label, callable) '
|
|
224
|
+
'pair or a callable, not %s' % type(item).__name__)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _asList(observables):
|
|
228
|
+
"""Normalise the *observables* argument to a list of specs."""
|
|
229
|
+
if isinstance(observables, str) or callable(observables) or _isPair(observables):
|
|
230
|
+
return [observables]
|
|
231
|
+
return list(observables)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class monitor(object):
|
|
235
|
+
"""Plot chosen quantities of a RAMSES simulation while it runs.
|
|
236
|
+
|
|
237
|
+
The simulation must already be initialised and paused, which
|
|
238
|
+
:meth:`~stepss.simulator.sim.execSim` does when given a *pause* time.
|
|
239
|
+
:meth:`run` then advances it in slices, sampling and redrawing at each
|
|
240
|
+
pause, and returns the collected curves.
|
|
241
|
+
|
|
242
|
+
Each observable gets its own panel; the panels are stacked and share the
|
|
243
|
+
time axis.
|
|
244
|
+
|
|
245
|
+
**Instance attributes:**
|
|
246
|
+
|
|
247
|
+
- ``figure`` (*matplotlib.figure.Figure* or *None*): the chart, or None
|
|
248
|
+
when built with ``show=False``.
|
|
249
|
+
- ``axes`` (*list*): one axes per observable, top to bottom.
|
|
250
|
+
|
|
251
|
+
:param ram: the simulator driving the run.
|
|
252
|
+
:type ram: :class:`stepss.sim`
|
|
253
|
+
:param observables: what to plot: descriptor strings such as ``'BV 4044'``
|
|
254
|
+
(tabulated below), ``(label,
|
|
255
|
+
callable)`` pairs, or bare callables taking the
|
|
256
|
+
simulator and returning a float. A single spec need
|
|
257
|
+
not be wrapped in a list.
|
|
258
|
+
:param str title: figure title, or None for none.
|
|
259
|
+
:param float refresh: minimum wall-clock seconds between redraws. Samples
|
|
260
|
+
are never skipped, only draws; 0 redraws at every
|
|
261
|
+
sample.
|
|
262
|
+
:param bool show: False builds no figure at all and only collects data.
|
|
263
|
+
:raises ValueError: if *observables* is empty or holds a spec that cannot
|
|
264
|
+
be read.
|
|
265
|
+
|
|
266
|
+
**Descriptors.** The vocabulary is the one :meth:`~stepss.cases.cfg.addRunObs` uses, plus
|
|
267
|
+
``BA`` and the generic ``OBS`` escape hatch:
|
|
268
|
+
|
|
269
|
+
================== ========================================= ======
|
|
270
|
+
Descriptor Quantity Unit
|
|
271
|
+
================== ========================================= ======
|
|
272
|
+
``BV BUS`` voltage magnitude of a bus pu
|
|
273
|
+
``BA BUS`` voltage phase of a bus deg
|
|
274
|
+
``MS SYN`` speed of a synchronous machine pu
|
|
275
|
+
``BPO BRANCH`` active power at the branch origin MW
|
|
276
|
+
``BQO BRANCH`` reactive power at the branch origin Mvar
|
|
277
|
+
``BPE BRANCH`` active power at the branch extremity MW
|
|
278
|
+
``BQE BRANCH`` reactive power at the branch extremity Mvar
|
|
279
|
+
``ON INJ OBS`` named observable of an injector engine
|
|
280
|
+
``TO TWOP OBS`` named observable of a two-port engine
|
|
281
|
+
``OBS TYPE NAME O`` named observable of any component type engine
|
|
282
|
+
``RT RT`` elapsed wall-clock time s
|
|
283
|
+
================== ========================================= ======
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
:Example:
|
|
287
|
+
|
|
288
|
+
>>> import stepss
|
|
289
|
+
>>> ram = stepss.sim()
|
|
290
|
+
>>> case = stepss.cfg('cmd.txt')
|
|
291
|
+
>>> ram.execSim(case, 0.0)
|
|
292
|
+
>>> mon = stepss.monitor(ram, ['BV 4044', 'MS g6', 'RT RT'], title='Nordic')
|
|
293
|
+
>>> mon.run(step=0.5)
|
|
294
|
+
>>> mon.savefig('run.png')
|
|
295
|
+
|
|
296
|
+
.. note:: The buffers survive a failed run: if the engine raises part way
|
|
297
|
+
through, the samples taken up to that point are still available
|
|
298
|
+
from :meth:`curves`.
|
|
299
|
+
"""
|
|
300
|
+
|
|
301
|
+
def __init__(self, ram, observables, title=None, refresh=0.2, show=True):
|
|
302
|
+
specs = _asList(observables)
|
|
303
|
+
if not specs:
|
|
304
|
+
raise ValueError('RAMSES: a monitor needs at least one observable')
|
|
305
|
+
|
|
306
|
+
self._ram = ram
|
|
307
|
+
self._refresh = float(refresh)
|
|
308
|
+
self._labels = []
|
|
309
|
+
self._units = []
|
|
310
|
+
self._readers = []
|
|
311
|
+
for spec in specs:
|
|
312
|
+
label, unit, read = _reader(spec)
|
|
313
|
+
self._labels.append('%s (%s)' % (label, unit) if unit else label)
|
|
314
|
+
self._units.append(unit)
|
|
315
|
+
self._readers.append(read)
|
|
316
|
+
|
|
317
|
+
self._time = []
|
|
318
|
+
self._values = [[] for _ in self._readers]
|
|
319
|
+
self._lines = []
|
|
320
|
+
self._lastDraw = None
|
|
321
|
+
self._wasInteractive = None
|
|
322
|
+
self._drawing = False
|
|
323
|
+
self.figure = None
|
|
324
|
+
self.axes = []
|
|
325
|
+
if show:
|
|
326
|
+
self._openFigure(title)
|
|
327
|
+
|
|
328
|
+
def __enter__(self):
|
|
329
|
+
return self
|
|
330
|
+
|
|
331
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
332
|
+
self.close()
|
|
333
|
+
return False
|
|
334
|
+
|
|
335
|
+
def _openFigure(self, title):
|
|
336
|
+
"""Build the stacked figure, one panel per observable.
|
|
337
|
+
|
|
338
|
+
:param str title: figure title, or None.
|
|
339
|
+
"""
|
|
340
|
+
count = len(self._readers)
|
|
341
|
+
self.figure, grid = plt.subplots(count, 1, sharex=True, squeeze=False,
|
|
342
|
+
figsize=(8.0, 1.0 + 2.0 * count))
|
|
343
|
+
self.axes = [row[0] for row in grid]
|
|
344
|
+
for axes, label, unit in zip(self.axes, self._labels, self._units):
|
|
345
|
+
line, = axes.plot([], [])
|
|
346
|
+
self._lines.append(line)
|
|
347
|
+
axes.set_title(label, loc='left', fontsize='small')
|
|
348
|
+
axes.set_ylabel(unit)
|
|
349
|
+
axes.grid(True)
|
|
350
|
+
self.axes[-1].set_xlabel('time (s)')
|
|
351
|
+
if title:
|
|
352
|
+
self.figure.suptitle(title)
|
|
353
|
+
self.figure.tight_layout()
|
|
354
|
+
|
|
355
|
+
self._drawing = _canDraw()
|
|
356
|
+
if self._drawing:
|
|
357
|
+
self._wasInteractive = plt.isinteractive()
|
|
358
|
+
plt.ion()
|
|
359
|
+
self.figure.show()
|
|
360
|
+
|
|
361
|
+
def sample(self):
|
|
362
|
+
"""Read every observable once, at the current simulated time.
|
|
363
|
+
|
|
364
|
+
Called by :meth:`run` at each pause. Call it directly when driving the
|
|
365
|
+
simulation yourself.
|
|
366
|
+
|
|
367
|
+
:returns: None.
|
|
368
|
+
"""
|
|
369
|
+
self._time.append(self._ram.getSimTime())
|
|
370
|
+
for values, read in zip(self._values, self._readers):
|
|
371
|
+
values.append(read(self._ram))
|
|
372
|
+
|
|
373
|
+
def refresh(self, force=False):
|
|
374
|
+
"""Push the samples into the figure and redraw it.
|
|
375
|
+
|
|
376
|
+
Does nothing when the monitor was built with ``show=False``. A draw
|
|
377
|
+
that falls inside the *refresh* interval is skipped unless *force*.
|
|
378
|
+
|
|
379
|
+
:param bool force: redraw regardless of the interval.
|
|
380
|
+
:returns: None.
|
|
381
|
+
"""
|
|
382
|
+
if self.figure is None:
|
|
383
|
+
return
|
|
384
|
+
if self._drawing and not plt.fignum_exists(self.figure.number):
|
|
385
|
+
self._drawing = False # the window was closed; keep sampling
|
|
386
|
+
now = time.monotonic()
|
|
387
|
+
if not force and self._lastDraw is not None and now - self._lastDraw < self._refresh:
|
|
388
|
+
return
|
|
389
|
+
self._lastDraw = now
|
|
390
|
+
for line, values, axes in zip(self._lines, self._values, self.axes):
|
|
391
|
+
line.set_data(self._time, values)
|
|
392
|
+
axes.relim()
|
|
393
|
+
axes.autoscale_view()
|
|
394
|
+
if self._drawing:
|
|
395
|
+
self.figure.canvas.draw_idle()
|
|
396
|
+
self.figure.canvas.flush_events()
|
|
397
|
+
|
|
398
|
+
def run(self, step=1.0, until=None):
|
|
399
|
+
"""Advance the simulation in slices, sampling and redrawing at each pause.
|
|
400
|
+
|
|
401
|
+
Returns when the engine reports the end of the scenario, when *until*
|
|
402
|
+
is reached, or when a slice fails to advance the simulated time.
|
|
403
|
+
|
|
404
|
+
:param float step: simulated seconds per slice.
|
|
405
|
+
:param until: simulated time to stop at, or None to run to the end of
|
|
406
|
+
the disturbance scenario.
|
|
407
|
+
:type until: float or None
|
|
408
|
+
:returns: one curve per observable.
|
|
409
|
+
:rtype: list of :class:`stepss.cur`
|
|
410
|
+
:raises ValueError: if *step* is not positive.
|
|
411
|
+
:raises RAMSESError: propagated from the engine. The samples taken
|
|
412
|
+
before the failure remain available from
|
|
413
|
+
:meth:`curves`.
|
|
414
|
+
"""
|
|
415
|
+
if step <= 0.0:
|
|
416
|
+
raise ValueError('RAMSES: monitor.run needs a positive step, got %r' % step)
|
|
417
|
+
|
|
418
|
+
self.sample()
|
|
419
|
+
self.refresh(force=True)
|
|
420
|
+
try:
|
|
421
|
+
while not self._ram.getEndSim():
|
|
422
|
+
start = self._ram.getSimTime()
|
|
423
|
+
if until is not None and start >= until - _EPS:
|
|
424
|
+
break
|
|
425
|
+
target = start + step
|
|
426
|
+
if until is not None:
|
|
427
|
+
target = min(target, until)
|
|
428
|
+
self._ram.contSim(target)
|
|
429
|
+
self.sample()
|
|
430
|
+
self.refresh()
|
|
431
|
+
if self._ram.getSimTime() <= start + _EPS:
|
|
432
|
+
break # the scenario is over; the engine is not advancing
|
|
433
|
+
finally:
|
|
434
|
+
self.refresh(force=True)
|
|
435
|
+
return self.curves()
|
|
436
|
+
|
|
437
|
+
def curves(self):
|
|
438
|
+
"""Return everything sampled so far.
|
|
439
|
+
|
|
440
|
+
:returns: one :class:`stepss.cur` per observable, in the order given
|
|
441
|
+
to the constructor, all sharing the same time array.
|
|
442
|
+
:rtype: list of :class:`stepss.cur`
|
|
443
|
+
"""
|
|
444
|
+
times = np.asarray(self._time, dtype=float)
|
|
445
|
+
return [cur(times, np.asarray(values, dtype=float), label)
|
|
446
|
+
for values, label in zip(self._values, self._labels)]
|
|
447
|
+
|
|
448
|
+
def savefig(self, fname, **kwargs):
|
|
449
|
+
"""Redraw and write the figure to a file.
|
|
450
|
+
|
|
451
|
+
:param fname: path or file object, passed to
|
|
452
|
+
:meth:`matplotlib.figure.Figure.savefig`.
|
|
453
|
+
:param kwargs: forwarded to :meth:`~matplotlib.figure.Figure.savefig`.
|
|
454
|
+
:returns: None.
|
|
455
|
+
:raises RuntimeError: if the monitor was built with ``show=False``.
|
|
456
|
+
"""
|
|
457
|
+
if self.figure is None:
|
|
458
|
+
raise RuntimeError('RAMSES: this monitor has no figure (show=False)')
|
|
459
|
+
self.refresh(force=True)
|
|
460
|
+
self.figure.savefig(fname, **kwargs)
|
|
461
|
+
|
|
462
|
+
def close(self):
|
|
463
|
+
"""Close the figure and restore matplotlib's interactive mode.
|
|
464
|
+
|
|
465
|
+
The collected samples are unaffected: :meth:`curves` keeps working.
|
|
466
|
+
|
|
467
|
+
:returns: None.
|
|
468
|
+
"""
|
|
469
|
+
if self.figure is not None:
|
|
470
|
+
plt.close(self.figure)
|
|
471
|
+
self.figure = None
|
|
472
|
+
self.axes = []
|
|
473
|
+
self._lines = []
|
|
474
|
+
if self._wasInteractive is False:
|
|
475
|
+
plt.ioff()
|
|
476
|
+
self._wasInteractive = None
|
|
477
|
+
self._drawing = False
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: stepss
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.78
|
|
4
4
|
Summary: Python interface to the STEPSS power-system simulation platform.
|
|
5
5
|
Home-page: https://stepss.sps-lab.org
|
|
6
6
|
Author: Petros Aristidou
|
|
@@ -84,6 +84,7 @@ Key Features
|
|
|
84
84
|
- **Complete simulation workflow** - define cases, run simulations, pause/continue, and extract results, all from Python
|
|
85
85
|
- **Runtime interaction** - query bus voltages, branch flows, and component observables while paused; inject disturbances on-the-fly
|
|
86
86
|
- **Trajectory post-processing** - extract and plot time-series results from Fortran binary trajectory files
|
|
87
|
+
- **Live monitoring** - ``stepss.monitor`` steps a running simulation and plots the quantities you choose as they are computed
|
|
87
88
|
- **Parameter sweeps** - script multiple simulations with varying parameters or disturbances
|
|
88
89
|
- **Small-signal stability analysis** - compute eigenvalues, damping ratios, participation factors and mode shapes in the engine, or export the system Jacobian and drive your own solver. See ``examples/eigenanalysis/`` for an annotated walkthrough
|
|
89
90
|
- **AC power flow** - the ``stepss.helios`` module runs Helios power flows: solve, modify with redispatch, N-1 contingency screening, and file exports
|
|
@@ -103,7 +104,7 @@ Minimal installation (no plotting or notebook support)::
|
|
|
103
104
|
|
|
104
105
|
pip install stepss
|
|
105
106
|
|
|
106
|
-
**
|
|
107
|
+
**Live plots:** ``stepss.monitor`` draws chosen quantities while a simulation runs. It uses matplotlib, which pip installs above, so nothing further is needed.
|
|
107
108
|
|
|
108
109
|
Linux System Prerequisites
|
|
109
110
|
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|