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.
Files changed (30) hide show
  1. {stepss-3.77/stepss.egg-info → stepss-3.78}/PKG-INFO +3 -2
  2. {stepss-3.77 → stepss-3.78}/stepss/__init__.py +4 -2
  3. {stepss-3.77 → stepss-3.78}/stepss/_bundled.py +1 -1
  4. {stepss-3.77 → stepss-3.78}/stepss/cases.py +60 -6
  5. {stepss-3.77 → stepss-3.78}/stepss/globals.py +0 -26
  6. {stepss-3.77 → stepss-3.78}/stepss/libs/lin/ramses.so +0 -0
  7. {stepss-3.77 → stepss-3.78}/stepss/libs/mac/ramses.so +0 -0
  8. {stepss-3.77 → stepss-3.78}/stepss/libs/win/ramses.dll +0 -0
  9. stepss-3.78/stepss/live.py +477 -0
  10. {stepss-3.77 → stepss-3.78/stepss.egg-info}/PKG-INFO +3 -2
  11. {stepss-3.77 → stepss-3.78}/stepss.egg-info/SOURCES.txt +1 -0
  12. {stepss-3.77 → stepss-3.78}/LICENSE.rst +0 -0
  13. {stepss-3.77 → stepss-3.78}/MANIFEST.in +0 -0
  14. {stepss-3.77 → stepss-3.78}/README.rst +0 -0
  15. {stepss-3.77 → stepss-3.78}/setup.cfg +0 -0
  16. {stepss-3.77 → stepss-3.78}/setup.py +0 -0
  17. {stepss-3.77 → stepss-3.78}/stepss/extractor.py +0 -0
  18. {stepss-3.77 → stepss-3.78}/stepss/helios.py +0 -0
  19. {stepss-3.77 → stepss-3.78}/stepss/libs/helios_api.h +0 -0
  20. {stepss-3.77 → stepss-3.78}/stepss/libs/lin/libhelios_api.so +0 -0
  21. {stepss-3.77 → stepss-3.78}/stepss/libs/mac/libhelios_api.dylib +0 -0
  22. {stepss-3.77 → stepss-3.78}/stepss/libs/ramses.h +0 -0
  23. {stepss-3.77 → stepss-3.78}/stepss/libs/win/helios_api.dll +0 -0
  24. {stepss-3.77 → stepss-3.78}/stepss/scripts/__init__.py +0 -0
  25. {stepss-3.77 → stepss-3.78}/stepss/scripts/exec.py +0 -0
  26. {stepss-3.77 → stepss-3.78}/stepss/simulator.py +0 -0
  27. {stepss-3.77 → stepss-3.78}/stepss.egg-info/dependency_links.txt +0 -0
  28. {stepss-3.77 → stepss-3.78}/stepss.egg-info/entry_points.txt +0 -0
  29. {stepss-3.77 → stepss-3.78}/stepss.egg-info/requires.txt +0 -0
  30. {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.77
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
- **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,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.77'
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
@@ -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.77"
12
+ RAMSES_VERSION = "v3.78"
13
13
  HELIOS_VERSION = "v1.4.1"
@@ -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 or not self._dstset:
178
- raise RAMSESError('Dataset and disturbance set cannot be empty.')
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
 
@@ -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.77
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
- **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
File without changes