stepss 3.59__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
stepss/__init__.py ADDED
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ """stepss - Python interface to the STEPSS power-system simulation platform.
4
+
5
+ Drives RAMSES, the time-domain dynamic simulator, and Helios, the AC
6
+ power-flow engine, both of which ship as bundled shared libraries.
7
+
8
+ Public API exported by this package:
9
+
10
+ - :class:`~stepss.cases.cfg` - build and manage a simulation case (input/output files).
11
+ - :class:`~stepss.simulator.sim` - load the RAMSES shared library and run simulations.
12
+ - :class:`~stepss.extractor.extractor` - parse Fortran binary trajectory files post-simulation.
13
+ - :class:`~stepss.extractor.cur` - lightweight NamedTuple holding a (time, value, msg) timeseries.
14
+ - :func:`~stepss.extractor.curplot` - plot one or more :class:`cur` objects on a single axes.
15
+ - :class:`~stepss.helios.HeliosSession` - run AC power flows with the Helios engine.
16
+ - :class:`~stepss.globals.HeliosError` - exception raised by Helios calls.
17
+
18
+ Module-level flags set at import time:
19
+
20
+ - ``__runTimeObs__`` - ``True`` when gnuplot is available on the system PATH and runtime
21
+ observable plots are therefore enabled; ``False`` otherwise.
22
+
23
+ This package was previously distributed as ``pyramses``. The ``pyramses``
24
+ distribution on PyPI is now a forwarding shim onto this one.
25
+ """
26
+
27
+ __package_name__ = "stepss"
28
+ __version__ = '3.59'
29
+ __author__ = "Petros Aristidou"
30
+ __copyright__ = "Petros Aristidou"
31
+ __license__ = "Apache-2.0"
32
+ __maintainer__ = "Petros Aristidou"
33
+ __email__ = "apetros@pm.me"
34
+ __url__ = "https://stepss.sps-lab.org"
35
+ __status__ = "5 - Production/Stable"
36
+
37
+ import sys
38
+ from warnings import warn
39
+
40
+ from . import globals as _globals
41
+ from .cases import cfg
42
+ from .globals import __which, HeliosError
43
+ from .simulator import sim
44
+ from .extractor import extractor, curplot, cur
45
+ from . import helios
46
+ from .helios import HeliosSession
47
+ from ._bundled import RAMSES_VERSION as __ramses_version__
48
+ from ._bundled import HELIOS_VERSION as __helios_version__
49
+
50
+ __all__ = ["cfg", "sim", "extractor", "cur", "curplot", "helios",
51
+ "HeliosSession", "HeliosError"]
52
+
53
+ # Detect gnuplot at import time; propagate result to globals so that cases.py
54
+ # (which reads __runTimeObs__ from globals at import time) also gets the correct value.
55
+ if sys.platform in ('win32', 'cygwin'):
56
+ checkGnuplot = __which('gnuplot.exe')
57
+ else:
58
+ checkGnuplot = __which('gnuplot')
59
+ if checkGnuplot is None:
60
+ warn("RAMSES: Gnuplot executable could not be found in the system path, so the runtime observables are disabled.")
61
+ _globals.__runTimeObs__ = False
62
+ else:
63
+ _globals.__runTimeObs__ = True
64
+
65
+ # Re-export the (now-updated) flag under the expected public name.
66
+ __runTimeObs__ = _globals.__runTimeObs__
stepss/_bundled.py ADDED
@@ -0,0 +1,13 @@
1
+ """Upstream component versions bundled in this stepss release.
2
+
3
+ Written by tools/bump_version.sh, which the release automation invokes when
4
+ stepss-ramses or stepss-helios publishes, and on a manual python-only release.
5
+ Do not edit by hand: a manual edit is silently overwritten by the next sync.
6
+
7
+ Only the upstream that triggered a sync changes; the other is carried forward.
8
+ RAMSES_VERSION is also what the stepss version number is built from, so the
9
+ leading components of __version__ always name the library bundled here.
10
+ """
11
+
12
+ RAMSES_VERSION = "v3.59"
13
+ HELIOS_VERSION = "v1.4.1"
stepss/cases.py ADDED
@@ -0,0 +1,555 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ """Simulation case configuration for stepss.
4
+
5
+ Defines :class:`cfg`, which collects all input and output file paths required
6
+ by the RAMSES solver and serialises them into the RAMSES command-file format.
7
+ """
8
+
9
+ import os
10
+ import warnings
11
+
12
+ from . import globals as _globals
13
+ from .globals import RAMSESError, CustomWarning
14
+
15
+ warnings.showwarning = CustomWarning
16
+
17
+
18
+ class cfg(object):
19
+ """Simulation case configuration.
20
+
21
+ Collects and validates all file paths needed to run a RAMSES simulation:
22
+ data files, disturbance file, trajectory file, observables, trace outputs,
23
+ and optional runtime observable definitions. The assembled configuration
24
+ can be serialised to a RAMSES command file via :meth:`writeCmdFile`, which
25
+ is called automatically by :meth:`~stepss.simulator.sim.execSim`.
26
+
27
+ A case can be built programmatically (by calling the ``add*`` methods) or
28
+ loaded from an existing command file by passing its path to *cmd*.
29
+
30
+ **Instance attributes (all private):**
31
+
32
+ - ``_dataset`` (*list of str*) — data files (.dat) defining the network model and solver settings.
33
+ - ``_dstset`` (*list of str*) — the single active disturbance file (.dst).
34
+ - ``_init`` (*list of str*) — path for the initialisation trace output.
35
+ - ``_cont`` (*list of str*) — path for the continuous convergence trace output.
36
+ - ``_disc`` (*list of str*) — path for the discrete events trace output.
37
+ - ``_obs`` (*list of str*) — observables definition file (.dat or .obs).
38
+ - ``_trj`` (*list of str*) — Fortran binary trajectory output file (.trj).
39
+ - ``_runobs`` (*list of str*) — runtime observable descriptors for gnuplot.
40
+ - ``_out`` (*list of str*) — execution log / output trace file.
41
+
42
+ :Example:
43
+
44
+ >>> import stepss
45
+ >>> case = stepss.cfg()
46
+ >>> case.addData('dyn.dat')
47
+ >>> case.addData('volt_rat.dat')
48
+ >>> case.addData('settings.dat')
49
+ >>> case.addInit('init.trace')
50
+ >>> case.addDst('disturbance.dst')
51
+ >>> case.addTrj('output.trj')
52
+ >>> case.addObs('obs.dat')
53
+ """
54
+
55
+ def __init__(self, cmd=None):
56
+ """Initialise a case, optionally loading it from a command file.
57
+
58
+ When *cmd* is ``None`` (the default) all file lists are empty and must
59
+ be populated with the ``add*`` methods before the case can be used.
60
+
61
+ When *cmd* is provided the command file is parsed line-by-line using a
62
+ simple state machine (``typeread``) that moves through eight sections in
63
+ fixed order:
64
+
65
+ 1. Data files — one per line; ends at the first blank line.
66
+ 2. Init trace — single optional line.
67
+ 3. Disturbance — single required line.
68
+ 4. Trajectory — single optional line.
69
+ 5. Observables — required when a trajectory file is present.
70
+ 6. Continuous trace — single optional line.
71
+ 7. Discrete trace — single optional line.
72
+ 8. Runtime observables — zero or more lines.
73
+
74
+ :param cmd: path to an existing RAMSES command file to parse, or
75
+ ``None`` to start with an empty case.
76
+ :type cmd: str or None
77
+ :raises IOError: if *cmd* is given but cannot be read.
78
+ :raises Exception: if the command file is missing required sections
79
+ (at least one data file, a disturbance file, or an
80
+ observables file when a trajectory is defined).
81
+ """
82
+ self._out = [] # output file
83
+ self._dataset = [] # data files
84
+ self._dstset = [] # disturbance files
85
+ self._runobs = [] # runtime observables
86
+ self._init = [] # initialization file
87
+ self._disc = [] # discrete trace file
88
+ self._cont = [] # continuous trace file
89
+ self._obs = [] # observables file
90
+ self._trj = [] # trajectory file
91
+ if cmd:
92
+ try:
93
+ typeread = 1 # 1=data, 2=init, 3=dst, 4=trj, 5=obs,6=cont, 7=disc, 8=runtime obs
94
+ with open(cmd) as data:
95
+ content = data.read().splitlines()
96
+ for line in content:
97
+ token = line.split()
98
+ if typeread == 1:
99
+ if len(token) > 0:
100
+ self.addData(line)
101
+ else:
102
+ if not self._dataset:
103
+ raise Exception('RAMSES: At least one datafile is necessary in the command file.')
104
+ typeread += 1
105
+ elif typeread == 2:
106
+ if len(token) > 0:
107
+ self.addInit(line)
108
+ typeread += 1
109
+ else:
110
+ typeread += 1
111
+ elif typeread == 3:
112
+ if len(token) > 0:
113
+ self.addDst(line)
114
+ typeread += 1
115
+ else:
116
+ raise Exception('RAMSES: At least one dstfile is necessary in the command file.')
117
+ elif typeread == 4:
118
+ if len(token) > 0:
119
+ self.addTrj(line)
120
+ typeread += 1
121
+ else:
122
+ typeread += 2
123
+ elif typeread == 5:
124
+ if len(token) > 0:
125
+ self.addObs(line)
126
+ typeread += 1
127
+ else:
128
+ raise Exception(
129
+ 'RAMSES: Since a trajectory file was defined, an observable file should be given.')
130
+ elif typeread == 6:
131
+ if len(token) > 0:
132
+ self.addCont(line)
133
+ typeread += 1
134
+ else:
135
+ typeread += 1
136
+ elif typeread == 7:
137
+ if len(token) > 0:
138
+ self.addDisc(line)
139
+ typeread += 1
140
+ else:
141
+ typeread += 1
142
+ elif typeread == 8:
143
+ if len(token) > 0:
144
+ self.addRunObs(line)
145
+ else:
146
+ typeread += 1
147
+ except IOError:
148
+ raise
149
+
150
+ def writeCmdFile(self, userFile=None):
151
+ """Serialise the case to a RAMSES command file.
152
+
153
+ The command file format is a fixed-section plain-text file consumed
154
+ directly by the RAMSES solver. Sections are separated by blank lines
155
+ and appear in this order:
156
+
157
+ 1. Data files (one per line)
158
+ 2. Initialisation trace path (or blank)
159
+ 3. Disturbance file path
160
+ 4. Trajectory file path + observables file path (or blank)
161
+ 5. Continuous trace path (or blank)
162
+ 6. Discrete trace path (or blank)
163
+ 7. Runtime observable descriptors (zero or more lines)
164
+
165
+ :param userFile: path where the command file should be written. If
166
+ ``None`` the serialised text is returned as a string
167
+ without writing to disk.
168
+ :type userFile: str or None
169
+ :returns: ``0`` after writing to *userFile*, or the command-file text
170
+ as a string when *userFile* is ``None``.
171
+ :rtype: int or str
172
+ :raises RAMSESError: if ``_dataset`` or ``_dstset`` is empty.
173
+ :raises IOError: if the file cannot be written.
174
+
175
+ .. warning:: If *userFile* already exists it will be overwritten.
176
+ """
177
+ if not self._dataset or not self._dstset:
178
+ raise RAMSESError('Dataset and disturbance set cannot be empty.')
179
+
180
+ cmdFile = ""
181
+
182
+ try:
183
+ for i in self._dataset:
184
+ cmdFile = cmdFile + i + '\n'
185
+ cmdFile = cmdFile + '\n'
186
+ if self._init:
187
+ cmdFile = cmdFile + self._init[0] + '\n'
188
+ else:
189
+ cmdFile = cmdFile + '\n'
190
+ if self._dstset:
191
+ cmdFile = cmdFile + self._dstset[0] + '\n'
192
+ if self._trj:
193
+ cmdFile = cmdFile + self._trj[0] + '\n'
194
+ cmdFile = cmdFile + self._obs[0] + '\n'
195
+ else:
196
+ cmdFile = cmdFile + '\n'
197
+ if self._cont:
198
+ cmdFile = cmdFile + self._cont[0] + '\n'
199
+ else:
200
+ cmdFile = cmdFile + '\n'
201
+ if self._disc:
202
+ cmdFile = cmdFile + self._disc[0] + '\n'
203
+ else:
204
+ cmdFile = cmdFile + '\n'
205
+ for i in self._runobs:
206
+ cmdFile = cmdFile + i + '\n'
207
+ cmdFile = cmdFile + '\n'
208
+ cmdFile = cmdFile + '\n'
209
+ except IOError as e:
210
+ raise IOError("RAMSES: I/O error({0}): {1}".format(e.errno, e.strerror))
211
+
212
+ if userFile is None:
213
+ return cmdFile
214
+ else:
215
+ if os.path.isfile(userFile):
216
+ warnings.warn('The file %s already exists. It will be overwritten!' % (userFile))
217
+ with open(userFile, "w") as text_file:
218
+ text_file.write(cmdFile)
219
+ return 0
220
+
221
+ # def __del__(self):
222
+ # silentremove(self._cmdfile.name)
223
+
224
+ def addInit(self, afile):
225
+ """Set the file where the simulation initialisation trace will be saved.
226
+
227
+ The initialisation trace records the output of RAMSES's power-flow
228
+ initialisation step. Only one initialisation file can be active at a
229
+ time; calling this method again replaces any previously set path.
230
+
231
+ :param str afile: the filename. The complete path can be given or a path
232
+ relative to the working directory (``os.getcwd()``).
233
+
234
+ :Example:
235
+
236
+ >>> import stepss
237
+ >>> case1 = stepss.cfg()
238
+ >>> case1.addInit("init.trace")
239
+
240
+ .. warning:: If the file already exists, it will be overwritten without warning!
241
+ """
242
+ del self._init[:]
243
+ if os.path.isfile(afile):
244
+ warnings.warn('The file %s already exists. It will be overwritten!' % (afile))
245
+ self._init.append(afile)
246
+
247
+ def getInit(self):
248
+ """Return the initialisation trace file path.
249
+
250
+ :returns: path of the initialisation trace file, or an empty string if none is set.
251
+ :rtype: str
252
+ """
253
+ if self._init:
254
+ return self._init[0]
255
+ else:
256
+ return ''
257
+
258
+ def addOut(self, afile):
259
+ """Define the file where the simulation output will be saved.
260
+
261
+ :param str afile: the filename. The complete path must be given.
262
+
263
+ :Example:
264
+
265
+ >>> import stepss
266
+ >>> import os
267
+ >>> case1 = stepss.cfg()
268
+ >>> case1.addOut(os.path.join(os.getcwd(),'output.trace'))
269
+
270
+ .. note:: If the file already exists, the output will be appended to that file.
271
+ """
272
+ del self._out[:]
273
+ self._out.append(afile)
274
+
275
+ def getOut(self):
276
+ """Return the output trace file path.
277
+
278
+ :returns: path of the output trace file, or an empty string if none is set.
279
+ :rtype: str
280
+ """
281
+ if self._out:
282
+ return self._out[0]
283
+ else:
284
+ return ''
285
+
286
+ def addCont(self, afile):
287
+ """Set the continuous trace output file.
288
+
289
+ The continuous trace records convergence information from the numerical
290
+ integration algorithm inside RAMSES. It is mainly useful for debugging
291
+ solver issues but can slow down the simulation significantly.
292
+
293
+ :param str afile: the filename. The complete path can be given or a path
294
+ relative to the working directory (``os.getcwd()``).
295
+
296
+ :Example:
297
+
298
+ >>> import stepss
299
+ >>> case1 = stepss.cfg()
300
+ >>> case1.addCont("cont.trace")
301
+
302
+ .. warning:: If the file already exists, it will be overwritten without warning!
303
+ """
304
+ del self._cont[:]
305
+ if os.path.isfile(afile):
306
+ warnings.warn('The file %s already exists. It will be overwritten!' % (afile))
307
+ self._cont.append(afile)
308
+
309
+ def getCont(self):
310
+ """Return the continuous trace file path.
311
+
312
+ :returns: path of the continuous trace file, or an empty string if none is set.
313
+ :rtype: str
314
+
315
+ """
316
+ if self._cont:
317
+ return self._cont[0]
318
+ else:
319
+ return ''
320
+
321
+ def addTrj(self, afile):
322
+ """Set the Fortran binary trajectory output file.
323
+
324
+ The trajectory file is written by RAMSES during the simulation and later
325
+ parsed by :class:`~stepss.extractor.extractor` to extract timeseries
326
+ for individual components.
327
+
328
+ :param str afile: the filename. The complete path can be given or a path
329
+ relative to the working directory (``os.getcwd()``).
330
+
331
+ :Example:
332
+
333
+ >>> import stepss
334
+ >>> case1 = stepss.cfg()
335
+ >>> case1.addTrj("output.trj")
336
+
337
+ .. warning:: If the file already exists, it will be overwritten without warning!
338
+ """
339
+ del self._trj[:]
340
+ if os.path.isfile(afile):
341
+ warnings.warn('The file %s already exists. It will be overwritten!' % (afile))
342
+ self._trj.append(afile)
343
+
344
+ def getTrj(self):
345
+ """Return the trajectory output file path.
346
+
347
+ :returns: path of the trajectory file, or an empty string if none is set.
348
+ :rtype: str
349
+ """
350
+ if self._trj:
351
+ return self._trj[0]
352
+ else:
353
+ return ''
354
+
355
+ def addDisc(self, afile):
356
+ """Set the discrete events trace output file.
357
+
358
+ The discrete trace records events that occur during the simulation, such
359
+ as discrete controller transitions, disturbance-file events, and state
360
+ changes inside injector, two-port, torque, or exciter models.
361
+
362
+ :param str afile: the filename. The complete path can be given or a path
363
+ relative to the working directory (``os.getcwd()``).
364
+
365
+ :Example:
366
+
367
+ >>> import stepss
368
+ >>> case1 = stepss.cfg()
369
+ >>> case1.addDisc("disc.trace")
370
+
371
+ .. warning:: If the file already exists, it will be overwritten without warning!
372
+ """
373
+ del self._disc[:]
374
+ if os.path.isfile(afile):
375
+ warnings.warn('The file %s already exists. It will be overwritten!' % (afile))
376
+ self._disc.append(afile)
377
+
378
+ def clearDisc(self):
379
+ """Remove the discrete trace file path from the case."""
380
+ del self._disc[:]
381
+
382
+ def getDisc(self):
383
+ """Return the discrete trace file path.
384
+
385
+ :returns: path of the discrete trace file, or an empty string if none is set.
386
+ :rtype: str
387
+ """
388
+ if self._disc:
389
+ return self._disc[0]
390
+ else:
391
+ return ''
392
+
393
+ def addRunObs(self, runobs):
394
+ """Add runtime observable.
395
+
396
+ This defines some states that will be displayed during the simulation using gnuplot.
397
+
398
+ :param str runobs: Description of runtime observable as defined in the RAMSES userguide
399
+
400
+ :Example:
401
+
402
+ - BV BUSNAME: Voltage magnitude of bus::
403
+
404
+ >>> case.addRunObs('BV 1041') # observe the bus voltage of bus 1041
405
+
406
+ - MS SYNHRONOUS_MACHINE: Synchronous speed of machine::
407
+
408
+ >>> case.addRunObs('MS g1')
409
+
410
+ - RT RT: Real-time vs simulated time plot::
411
+
412
+ >>> case.addRunObs('RT RT')
413
+
414
+ - BPE/BQE/BPO/BQO BRANCH_NAME: Branch active (P), reactive (Q) power at the origin (O) or extremity (E) of a branch::
415
+
416
+ >>> case.addRunObs('BPO 1041-01') # active power at the origin of branch 1041-01
417
+ >>> case.addRunObs('BPE 1041-01') # active power at the extremity of branch 1041-01
418
+
419
+ - ON INJECTOR_NAME OBSERVABLE_NAME: Monitor a named observable from an injector ::
420
+
421
+ >>> case.addRunObs('ON WT1a Pw') # observable Pw from injector WT1a
422
+
423
+ - TO TWOP_NAME OBSERVABLE_NAME: Monitor a named observable from a two-port ::
424
+
425
+ >>> case.addRunObs('TO hvdc1 P1') # observable P1 from two-port hvdc1
426
+
427
+
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.')
434
+
435
+ def clearRunObs(self):
436
+ """Remove all runtime observable descriptors from the case."""
437
+ del self._runobs[:]
438
+
439
+ def addObs(self, ofile):
440
+ """Add observables file
441
+
442
+ :param str ofile: the filename. The complete path can be given or a the path relative to the working directory (os.getcwd())
443
+
444
+ :Example:
445
+
446
+ >>> import stepss
447
+ >>> case1 = stepss.cfg()
448
+ >>> case1.addObs("obs.dat")
449
+
450
+ """
451
+ if os.path.isfile(ofile):
452
+ del self._obs[:]
453
+ self._obs.append(ofile)
454
+ else:
455
+ raise IOError('RAMSES: The observables file %s does not exist or is not valid.' % (ofile))
456
+
457
+ def getObs(self):
458
+ """Return the observables file path.
459
+
460
+ :returns: path of the observables file, or an empty string if none is set.
461
+ :rtype: str
462
+ """
463
+ if self._obs:
464
+ return self._obs[0]
465
+ else:
466
+ return ''
467
+
468
+ def addData(self, datafile):
469
+ """Add a data file to the dataset.
470
+
471
+ Data files provide the network model (dynamic models, power-flow solution)
472
+ and solver parameters consumed by RAMSES. Multiple data files can be
473
+ added; they are passed to the solver in the order they were added.
474
+ Duplicate paths are silently ignored.
475
+
476
+ :param str datafile: path to an existing data file. The complete path
477
+ can be given or a path relative to the working
478
+ directory (``os.getcwd()``).
479
+
480
+ :Example:
481
+
482
+ >>> import stepss
483
+ >>> case1 = stepss.cfg()
484
+ >>> case1.addData("dyn_A.dat")
485
+
486
+ .. warning:: At least one data file must be added before running a simulation.
487
+ """
488
+ if os.path.isfile(datafile):
489
+ if datafile not in self._dataset:
490
+ self._dataset.append(datafile)
491
+ else:
492
+ raise IOError('RAMSES: The datafile %s does not exist or is not valid.' % (datafile))
493
+
494
+ def getData(self):
495
+ """Get datafile list
496
+
497
+ :returns: list of data file names
498
+ :rtype: list
499
+
500
+ """
501
+ return self._dataset
502
+
503
+ def delData(self, datafile):
504
+ """Remove a specific data file from the dataset.
505
+
506
+ :param str datafile: path of the data file to remove. No error is
507
+ raised if the file is not currently in the dataset.
508
+ """
509
+ if datafile in self._dataset:
510
+ self._dataset.remove(datafile)
511
+
512
+ def clearData(self):
513
+ """Remove all data files from the dataset."""
514
+ del self._dataset[:]
515
+
516
+ def addDst(self, dstfile):
517
+ """Set the disturbance file for the simulation.
518
+
519
+ The disturbance file defines the fault or event sequence to be applied
520
+ during the simulation. Only one disturbance file is active at a time;
521
+ calling this method again replaces the previous one.
522
+
523
+ :param str dstfile: path to an existing ``.dst`` file. The complete
524
+ path can be given or a path relative to the working
525
+ directory (``os.getcwd()``).
526
+
527
+ :Example:
528
+
529
+ >>> import stepss
530
+ >>> case1 = stepss.cfg()
531
+ >>> case1.addDst("short.dst")
532
+
533
+ .. warning:: A disturbance file must be provided before the simulation can start.
534
+ """
535
+ if os.path.isfile(dstfile):
536
+ del self._dstset[:]
537
+ self._dstset.append(dstfile)
538
+ else:
539
+ raise IOError('RAMSES: The dstfile %s does not exist or is not valid.' % (dstfile))
540
+
541
+ def getDst(self):
542
+ """Get disturbance
543
+
544
+ :returns: name of disturbance file
545
+ :rtype: str
546
+
547
+ """
548
+ if self._dstset:
549
+ return self._dstset[0]
550
+ else:
551
+ return ''
552
+
553
+ def clearDst(self):
554
+ """Remove the disturbance file from the case."""
555
+ del self._dstset[:]