stepss 3.76__tar.gz → 3.77.1__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.
Files changed (30) hide show
  1. {stepss-3.76/stepss.egg-info → stepss-3.77.1}/PKG-INFO +3 -2
  2. {stepss-3.76 → stepss-3.77.1}/stepss/__init__.py +18 -21
  3. {stepss-3.76 → stepss-3.77.1}/stepss/_bundled.py +1 -1
  4. {stepss-3.76 → stepss-3.77.1}/stepss/cases.py +9 -7
  5. {stepss-3.76 → stepss-3.77.1}/stepss/globals.py +0 -26
  6. {stepss-3.76 → stepss-3.77.1}/stepss/libs/lin/ramses.so +0 -0
  7. {stepss-3.76 → stepss-3.77.1}/stepss/libs/mac/ramses.so +0 -0
  8. {stepss-3.76 → stepss-3.77.1}/stepss/libs/win/ramses.dll +0 -0
  9. stepss-3.77.1/stepss/live.py +477 -0
  10. {stepss-3.76 → stepss-3.77.1/stepss.egg-info}/PKG-INFO +3 -2
  11. {stepss-3.76 → stepss-3.77.1}/stepss.egg-info/SOURCES.txt +1 -0
  12. {stepss-3.76 → stepss-3.77.1}/LICENSE.rst +0 -0
  13. {stepss-3.76 → stepss-3.77.1}/MANIFEST.in +0 -0
  14. {stepss-3.76 → stepss-3.77.1}/README.rst +0 -0
  15. {stepss-3.76 → stepss-3.77.1}/setup.cfg +0 -0
  16. {stepss-3.76 → stepss-3.77.1}/setup.py +0 -0
  17. {stepss-3.76 → stepss-3.77.1}/stepss/extractor.py +0 -0
  18. {stepss-3.76 → stepss-3.77.1}/stepss/helios.py +0 -0
  19. {stepss-3.76 → stepss-3.77.1}/stepss/libs/helios_api.h +0 -0
  20. {stepss-3.76 → stepss-3.77.1}/stepss/libs/lin/libhelios_api.so +0 -0
  21. {stepss-3.76 → stepss-3.77.1}/stepss/libs/mac/libhelios_api.dylib +0 -0
  22. {stepss-3.76 → stepss-3.77.1}/stepss/libs/ramses.h +0 -0
  23. {stepss-3.76 → stepss-3.77.1}/stepss/libs/win/helios_api.dll +0 -0
  24. {stepss-3.76 → stepss-3.77.1}/stepss/scripts/__init__.py +0 -0
  25. {stepss-3.76 → stepss-3.77.1}/stepss/scripts/exec.py +0 -0
  26. {stepss-3.76 → stepss-3.77.1}/stepss/simulator.py +0 -0
  27. {stepss-3.76 → stepss-3.77.1}/stepss.egg-info/dependency_links.txt +0 -0
  28. {stepss-3.76 → stepss-3.77.1}/stepss.egg-info/entry_points.txt +0 -0
  29. {stepss-3.76 → stepss-3.77.1}/stepss.egg-info/requires.txt +0 -0
  30. {stepss-3.76 → stepss-3.77.1}/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.76
3
+ Version: 3.77.1
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
- **Optional:** Install `Gnuplot <http://www.gnuplot.info/>`_ to enable real-time observable plots during simulation. stepss will still work without it, but runtime plots will be disabled.
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,17 +12,19 @@ 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
 
18
19
  Module-level flags set at import time:
19
20
 
