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/simulator.py ADDED
@@ -0,0 +1,1072 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ """Python interface to the RAMSES dynamic power-system simulator.
4
+
5
+ Provides :class:`sim`, a ctypes-based wrapper around the RAMSES shared library
6
+ (``ramses.dll`` on Windows, ``ramses.so`` on Linux and macOS). Function
7
+ signatures are read automatically from the bundled ``ramses.h`` C header at
8
+ instantiation time, so the wrapper is self-describing and does not hard-code
9
+ argument types.
10
+ """
11
+
12
+ import ctypes
13
+ import _ctypes
14
+ import os
15
+ import sys
16
+ import warnings
17
+ import datetime
18
+ import numpy as np
19
+ from scipy.sparse import coo_matrix
20
+
21
+ from .cases import cfg
22
+ from .globals import RAMSESError, CustomWarning, __libdir__, __platlibdir__, wrapToList
23
+
24
+ class sim(object):
25
+ """Interface to a RAMSES solver instance.
26
+
27
+ Each :class:`sim` object loads the RAMSES shared library into the current
28
+ process and exposes the solver's C API through Python methods. The library
29
+ is unloaded (via :func:`ctypes.FreeLibrary` / :func:`ctypes._ctypes.dlclose`)
30
+ when the instance is garbage-collected.
31
+
32
+ **Thread safety:** RAMSES maintains internal global state; running multiple
33
+ :class:`sim` instances concurrently in separate threads is not supported.
34
+ Multiple sequential instances within the same process are safe.
35
+
36
+ **Class attributes:**
37
+
38
+ - ``ramsesCount`` (*int*) — number of :class:`sim` instances currently alive.
39
+ Incremented in :meth:`__init__` and decremented in :meth:`__del__`.
40
+ """
41
+
42
+ warnings.showwarning = CustomWarning
43
+
44
+ ramsesCount = 0 # number of active sim instances
45
+ os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE"
46
+
47
+ def __init__(self, custLibDir = None):
48
+ """Load the RAMSES shared library and initialise C function signatures.
49
+
50
+ On Windows the library is ``ramses.dll``; on all other platforms
51
+ (Linux and macOS) it is ``ramses.so``. By default the library bundled
52
+ with the package for the current platform is used
53
+ (``src/stepss/libs/win/``, ``libs/lin/`` or ``libs/mac/``). A custom
54
+ directory can be supplied via *custLibDir* to override that library —
55
+ useful for testing pre-release solver builds; the directory must
56
+ contain the library file directly (no platform subfolder).
57
+
58
+ After loading the library, :meth:`_setcalls` is invoked to parse
59
+ ``ramses.h`` and configure the ctypes argument/return types for every
60
+ exported function.
61
+
62
+ :param custLibDir: path to a directory containing an alternative
63
+ ``ramses.dll`` / ``ramses.so``. The directory must
64
+ exist. When provided, the library file in that
65
+ location will be locked by the OS until this instance
66
+ is deleted.
67
+ :type custLibDir: str or None
68
+ :raises RAMSESError: if *custLibDir* is given but does not exist or is
69
+ not a directory.
70
+ :raises ImportError: if the shared library cannot be loaded (e.g. missing
71
+ system runtime libraries such as OpenBLAS on Linux).
72
+ """
73
+ if custLibDir is None:
74
+ ramLibDir = __platlibdir__
75
+ else:
76
+ try:
77
+ if os.path.exists(custLibDir) and os.path.isdir(custLibDir):
78
+ ramLibDir = custLibDir
79
+ warnings.warn("Overwriting the internal DLL with the one at %s. The DLL in that location will be locked. To unlock it, you need to del() this instance of stepss.sim() or restart the kernel." % ramLibDir)
80
+ else:
81
+ raise RAMSESError('RAMSES: The path %s does not exist or it is not a directory.' % (custLibDir))
82
+ except OSError as e:
83
+ raise RAMSESError('RAMSES: The path %s gave an error.' % custLibDir) from e
84
+
85
+ try:
86
+ if sys.platform in ('win32', 'cygwin'):
87
+ self._ramseslib = ctypes.CDLL(os.path.join(ramLibDir, "ramses.dll"))
88
+ else:
89
+ self._ramseslib = ctypes.CDLL(os.path.join(ramLibDir, "ramses.so"))
90
+ except OSError as e:
91
+ raise ImportError('RAMSES: Could not load shared library from %s.' % ramLibDir) from e
92
+
93
+ sim.ramsesCount += 1
94
+ self._ramsesNum = sim.ramsesCount # This is the current instance of Ramses
95
+ self._setcalls()
96
+
97
+ def __del__(self):
98
+ """Unload the RAMSES shared library and decrement the instance counter.
99
+
100
+ Called automatically by the garbage collector. Releases the OS-level
101
+ handle to the DLL/SO so the file can be replaced or deleted afterwards.
102
+ Guards against partially-initialised instances (e.g. if ``__init__``
103
+ raised before the library was loaded).
104
+ """
105
+ if not hasattr(self, '_ramseslib'):
106
+ return
107
+ warnings.warn("Simulator with number %i was deleted." % getattr(self, '_ramsesNum', -1))
108
+ if sys.platform in ('win32', 'cygwin'):
109
+ _ctypes.FreeLibrary(self._ramseslib._handle)
110
+ else:
111
+ _ctypes.dlclose(self._ramseslib._handle)
112
+ sim.ramsesCount -= 1
113
+
114
+ def _c_func_wrapper(self, cdecl_text):
115
+ """Parse a single C function declaration and configure its ctypes binding.
116
+
117
+ Reads one line from ``ramses.h``, extracts the return type, function
118
+ name, and parameter types, then sets ``restype`` and ``argtypes`` on the
119
+ corresponding function object retrieved from ``self._ramseslib``.
120
+
121
+ :param str cdecl_text: a single-line C function declaration in the form
122
+ ``<return_type> <name>(<param_type> <param_name>, ...)``
123
+ :raises KeyError: (internally caught) if a C type has no ctypes mapping.
124
+
125
+ .. note:: Pointer qualifiers (``*``) attached to the variable name are
126
+ moved to the type string before lookup, so both ``char* x``
127
+ and ``char *x`` are handled correctly.
128
+ """
129
+
130
+ def move_pointer_and_strip(type_def, name):
131
+ """Move any trailing ``*`` from *name* onto *type_def* and strip whitespace."""
132
+ if '*' in name:
133
+ type_def += ' ' + name[:name.rindex('*') + 1]
134
+ name = name.rsplit('*', 1)[1]
135
+ return type_def.strip(), name.strip()
136
+
137
+ def type_lookup(type_def):
138
+ '''Supported C variable types and their ctypes equivalents.'''
139
+ types = {
140
+ 'void': None,
141
+ 'char *': ctypes.c_char_p,
142
+ 'int': ctypes.c_int,
143
+ 'int *': ctypes.POINTER(ctypes.c_int),
144
+ 'void *': ctypes.c_void_p,
145
+ 'size_t': ctypes.c_size_t,
146
+ 'size_t *': ctypes.POINTER(ctypes.c_size_t),
147
+ 'double': ctypes.c_double,
148
+ 'double *': ctypes.POINTER(ctypes.c_double)
149
+ }
150
+ type_def_without_const = type_def.replace('const ', '')
151
+ if type_def_without_const in types:
152
+ return types[type_def_without_const]
153
+ elif (type_def_without_const.endswith('*') and type_def_without_const[:-1] in types):
154
+ return ctypes.POINTER(types[type_def_without_const[:-1]])
155
+ else:
156
+ raise KeyError(type_def)
157
+
158
+ a, b = [i.strip() for i in cdecl_text.split('(', 1)]
159
+ params, _ = b.rsplit(')', 1)
160
+ rtn_type, name = move_pointer_and_strip(*a.rsplit(' ', 1))
161
+ param_spec = []
162
+ for param in params.split(','):
163
+ if param != 'void':
164
+ param_spec.append(move_pointer_and_strip(*param.rsplit(' ', 1)))
165
+
166
+ try:
167
+ func = getattr(self._ramseslib, name) # get the function from the dll
168
+ setattr(func, 'restype', type_lookup(rtn_type)) # set the return type
169
+ setattr(func, 'argtypes', [type_lookup(type_def) for type_def, _ in param_spec]) # set the argument types
170
+ except AttributeError as e:
171
+ warnings.warn('RAMSES: Function %s is listed in ramses.h but cannot be found in the library.' % (name))
172
+ warnings.warn(str(e))
173
+
174
+ def _setcalls(self):
175
+ """Parse ``ramses.h`` and configure ctypes bindings for all exported functions.
176
+
177
+ Reads the C header file bundled with the package line by line, skipping
178
+ blank lines and ``//`` comments, and delegates each declaration to
179
+ :meth:`_c_func_wrapper`.
180
+
181
+ :raises IOError: if ``ramses.h`` cannot be opened.
182
+ """
183
+ try:
184
+ with open(os.path.join(__libdir__, "ramses.h"), 'r') as f:
185
+ _C_HEADER = f.read()
186
+ for cdecl_text in _C_HEADER.splitlines():
187
+ if cdecl_text.strip():
188
+ if not cdecl_text.startswith("//"):
189
+ self._c_func_wrapper(cdecl_text)
190
+ except IOError as e:
191
+ raise IOError("RAMSES: Cannot open ramses.h files", e)
192
+
193
+ def getLastErr(self):
194
+ """Return the last error message issued by RAMSES.
195
+
196
+ :rtype: str
197
+
198
+ :Example:
199
+
200
+ >>> import stepss
201
+ >>> ram = stepss.sim()
202
+ >>> case = stepss.cfg("cmd.txt")
203
+ >>> ram.execSim(case, 0) # start simulation paused
204
+ >>> ram.getLastErr()
205
+ 'This is an error msg'
206
+
207
+ """
208
+
209
+ errMsg = ctypes.create_string_buffer(1024)
210
+
211
+ try:
212
+ retval = self._ramseslib.get_last_err_log(errMsg)
213
+ except KeyError:
214
+ retval = 1
215
+
216
+ if (retval != 0):
217
+ raise RAMSESError('RAMSES: Function getLastErr failed.')
218
+
219
+ return errMsg.value.decode()
220
+
221
+ def getJac(self):
222
+ """Return the system Jacobian matrices written by RAMSES to temporary files.
223
+
224
+ Triggers the RAMSES ``get_Jac`` routine, which writes two sparse matrix
225
+ files to the current working directory (``py_eqs.dat`` for the structural
226
+ incidence matrix *E* and ``py_val.dat`` for the Jacobian values *A*).
227
+ Both files are then parsed and returned as SciPy CSC sparse matrices.
228
+
229
+ The simulation must have been initialised (i.e. :meth:`execSim` called
230
+ with ``pause=0.0``) before this method is called.
231
+
232
+ :returns: tuple ``(A, E)`` where *A* is the Jacobian value matrix and
233
+ *E* is the structural incidence matrix, both as
234
+ :class:`scipy.sparse.csc_matrix` of shape ``(N, N)``.
235
+ :rtype: tuple
236
+
237
+ :Example:
238
+
239
+ >>> import stepss
240
+ >>> ram = stepss.sim()
241
+ >>> case = stepss.cfg("cmd.txt")
242
+ >>> ram.execSim(case, 0) # start simulation paused
243
+ >>> ram.getJac()
244
+
245
+ """
246
+ try:
247
+ retval = self._ramseslib.get_Jac()
248
+ except KeyError:
249
+ retval = 1
250
+
251
+ if (retval != 0) and (retval != 112):
252
+ raise RAMSESError('RAMSES: Function get_Jac failed.')
253
+
254
+ Nmat = 0
255
+ try:
256
+ with open('py_eqs.dat', 'r') as f:
257
+ lines = f.readlines()
258
+ rowl = []
259
+ coll = []
260
+ datal = []
261
+ for x in lines:
262
+ xsplit = x.split()
263
+ if int(xsplit[0]) > Nmat:
264
+ Nmat = int(xsplit[0])
265
+ if int(xsplit[5]) > 0:
266
+ rowl.append(int(xsplit[0])-1)
267
+ coll.append(int(xsplit[5])-1)
268
+ datal.append(1)
269
+ row = np.array( rowl, dtype=int )
270
+ col = np.array( coll, dtype=int )
271
+ data = np.array( datal, dtype=float )
272
+ E = coo_matrix((data,(row,col)), shape=(Nmat,Nmat)).tocsc()
273
+ except Exception:
274
+ raise RAMSESError('RAMSES: Function get_Jac failed while reading E.')
275
+
276
+ try:
277
+ with open('py_val.dat', 'r') as f:
278
+ lines = f.readlines()
279
+ row = np.array( [ int(x.split()[0])-1 for x in lines ], dtype=int )
280
+ col = np.array( [ int(x.split()[1])-1 for x in lines ], dtype=int )
281
+ data = np.array( [ x.split()[2] for x in lines ], dtype=float )
282
+ A = coo_matrix((data,(row,col)), shape=(Nmat,Nmat)).tocsc()
283
+ except Exception:
284
+ raise RAMSESError('RAMSES: Function get_Jac failed while reading A.')
285
+
286
+ return A, E
287
+
288
+ def getCompName(self, comp_type, num):
289
+ """Return the name of the *num*-th component of the given type.
290
+
291
+ :param str comp_type: component type string — one of ``'BUS'``,
292
+ ``'SYNC'``, ``'INJ'``, ``'DCTL'``, ``'BRANCH'``,
293
+ ``'TWOP'``, ``'SHUNT'``, ``'LOAD'``.
294
+ :param int num: 1-based index of the component.
295
+ :returns: component name as returned by RAMSES (trailing whitespace stripped).
296
+ :rtype: str
297
+ :raises RAMSESError: if the component does not exist or the call fails.
298
+
299
+ :Example:
300
+
301
+ >>> import stepss
302
+ >>> ram = stepss.sim()
303
+ >>> case = stepss.cfg("cmd.txt")
304
+ >>> ram.execSim(case, 0) # start simulation paused
305
+ >>> ram.getCompName("BUS",1)
306
+ 'B1'
307
+
308
+ """
309
+ get_name_func = {
310
+ 'BUS': self._ramseslib.get_bus_name,
311
+ 'SYNC': self._ramseslib.get_sync_name,
312
+ 'INJ': self._ramseslib.get_inj_name,
313
+ 'DCTL': self._ramseslib.get_dctl_name,
314
+ 'BRANCH': self._ramseslib.get_branch_name,
315
+ 'TWOP': self._ramseslib.get_twop_name,
316
+ 'SHUNT': self._ramseslib.get_shunt_name,
317
+ 'LOAD': self._ramseslib.get_load_name
318
+ }
319
+
320
+ name = ctypes.create_string_buffer(21)
321
+
322
+ try:
323
+ retval = get_name_func[comp_type](num, name)
324
+ except KeyError:
325
+ retval = 1
326
+
327
+ if (retval != 0):
328
+ raise RAMSESError(
329
+ 'RAMSES: Function getCompName(%s,%i) failed. Does this component exist?' % (comp_type, num))
330
+
331
+ return name.value.decode()
332
+
333
+ def getAllCompNames(self, comp_type):
334
+ """Return the names of all components of the given type.
335
+
336
+ :param str comp_type: component type string — one of ``'BUS'``,
337
+ ``'SYNC'``, ``'INJ'``, ``'DCTL'``, ``'BRANCH'``,
338
+ ``'TWOP'``, ``'SHUNT'``, ``'LOAD'``.
339
+ :returns: list of component names (empty list if none exist).
340
+ :rtype: list of str
341
+ :raises RAMSESError: if *comp_type* is not recognised.
342
+
343
+ :Example:
344
+
345
+ >>> import stepss
346
+ >>> ram = stepss.sim()
347
+ >>> case = stepss.cfg("cmd.txt")
348
+ >>> ram.execSim(case, 0) # start simulation paused
349
+ >>> ram.getAllCompNames("BUS")
350
+ ['B1',
351
+ 'B2',
352
+ 'B3']
353
+
354
+ """
355
+ get_nb_func = {
356
+ 'BUS': self._ramseslib.get_nbbus,
357
+ 'SYNC': self._ramseslib.get_nbsync,
358
+ 'INJ': self._ramseslib.get_nbinj,
359
+ 'DCTL': self._ramseslib.get_nbdctl,
360
+ 'BRANCH': self._ramseslib.get_nbbra,
361
+ 'TWOP': self._ramseslib.get_nbtwop,
362
+ 'SHUNT': self._ramseslib.get_nbshunt,
363
+ 'LOAD': self._ramseslib.get_nbload
364
+ }
365
+
366
+ try:
367
+ Nb = get_nb_func[comp_type]()
368
+ except KeyError:
369
+ raise RAMSESError(
370
+ 'RAMSES: Function getAllCompName(%s) failed. Does this component type exists?' % (comp_type))
371
+
372
+ names = []
373
+ if Nb > 0:
374
+ for comp in range(1, Nb + 1):
375
+ names.append(self.getCompName(comp_type, comp))
376
+ return names
377
+
378
+ def execSim(self, cmd, pause=None):
379
+ """Execute a RAMSES simulation.
380
+
381
+ Serialises *cmd* to a RAMSES command file (via :meth:`~stepss.cfg.writeCmdFile`),
382
+ then calls the RAMSES solver. If *pause* is supplied, the simulation is
383
+ scheduled to stop at that simulated time and can be resumed later with
384
+ :meth:`contSim`. Pass ``pause=0.0`` to initialise only (power-flow
385
+ solution) without running the dynamic simulation.
386
+
387
+ :param cmd: case configuration describing all input and output files.
388
+ :type cmd: :class:`stepss.cfg`
389
+ :param pause: simulated time (seconds) at which to pause, or ``None``
390
+ to run until the end of the disturbance scenario.
391
+ :type pause: float or None
392
+ :returns: ``0`` on success.
393
+ :rtype: int
394
+ :raises TypeError: if *cmd* is not a :class:`stepss.cfg` instance.
395
+ :raises RAMSESError: if the solver returns a non-zero, non-112 flag.
396
+
397
+ .. note:: Return code ``112`` means the simulation paused normally; it
398
+ is treated as success by this method.
399
+
400
+ :Example:
401
+
402
+ >>> import stepss
403
+ >>> ram = stepss.sim()
404
+ >>> case = stepss.cfg("cmd.txt")
405
+ >>> ram.execSim(case) # run to end
406
+ >>> ram.execSim(case, pause=0.0) # initialise only, then pause
407
+
408
+ .. note:: If you have an existing command file, you can load it with
409
+ ``stepss.cfg("cmd.txt")`` before passing it here.
410
+ """
411
+ if not isinstance(cmd, cfg):
412
+ raise TypeError('RAMSES: Function execSim failed because the command file is not of type stepss.cfg()')
413
+ if pause is not None:
414
+ self.pauseSim(pause)
415
+ cmdfilename = cmd.writeCmdFile()
416
+ if not cmd._out:
417
+ outfilename = os.path.join(os.getcwd(),'output '+datetime.datetime.now().strftime("%d.%m.%Y-%H.%M.%S")+'.trace')
418
+ else:
419
+ outfilename = cmd._out[0]
420
+ retval = self._ramseslib.ramses(cmdfilename.encode('utf-8'), outfilename.encode('utf-8'))
421
+ if (retval != 0) and (retval != 112):
422
+ raise RAMSESError('RAMSES: Function execSim() failed with the flag %i. Last message was: %s' % (retval, self.getLastErr()))
423
+ return 0
424
+
425
+ def contSim(self, pause=None):
426
+ """Continue a paused simulation until *pause* seconds of simulated time.
427
+
428
+ If *pause* is given, :meth:`pauseSim` is called first to schedule the
429
+ next stop. Pass ``self.getInfTime()`` to run until the end of the
430
+ disturbance scenario.
431
+
432
+ :param pause: simulated time (in seconds) at which to pause again, or
433
+ ``None`` to keep the currently scheduled pause time.
434
+ :type pause: float or None
435
+ :returns: ``0`` on success.
436
+ :rtype: int
437
+ :raises RAMSESError: if the RAMSES call returns a non-zero, non-112 flag.
438
+
439
+ :Example:
440
+
441
+ >>> import stepss
442
+ >>> ram = stepss.sim()
443
+ >>> case = stepss.cfg("cmd.txt")
444
+ >>> ram.execSim(case, 0.0) # initialise and pause at t=0
445
+ >>> ram.contSim(ram.getInfTime()) # run to end of scenario
446
+ """
447
+ if pause is not None:
448
+ self.pauseSim(pause)
449
+ retval = self._ramseslib.continue_simul()
450
+ if (retval != 0) and (retval != 112):
451
+ raise RAMSESError('RAMSES: Function contSim() failed with the flag %i. Last message was: %s' % (retval, self.getLastErr()))
452
+ return 0
453
+
454
+ def getBusVolt(self, busNames):
455
+ """Return the voltage magnitude of a list of buses
456
+
457
+ :param busNames: the names of buses
458
+ :type busNames: list of str
459
+ :returns: list of bus voltage magnitudes
460
+ :rtype: list of floats
461
+
462
+ :Example:
463
+
464
+ >>> import stepss
465
+ >>> ram = stepss.sim()
466
+ >>> case = stepss.cfg("cmd.txt")
467
+ >>> ram.execSim(case, 10.0) # simulate until 10 seconds and pause
468
+ >>> buses = ['g1','g2','g3']
469
+ >>> ram.getBusVolt(buses)
470
+ [1.0736851673414456,
471
+ 1.0615442362180327,
472
+ 1.064702686997689]
473
+ """
474
+ volts = []
475
+ bus_volt = ctypes.c_double()
476
+ for bus in busNames:
477
+ retval = self._ramseslib.get_volt_mag(bus.encode('utf-8'), bus_volt)
478
+ if (retval != 0):
479
+ raise RAMSESError(
480
+ 'RAMSES: Function getBusVolt(%s) failed with the flag %i. Does the bus exist? Last message was: %s' % (bus, retval, self.getLastErr()))
481
+ volts.append(bus_volt.value)
482
+ return volts
483
+
484
+ def getBranchPow(self, branchName):
485
+ """Return the active and reactive powers of a list of branches
486
+
487
+ :param branchName: the names of branches
488
+ :type branchName: list of str
489
+ :returns: list of branch powers. These are active and reactive power at the origin and extremity respectively (p_orig, q_orig, p_extr, q_extr)
490
+ :rtype: list of floats
491
+
492
+ :Example:
493
+
494
+ >>> import stepss
495
+ >>> ram = stepss.sim()
496
+ >>> case = stepss.cfg("cmd.txt")
497
+ >>> ram.execSim(case, 10.0) # simulate until 10 seconds and pause
498
+ >>> branchName = ['1011-1013','1012-1014','1021-1022']
499
+ >>> ram.getBranchPow(branchName)
500
+ """
501
+ pows = []
502
+ p_orig = ctypes.c_double()
503
+ q_orig = ctypes.c_double()
504
+ p_extr = ctypes.c_double()
505
+ q_extr = ctypes.c_double()
506
+ for branch in branchName:
507
+ retval = self._ramseslib.get_line_pow(branch.encode('utf-8'), p_orig, q_orig, p_extr, q_extr)
508
+ if (retval != 0):
509
+ raise RAMSESError(
510
+ 'RAMSES: Function get_line_pow(%s) failed with the flag %i. Does the branch exist? Last message was: %s' % (branch, retval, self.getLastErr()))
511
+ thisBranch = [p_orig.value, q_orig.value, p_extr.value, q_extr.value]
512
+ pows.append(thisBranch)
513
+ return pows
514
+
515
+ def getBranchCur(self, branchName):
516
+ """Return the currents of a list of branches
517
+
518
+ :param branchName: the names of branches
519
+ :type branchName: list of str
520
+ :returns: list of branch currents. These are x-y components at the origin and extremity respectively (ix_orig, iy_orig, ix_extr, iy_extr)
521
+ :rtype: list of floats
522
+
523
+ :Example:
524
+
525
+ >>> import stepss
526
+ >>> ram = stepss.sim()
527
+ >>> case = stepss.cfg("cmd.txt")
528
+ >>> ram.execSim(case, 10.0) # simulate until 10 seconds and pause
529
+ >>> branchName = ['1011-1013','1012-1014','1021-1022']
530
+ >>> ram.getBranchCur(branchName)
531
+
532
+ """
533
+ curs = []
534
+ ix_orig = ctypes.c_double()
535
+ iy_orig = ctypes.c_double()
536
+ ix_extr = ctypes.c_double()
537
+ iy_extr = ctypes.c_double()
538
+ for branch in branchName:
539
+ retval = self._ramseslib.get_line_cur(branch.encode('utf-8'), ix_orig, iy_orig, ix_extr, iy_extr)
540
+ if (retval != 0):
541
+ raise RAMSESError(
542
+ 'RAMSES: Function get_line_cur(%s) failed with the flag %i. Does the branch exist? Last message was: %s' % (branch, retval, self.getLastErr()))
543
+ thisBranch = [ix_orig.value, iy_orig.value, ix_extr.value, iy_extr.value]
544
+ curs.append(thisBranch)
545
+ return curs
546
+
547
+ def getBusPha(self, busNames):
548
+ """Return the voltage phase of a list of buses
549
+
550
+ :param busNames: the names of buses
551
+ :type busNames: list of str
552
+ :returns: list of bus voltage phase
553
+ :rtype: list of floats
554
+
555
+ :Example:
556
+
557
+ >>> import stepss
558
+ >>> ram = stepss.sim()
559
+ >>> case = stepss.cfg("cmd.txt")
560
+ >>> ram.execSim(case, 10.0) # simulate until 10 seconds and pause
561
+ >>> buses = ['g1','g2','g3']
562
+ >>> ram.getBusPha(buses)
563
+ [0.0000000000000000,
564
+ 10.0615442362180327,
565
+ 11.064702686997689]
566
+
567
+ """
568
+ pha = []
569
+ bus_pha = ctypes.c_double()
570
+ for bus in busNames:
571
+ retval = self._ramseslib.get_volt_pha(bus.encode('utf-8'), bus_pha)
572
+ if (retval != 0):
573
+ raise RAMSESError(
574
+ 'RAMSES: Function getBusPha(%s) failed with the flag %i. Does the bus exist? Last message was: %s' % (bus, retval, self.getLastErr()))
575
+ pha.append(bus_pha.value)
576
+ return pha
577
+
578
+ def endSim(self):
579
+ """Signal RAMSES to end the simulation and block until it terminates.
580
+
581
+ Sets the internal RAMSES end-of-simulation flag and then calls
582
+ :meth:`contSim` with an infinite time horizon so that the solver
583
+ flushes its output and cleans up.
584
+
585
+ :returns: return value of the underlying :meth:`contSim` call (``0`` on success).
586
+ :rtype: int
587
+ """
588
+
589
+ self._ramseslib.set_end_simul()
590
+ return self.contSim(self.getInfTime())
591
+
592
+ def getEndSim(self):
593
+ """Check whether the simulation has ended.
594
+
595
+ :returns: ``0`` if the simulation is still running; ``1`` if it has ended.
596
+ :rtype: int
597
+ """
598
+
599
+ return self._ramseslib.get_end_simul()
600
+
601
+ def getSimTime(self):
602
+ """Return the current simulated time reported by the RAMSES solver.
603
+
604
+ Only meaningful while a simulation is paused (between :meth:`execSim`
605
+ and :meth:`contSim` calls).
606
+
607
+ :returns: current simulation time in seconds.
608
+ :rtype: float
609
+ """
610
+ return self._ramseslib.get_sim_time()
611
+
612
+ def getInfTime(self):
613
+ """Return the largest representable double recognised by the RAMSES solver.
614
+
615
+ Passing this value to :meth:`contSim` or :meth:`execSim` instructs
616
+ RAMSES to run until the end of the disturbance scenario.
617
+
618
+ :returns: maximum double value used internally by RAMSES as a sentinel
619
+ for "simulate to the end".
620
+ :rtype: float
621
+ """
622
+ return self._ramseslib.get_huge_double()
623
+
624
+ def initObserv(self, traj_filenm):
625
+ """Initialise the runtime observable recording system.
626
+
627
+ Must be called after :meth:`execSim` (with ``pause=0.0``) and before
628
+ :meth:`addObserv` / :meth:`finalObserv`. Sets the path of the output
629
+ trajectory file and prepares the internal observable structures.
630
+
631
+ :param str traj_filenm: path of the ``.trj`` file where recorded
632
+ timeseries will be written.
633
+ :returns: return value from the underlying RAMSES call (non-zero on error).
634
+
635
+ :Example:
636
+
637
+ >>> import stepss
638
+ >>> ram = stepss.sim()
639
+ >>> case = stepss.cfg("cmd.txt") # command file without any observables
640
+ >>> ram.execSim(case, 0.0) # start
641
+ >>> traj_filenm = 'obs.trj'
642
+ >>> ram.initObserv(traj_filenm)
643
+ """
644
+
645
+ return self._ramseslib.initObserv(traj_filenm.encode('utf-8'))
646
+
647
+ def addObserv(self, string):
648
+ """Register one observable element for runtime recording.
649
+
650
+ Must be called after :meth:`initObserv` and before :meth:`finalObserv`.
651
+ Accepts RAMSES observable selector strings such as ``'BUS *'`` (all
652
+ buses) or ``'SYNC g1'`` (a specific generator).
653
+
654
+ :param str string: observable selector in RAMSES format.
655
+ :returns: return value from the underlying RAMSES call (non-zero on error).
656
+
657
+ :Example:
658
+
659
+ >>> import stepss
660
+ >>> ram = stepss.sim()
661
+ >>> case = stepss.cfg("cmd.txt") # command file without any observables
662
+ >>> ram.execSim(case, 0.0) # start
663
+ >>> traj_filenm = 'obs.trj'
664
+ >>> ram.initObserv(traj_filenm)
665
+ >>> string = 'BUS *' # monitor all buses
666
+ >>> ram.addObserv(string)
667
+ """
668
+
669
+ return self._ramseslib.addObserv(string.encode('utf-8'))
670
+
671
+ def finalObserv(self):
672
+ """Finalise observable selection, allocate recording buffers, and write the trajectory file header.
673
+
674
+ Must be called after all :meth:`addObserv` calls. RAMSES will write
675
+ timeseries data to the ``.trj`` file specified in :meth:`initObserv`
676
+ as the simulation advances.
677
+
678
+ :returns: return value from the underlying RAMSES call (non-zero on error).
679
+
680
+ :Example:
681
+
682
+ >>> import stepss
683
+ >>> ram = stepss.sim()
684
+ >>> case = stepss.cfg("cmd.txt") # command file without any observables
685
+ >>> ram.execSim(case, 0.0) # start
686
+ >>> traj_filenm = 'obs.trj'
687
+ >>> ram.initObserv(traj_filenm)
688
+ >>> string = 'BUS *' # monitor all buses
689
+ >>> ram.addObserv(string)
690
+ >>> ram.finalObserv()
691
+ """
692
+
693
+ return self._ramseslib.finalObserv()
694
+
695
+ def getPrm(self, comp_type, comp_name, prm_name):
696
+ """Get the value of a named parameter
697
+
698
+ :param comp_type: the types of components (EXC, TOR, INJ, DCTL, TWOP)
699
+ :type comp_type: list of str
700
+ :param comp_name: the names of components
701
+ :type comp_name: list of str
702
+ :param prm_name: the names of parameters
703
+ :type prm_name: list of str
704
+
705
+ :returns: list of parameter values
706
+ :rtype: list of floats
707
+
708
+ :Example:
709
+
710
+ >>> import stepss
711
+ >>> ram = stepss.sim()
712
+ >>> case = stepss.cfg("cmd.txt")
713
+ >>> ram.execSim(case, 10.0) # simulate until 10 seconds and pause
714
+ >>> comp_type = ['EXC','EXC','EXC']
715
+ >>> comp_name = ['g1','g2','g3']
716
+ >>> prm_name = ['V0','V0','V0']
717
+ >>> ram.getPrm(comp_type,comp_name, prm_name)
718
+ [1.0736851673414456,
719
+ 1.0615442362180327,
720
+ 1.064702686997689]
721
+
722
+ """
723
+
724
+ comp_type = wrapToList(comp_type)
725
+ comp_name = wrapToList(comp_name)
726
+ prm_name = wrapToList(prm_name)
727
+
728
+ if not (len(comp_type) == len(comp_name) == len(prm_name)):
729
+ raise ValueError('RAMSES: Function getPrm failed because the lists are not equal!')
730
+ prm_values = []
731
+ for a, b, c in zip(comp_type, comp_name, prm_name):
732
+ prm_value = ctypes.c_double()
733
+ retval = self._ramseslib.get_named_prm(a.encode('utf-8'), b.encode('utf-8'), c.encode('utf-8'),
734
+ prm_value)
735
+ if retval != 0:
736
+ raise RAMSESError(
737
+ 'RAMSES: Function getPrm(%s,%s,%s) failed with the flag %i. Does the parameter or equipment exist? Last message was: %s'
738
+ % (a, b, c, retval, self.getLastErr()))
739
+ prm_values.append(prm_value.value)
740
+ if len(prm_values) == 1:
741
+ return prm_values[0]
742
+ else:
743
+ return prm_values
744
+
745
+ def defineSS(self, ssID, filter1, filter2, filter3):
746
+ """Define a subsytem using three filters. The resulting list is an intersection of the filters.
747
+
748
+ :param ssID: Number of the SS
749
+ :type ssID: int
750
+ :param filter1: Voltage levels to be included
751
+ :type filter1: list of str or str
752
+ :param filter2: Zones (which zone/zones will be included)
753
+ :type filter2: list of str or str
754
+ :param filter3: Bus names to be included
755
+ :type filter3: list of str or str
756
+
757
+ :Example:
758
+
759
+ >>> import stepss
760
+ >>> ram = stepss.sim()
761
+ >>> case = stepss.cfg("cmd.txt")
762
+ >>> ram.execSim(case, 0.0)
763
+ >>> ram.defineSS(1, ['735'], [], []) # SS 1 with all buses at 735 kV, no zones, no list of buses
764
+
765
+ :raises RAMSESError: if *ssID*, *location*, or *in_service* are not valid, or the RAMSES call fails.
766
+ :returns: ``None``
767
+
768
+ .. note:: An empty filter means it is deactivated and discarded.
769
+
770
+ """
771
+ filter1 = wrapToList(filter1)
772
+ filter2 = wrapToList(filter2)
773
+ filter3 = wrapToList(filter3)
774
+
775
+ strF1 = ' '.join(filter1)
776
+ strF2 = ' '.join(filter2)
777
+ strF3 = ' '.join(filter3)
778
+
779
+ retval = self._ramseslib.define_SS(ctypes.c_int(ssID), strF1.encode('utf-8'), ctypes.c_int(len(filter1)),
780
+ strF2.encode('utf-8'), ctypes.c_int(len(filter2)), strF3.encode('utf-8'),
781
+ ctypes.c_int(len(filter3)))
782
+ if retval != 0:
783
+ raise RAMSESError('RAMSES: Function define_SS(%i,...) failed with the flag %i. Last message was: %s' % (ssID, retval, self.getLastErr()))
784
+
785
+ def getSS(self, ssID):
786
+ """Retrieve the buses of a subsytem.
787
+
788
+ :param ssID: Number of the SS
789
+ :type ssID: int
790
+
791
+ :returns: list of buses
792
+ :rtype: list of str
793
+
794
+ :Example:
795
+
796
+ >>> import stepss
797
+ >>> ram = stepss.sim()
798
+ >>> case = stepss.cfg("cmd.txt")
799
+ >>> ram.execSim(case, 0.0)
800
+ >>> ram.defineSS(1, ['735'], [], []) # SS 1 with all buses at 735 kV
801
+ >>> ram.getSS(1) # get list of buses in SS 1
802
+
803
+ """
804
+
805
+ mxreclen = self._ramseslib.get_nbbus()
806
+ string_buffer = ctypes.create_string_buffer(19 * mxreclen)
807
+
808
+ retval = self._ramseslib.get_SS(ctypes.c_int(ssID), ctypes.c_int(mxreclen), string_buffer)
809
+ if retval != 0:
810
+ raise RAMSESError('RAMSES: Function getSS(%i) failed with the flag %i. Last message was: %s' % (ssID, retval, self.getLastErr()))
811
+
812
+ return string_buffer.value.split()
813
+
814
+ def getTrfoSS(self, ssID, location, in_service, rettype):
815
+ """Retrieve transformer information of subsystem after applying some filters
816
+
817
+ :param ssID: Number of the SS
818
+ :type ssID: int
819
+ :param location: 1 – both buses inside SS, 2 - tie transformers, 3 – 1 & 2
820
+ :type location: int
821
+ :param in_service: 1 – transformers in service, 2 - all transformers
822
+ :type in_service: int
823
+ :param rettype: Type of response (NAME, From, To, Status, Tap, Currentf, Currentt, Pf, Qf, Pt, Qt).
824
+ :type rettype: str
825
+
826
+ :returns: list of transformer names
827
+ :rtype: list of str
828
+
829
+ :Example:
830
+
831
+ >>> import stepss
832
+ >>> ram = stepss.sim()
833
+ >>> case = stepss.cfg("cmd.txt")
834
+ >>> ram.execSim(case, 0.0)
835
+ >>> ram.defineSS(1, ['735'], [], []) # SS 1 with all buses at 735 kV
836
+ >>> ram.getTrfoSS(1,3,2,'Status')
837
+
838
+ .. note:: Tap is not implemented yet.
839
+
840
+ .. todo:: Implement Taps after discussing with Lester.
841
+ """
842
+
843
+ if rettype not in ['NAME', 'From', 'To', 'Status', 'Tap', 'Currentf', 'Currentt', 'Pf', 'Qf', 'Pt', 'Qt']:
844
+ raise RAMSESError('RAMSES: Function getTrfoSS(%i,%i,%i,%s): rettype is not valid!' % (
845
+ ssID, location, in_service, rettype))
846
+ elif location not in [1, 2, 3]:
847
+ raise RAMSESError('RAMSES: Function getTrfoSS(%i,%i,%i,%s): location is not valid!' % (
848
+ ssID, location, in_service, rettype))
849
+ elif in_service not in [1, 2]:
850
+ raise RAMSESError('RAMSES: Function getTrfoSS(%i,%i,%i,%s): in_service is not valid!' % (
851
+ ssID, location, in_service, rettype))
852
+
853
+ mxreclen = self._ramseslib.get_nbbra()
854
+ string_buffer = ctypes.create_string_buffer(21 * mxreclen)
855
+ dp_vec = (ctypes.c_double * mxreclen)()
856
+ dp_int = (ctypes.c_int * mxreclen)()
857
+ elem = ctypes.c_int()
858
+
859
+ retval = self._ramseslib.get_transformer_in_SS(ctypes.c_int(ssID), ctypes.c_int(location),
860
+ ctypes.c_int(in_service), rettype.encode('utf-8'),
861
+ ctypes.c_int(mxreclen), string_buffer, dp_vec, dp_int, elem)
862
+ if retval != 0:
863
+ raise RAMSESError('RAMSES: Function getTrfoSS(%i,%i,%i,%s) failed with the flag %i.' % (
864
+ ssID, location, in_service, rettype, retval))
865
+
866
+ if elem == 0:
867
+ return []
868
+ elif rettype in ['Tap', 'Status']:
869
+ return [dp_int[i] for i in range(0, elem.value)]
870
+ elif rettype in ['Currentf', 'Currentt', 'Pf', 'Qf', 'Pt', 'Qt']:
871
+ return [dp_vec[i] for i in range(0, elem.value)]
872
+ else:
873
+ return string_buffer.value.split()
874
+
875
+ def getPrmNames(self, comp_type, comp_name):
876
+ """Get the named parameters of a model
877
+
878
+ :param comp_type: the types of components (EXC, TOR, INJ, DCTL, TWOP)
879
+ :type comp_type: list of str or str
880
+ :param comp_name: the names of component instances
881
+ :type comp_name: list of str or str
882
+
883
+ :returns: list of parameter names
884
+ :rtype: list of lists of strings
885
+
886
+ :Example:
887
+
888
+ >>> import stepss
889
+ >>> ram = stepss.sim()
890
+ >>> case = stepss.cfg("cmd.txt")
891
+ >>> ram.execSim(case, 0.0) # initialize and wait
892
+ >>> comp_type = ['EXC','EXC','EXC']
893
+ >>> comp_name = ['g1','g2','g3'] # name of synchronous machines
894
+ >>> ram.getPrmNames(comp_type,comp_name)
895
+
896
+ """
897
+ comp_type = wrapToList(comp_type)
898
+ comp_name = wrapToList(comp_name)
899
+
900
+ if not (len(comp_type) == len(comp_name)):
901
+ raise ValueError('RAMSES: Function getPrmNames failed because the lists are not equal!')
902
+ prm_names = []
903
+ mxprm = self._ramseslib.get_mxprm() # get maximum number of parameters in any model
904
+ for a, b in zip(comp_type, comp_name):
905
+ string_buffer = ctypes.create_string_buffer(11 * mxprm)
906
+ retval = self._ramseslib.get_comp_prm_names(a.encode('utf-8'), b.encode('utf-8'), ctypes.c_int(mxprm),
907
+ string_buffer)
908
+ if retval != 0:
909
+ raise RAMSESError(
910
+ 'RAMSES: Function getPrm(%s,%s) failed with the flag %i. Does the parameter or equipment exist?'
911
+ % (a, b, retval))
912
+ decodedstring = string_buffer.value.decode()
913
+ prm_names.append(decodedstring.split())
914
+ if len(prm_names) == 1:
915
+ return prm_names[0]
916
+ else:
917
+ return prm_names
918
+
919
+ def getObs(self, comp_type, comp_name, obs_name):
920
+ """Get the value of a named observable.
921
+
922
+ :param comp_type: the types of components ('EXC','TOR','INJ','TWOP','DCTL','SYN')
923
+ :type comp_type: list of str
924
+ :param comp_name: the names of components
925
+ :type comp_name: list of str
926
+ :param obs_name: the names of observables
927
+ :type obs_name: list of str
928
+
929
+ :returns: list of observable values
930
+ :rtype: list of floats
931
+
932
+ .. note::
933
+
934
+ For the synchronous generator ('SYN') the accepted obs_name values are
935
+ - 'P': Active power (MW)
936
+ - 'Q': Reactive power (MVAr)
937
+ - ``'Omega'``: Machine speed (pu)
938
+ - 'S': Apparent power (MVA)
939
+ - 'SNOM': Nominal apparent power (MVA)
940
+ - 'PNOM': Nominal active power (MW)
941
+
942
+ :Example:
943
+
944
+ >>> import stepss
945
+ >>> ram = stepss.sim()
946
+ >>> case = stepss.cfg("cmd.txt")
947
+ >>> ram.execSim(case, 10.0) # simulate until 10 seconds and pause
948
+ >>> comp_type = ['INJ','EXC','TOR']
949
+ >>> comp_name = ['L_11','g2','g3']
950
+ >>> obs_name = ['P','vf','Pm']
951
+ >>> ram.getObs(comp_type,comp_name, obs_name)
952
+ [199.73704732259995,
953
+ 1.3372945282585218,
954
+ 0.816671075203357]
955
+
956
+ """
957
+ comp_type = wrapToList(comp_type)
958
+ comp_name = wrapToList(comp_name)
959
+ obs_name = wrapToList(obs_name)
960
+
961
+ if not (len(comp_type) == len(comp_name) == len(obs_name)):
962
+ raise ValueError('RAMSES: Function getObs failed because the lists are not equal!')
963
+ obs_values = []
964
+ for a, b, c in zip(comp_type, comp_name, obs_name):
965
+ obs_value = ctypes.c_double()
966
+ retval = self._ramseslib.get_named_obs(a.encode('utf-8'), b.encode('utf-8'), c.encode('utf-8'),
967
+ obs_value)
968
+ if retval != 0:
969
+ raise RAMSESError(
970
+ 'RAMSES: Function getObs(%s,%s,%s) failed with the flag %i. Does the observable or equipment exist?'
971
+ % (a, b, c, retval))
972
+ obs_values.append(obs_value.value)
973
+ return obs_values
974
+
975
+ def pauseSim(self, t_pause):
976
+ """Schedule a pause at *t_pause* seconds of simulated time.
977
+
978
+ The pause is registered in the solver's internal state and takes effect
979
+ during the next :meth:`execSim` or :meth:`contSim` call.
980
+
981
+ :param float t_pause: simulated time (seconds) at which to pause.
982
+ :returns: return value from the underlying RAMSES call (non-zero on error).
983
+ :rtype: int
984
+
985
+ :Example:
986
+
987
+ >>> import stepss
988
+ >>> ram = stepss.sim()
989
+ >>> case = stepss.cfg("cmd.txt")
990
+ >>> ram.pauseSim(10.0)
991
+ >>> ram.execSim(case) # will pause at t=10 s
992
+ """
993
+ return self._ramseslib.set_pause_time(t_pause)
994
+
995
+ def addDisturb(self, t_dist, disturb):
996
+ """Add a new disturbance at a specific time. Follows the same structure as the disturbances in the dst files.
997
+
998
+ :param t_dist: time of the disturbance
999
+ :type t_dist: float
1000
+ :param disturb: description of disturbance
1001
+ :type disturb: str
1002
+
1003
+ :Example:
1004
+
1005
+ >>> import stepss
1006
+ >>> ram = stepss.sim()
1007
+ >>> case = stepss.cfg("cmd.txt")
1008
+ >>> ram.execSim(case, 80.0) # simulate until 80 seconds and pause
1009
+ >>> ram.addDisturb(100.000, 'CHGPRM DCTL 1-1041 Vsetpt -0.05 0') # Decrease the setpoint of the DCTL by 0.015 pu, at t=100 s
1010
+ >>> ram.addDisturb(100.000, 'CHGPRM DCTL 2-1042 Vsetpt -0.05 0')
1011
+ >>> ram.addDisturb(100.000, 'CHGPRM DCTL 3-1043 Vsetpt -0.05 0')
1012
+ >>> ram.addDisturb(100.000, 'CHGPRM DCTL 4-1044 Vsetpt -0.05 0')
1013
+ >>> ram.addDisturb(100.000, 'CHGPRM DCTL 5-1045 Vsetpt -0.05 0')
1014
+ >>> ram.contSim(ram.getInfTime()) # continue the simulation
1015
+ """
1016
+ return self._ramseslib.add_disturb(t_dist, disturb.encode('utf-8'))
1017
+
1018
+ def load_MDL(self, MDLName):
1019
+ """Load an external shared library containing user-defined RAMSES models.
1020
+
1021
+ The library must expose the standard RAMSES user-model interface. Once
1022
+ loaded, the models it defines become available to the solver for the
1023
+ lifetime of this :class:`sim` instance.
1024
+
1025
+ :param str MDLName: path to the model library (``*.dll`` on Windows,
1026
+ ``*.so`` on Linux). Use the current directory or
1027
+ an absolute path.
1028
+ :returns: return value from the underlying RAMSES call (non-zero on error).
1029
+
1030
+ :Example:
1031
+
1032
+ >>> import stepss
1033
+ >>> stepss.sim.load_MDL("MDLs.dll")
1034
+ >>> ram = stepss.sim()
1035
+ >>> case = stepss.cfg("cmd.txt")
1036
+ >>> ram.execSim(case)
1037
+ """
1038
+ return self._ramseslib.c_load_MDL(MDLName.encode('utf-8'))
1039
+
1040
+ def unload_MDL(self, MDLName):
1041
+ """Unload a previously loaded user-model shared library.
1042
+
1043
+ :param str MDLName: path to the model library that was passed to
1044
+ :meth:`load_MDL`.
1045
+ :returns: return value from the underlying RAMSES call (non-zero on error).
1046
+
1047
+ :Example:
1048
+
1049
+ >>> import stepss
1050
+ >>> stepss.sim.load_MDL("MDLs.dll")
1051
+ >>> ram = stepss.sim()
1052
+ >>> case = stepss.cfg("cmd.txt")
1053
+ >>> ram.execSim(case)
1054
+ >>> stepss.sim.unload_MDL("MDLs.dll")
1055
+ """
1056
+ return self._ramseslib.c_unload_MDL(MDLName.encode('utf-8'))
1057
+
1058
+
1059
+ def get_MDL_no(self):
1060
+ """Return the number of user-model libraries currently loaded.
1061
+
1062
+ :returns: count of loaded user-model libraries.
1063
+ :rtype: int
1064
+
1065
+ :Example:
1066
+
1067
+ >>> import stepss
1068
+ >>> stepss.sim.load_MDL("MDLs.dll")
1069
+ >>> stepss.sim.get_MDL_no()
1070
+ 1
1071
+ """
1072
+ return self._ramseslib.c_get_MDL_no()