20
- - ``__runTimeObs__`` - ``True`` when gnuplot is available on the system PATH and runtime
21
- observable plots are therefore enabled; ``False`` otherwise.
21
+ - ``__runTimeObs__`` - always ``True``. Run-time observables no longer depend on
22
+ anything being installed, so this flag has nothing left to report. It is kept
23
+ because it is public, and it is deprecated: do not branch on it.
22
24
  """
23
25
 
24
26
  __package_name__ = "stepss"
25
- __version__ = '3.76'
27
+ __version__ = '3.77.1'
26
28
  __author__ = "Petros Aristidou"
27
29
  __copyright__ = "Petros Aristidou"
28
30
  __license__ = "Apache-2.0"
@@ -31,33 +33,28 @@ __email__ = "apetros@pm.me"
31
33
  __url__ = "https://stepss.sps-lab.org"
32
34
  __status__ = "5 - Production/Stable"
33
35
 
34
- import sys
35
- from warnings import warn
36
-
37
36
  from . import globals as _globals
38
37
  from .cases import cfg
39
- from .globals import __which, HeliosError
38
+ from .globals import HeliosError
40
39
  from .simulator import sim
41
40
  from .extractor import extractor, curplot, cur
41
+ from .live import monitor
42
42
  from . import helios
43
43
  from .helios import HeliosSession
44
44
  from ._bundled import RAMSES_VERSION as __ramses_version__
45
45
  from ._bundled import HELIOS_VERSION as __helios_version__
46
46
 
47
- __all__ = ["cfg", "sim", "extractor", "cur", "curplot", "helios",
47
+ __all__ = ["cfg", "sim", "extractor", "cur", "curplot", "monitor", "helios",
48
48
  "HeliosSession", "HeliosError"]
49
49
 
50
- # Detect gnuplot at import time; propagate result to globals so that cases.py
51
- # (which reads __runTimeObs__ from globals at import time) also gets the correct value.
52
- if sys.platform in ('win32', 'cygwin'):
53
- checkGnuplot = __which('gnuplot.exe')
54
- else:
55
- checkGnuplot = __which('gnuplot')
56
- if checkGnuplot is None:
57
- warn("RAMSES: Gnuplot executable could not be found in the system path, so the runtime observables are disabled.")
58
- _globals.__runTimeObs__ = False
59
- else:
60
- _globals.__runTimeObs__ = True
61
-
62
- # Re-export the (now-updated) flag under the expected public name.
50
+ # No gnuplot probe. This package has always plotted with matplotlib, through
51
+ # curplot; the probe existed because RAMSES used to draw run-time observables
52
+ # itself by piping to gnuplot, so a machine without gnuplot could not produce
53
+ # them. RAMSES no longer does that: it writes the observable file and calls
54
+ # nothing, so gnuplot's presence has no bearing on anything here.
55
+ #
56
+ # Leaving the probe in place was not merely untidy. addRunObs() gated on this
57
+ # flag and became a silent no-op when the probe failed, so every user without
58
+ # gnuplot installed lost run-time observables for a reason that no longer
59
+ # existed. Kept as a name, always True, because it is documented public API.
63
60
  __runTimeObs__ = _globals.__runTimeObs__
@@ -9,5 +9,5 @@ RAMSES_VERSION is also what the stepss version number is built from, so the
9
9
  leading components of __version__ always name the library bundled here.
10
10
  """
11
11
 
12
- RAMSES_VERSION = "v3.76"
12
+ RAMSES_VERSION = "v3.77"
13
13
  HELIOS_VERSION = "v1.4.1"
@@ -36,7 +36,7 @@ class cfg(object):
36
36
  - ``_disc`` (*list of str*) — path for the discrete events trace output.
37
37
  - ``_obs`` (*list of str*) — observables definition file (.dat or .obs).
38
38
  - ``_trj`` (*list of str*) — Fortran binary trajectory output file (.trj).
39
- - ``_runobs`` (*list of str*) — runtime observable descriptors for gnuplot.
39
+ - ``_runobs`` (*list of str*): runtime observable descriptors.
40
40
  - ``_out`` (*list of str*) — execution log / output trace file.
41
41
 
42
42
  :Example:
@@ -393,7 +393,7 @@ class cfg(object):
393
393
  def addRunObs(self, runobs):
394
394
  """Add runtime observable.
395
395
 
396
- This defines some states that will be displayed during the simulation using gnuplot.
396
+ This defines some states the engine records while the simulation runs.
397
397
 
398
398
  :param str runobs: Description of runtime observable as defined in the RAMSES userguide
399
399
 
@@ -426,11 +426,13 @@ class cfg(object):
426
426
 
427
427
 
428
428
  """
429
- if _globals.__runTimeObs__:
430
- if runobs not in self._runobs:
431
- self._runobs.append(runobs)
432
- else:
433
- warnings.warn('Gnuplot is not the path, so runtime observables are disabled.')
429
+ # Unconditional. This used to be gated on a gnuplot PATH probe, from
430
+ # when RAMSES drew run-time observables itself by piping to gnuplot, and
431
+ # the else branch made this method a silent no-op for anyone without
432
+ # gnuplot installed. The engine writes the observable file and calls
433
+ # nothing, so there is no longer a condition to test.
434
+ if runobs not in self._runobs:
435
+ self._runobs.append(runobs)
434
436
 
435
437
  def clearRunObs(self):
436
438
  """Remove all runtime observable descriptors from the case."""
@@ -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
 
@@ -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.76
3
+ Version: 3.77.1
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
- **Optional:** Install `Gnuplot <http://www.gnuplot.info/>`_ to enable real-time observable plots during simulation. stepss will still work without it, but runtime plots will be disabled.
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
  ~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -8,6 +8,7 @@ stepss/cases.py
8
8
  stepss/extractor.py
9
9
  stepss/globals.py
10
10
  stepss/helios.py
11
+ stepss/live.py
11
12
  stepss/simulator.py
12
13
  stepss.egg-info/PKG-INFO
13
14
  stepss.egg-info/SOURCES.txt
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