1
1
"""
2
2
This module provides a **work-in-progress** implementation of the original OpenDSS plots
3
- using the new features from DSS C-API v0.12 and common Python modules such as matplotlib.
3
+ using the new features from DSS C-API v0.12+ and common Python modules such as matplotlib.
4
4
5
- This is not a complete implementation yet and there are known limitations
5
+ This is not a complete implementation and there are known limitations, but should suffice
6
+ for many use-cases. We'd like to add another backend later.
6
7
"""
7
8
from . import api_util
8
9
from . import DSS as DSSPrime
9
10
from ._cffi_api_util import CffiApiUtil
10
11
from .IDSS import IDSS
11
12
from .IBus import IBus
13
+ from typing import List
12
14
import os
13
15
try :
14
16
import numpy as np
36
38
37
39
def link_file (fn ):
38
40
relfn = os .path .relpath (fn , os .getcwd ())
39
- display (FileLink (relfn , result_html_prefix = f'<b>File output</b> ("{ html .escape (fn )} "): ' ))
41
+ if relfn .startswith ('..' ):
42
+ # cannot show in the notebook :(
43
+ display (HTML (f'<p><b>File output</b> ("{ html .escape (relfn )} ") outside current workspace.<p>' ))
44
+ else :
45
+ display (FileLink (relfn , result_html_prefix = f'<b>File output</b> ("{ html .escape (fn )} "): ' ))
40
46
41
47
def show (text ):
42
48
display (text )
@@ -94,7 +100,7 @@ def show(text):
94
100
DSS_MARKER_22 = Path ([(- 0.23 , 0.147 ), (0.0 , - 0.13 ), (0.23 , 0.147 )], [1 , 2 , 2 ])
95
101
DSS_MARKER_23 = Path ([(- 0.28 , 0.147 ), (0.0 , - 0.13 ), (0.28 , 0.147 )], [1 , 2 , 2 ])
96
102
97
- marker_map = {
103
+ MARKER_MAP = {
98
104
# marker, size multipler (1=normal, 2=small, 3=tiny), fill
99
105
0 : (',' , 1 , 1 ),
100
106
1 : ('+' , 3 , 1 ),
@@ -168,8 +174,10 @@ def show(text):
168
174
169
175
sizes = np .array ([0 , 9 , 6 , 4 ], dtype = float ) * 0.7
170
176
177
+ MARKER_SEQ = (5 , 15 , 2 , 8 , 26 , 36 , 39 , 19 , 18 )
178
+
171
179
def get_marker_dict (dss_code ):
172
- marker , size , fill = marker_map [dss_code ]
180
+ marker , size , fill = MARKER_MAP [dss_code ]
173
181
res = dict (
174
182
marker = marker ,
175
183
markersize = sizes [size ],
@@ -187,35 +195,48 @@ def get_marker_dict(dss_code):
187
195
def nodot (b ):
188
196
return b .split ('.' , 1 )[0 ]
189
197
190
- def dss_monitor_plot (DSS , params ):
198
+ def dss_monitor_plot (DSS : IDSS , params ):
191
199
monitor = DSS .ActiveCircuit .Monitors
192
200
monitor .Name = params ['ObjectName' ]
193
201
data = monitor .AsMatrix ()
202
+ channels = [x + 1 for x in params ['Channels' ]]
203
+ if min (channels ) <= 1 or max (channels ) >= monitor .NumChannels :
204
+ raise IndexError ("Invalid channel number" )
194
205
206
+ bases = params ['Bases' ]
195
207
header = monitor .Header
196
- if header [0 ].strip ().lower () == 'freq' :
208
+ if len (monitor .dblHour ) < len (monitor .dblFreq ):
209
+ header .insert (0 , 'Frequency' )
210
+ header .insert (1 , 'Harmonic' )
197
211
xlabel = 'Frequency (Hz)'
198
212
h = data [:, 0 ]
199
213
else :
200
- xlabel = 'Time (s)'
214
+ header .insert (0 , 'Hour' )
215
+ header .insert (1 , 'Seconds' )
201
216
h = data [:, 0 ] * 3600 + data [:, 1 ]
202
-
217
+ total_seconds = max (h ) - min (h )
218
+ if total_seconds < 7200 :
219
+ xlabel = 'Time (s)'
220
+ else :
221
+ xlabel = 'Time (h)'
222
+ h /= 3600
223
+
203
224
separate = False
204
225
if separate :
205
- fig , axs = plt .subplots (len (params [ 'Channels' ] ), sharex = True , figsize = (8 , 9 ))
226
+ fig , axs = plt .subplots (len (channels ), sharex = True , figsize = (8 , 9 ))
206
227
icolor = - 1
207
- for ax , base , ch in zip (axs , params [ 'Bases' ], params [ 'Channels' ] ):
228
+ for ax , base , ch in zip (axs , bases , channels ):
208
229
icolor += 1
209
- ax .plot (h , data [:, ch + 1 ] / base , color = Colors [icolor % len (Colors )])
230
+ ax .plot (h , data [:, ch ] / base , color = Colors [icolor % len (Colors )])
210
231
ax .grid ()
211
- ax .set_ylabel (header [ch - 1 ])
212
-
232
+ ax .set_ylabel (header [ch ])
233
+
213
234
else :
214
235
fig , ax = plt .subplots (1 )
215
236
icolor = - 1
216
- for base , ch in zip (params [ 'Bases' ], params [ 'Channels' ] ):
237
+ for base , ch in zip (bases , channels ):
217
238
icolor += 1
218
- ax .plot (h , data [:, ch + 1 ] / base , label = header [ch - 1 ], color = Colors [icolor % len (Colors )])
239
+ ax .plot (h , data [:, ch ] / base , label = header [ch ], color = Colors [icolor % len (Colors )])
219
240
220
241
ax .grid ()
221
242
ax .legend ()
@@ -265,7 +286,7 @@ def dss_tshape_plot(DSS, params):
265
286
266
287
267
288
def dss_priceshape_plot (DSS , params ):
268
- # There is no dedicated API yet
289
+ # There is no dedicated API yet but we can move to the Obj API
269
290
name = params ['ObjectName' ]
270
291
DSS .Text .Command = f'? priceshape.{ name } .price'
271
292
p = np .fromstring (DSS .Text .Result [1 :- 1 ].strip (), dtype = float , sep = ' ' )
@@ -885,7 +906,7 @@ def dss_circuit_plot(DSS, params={}, fig=None, ax=None, is3d=False):
885
906
886
907
points = get_point_data (DSS , objs , bus_coords )
887
908
888
- # if marker_code not in marker_map :
909
+ # if marker_code not in MARKER_MAP :
889
910
#marker_code = 25
890
911
891
912
marker_dict = get_marker_dict (marker_code )
@@ -1097,9 +1118,6 @@ def get_text():
1097
1118
ax .set_ylim (- 15 , y + 5 )
1098
1119
1099
1120
1100
- def dss_yearly_curve_plot (DSS , params ):
1101
- print ("TODO: YearCurveplot" )#, params)
1102
-
1103
1121
def dss_general_data_plot (DSS , params ):
1104
1122
is_general = params ['PlotType' ] == 'GeneralData'
1105
1123
ValueIndex = max (1 , params ['ValueIndex' ] - 1 )
@@ -1297,6 +1315,163 @@ def dss_daisy_plot(DSS, params):
1297
1315
ax .text (bus .x , bus .y , bus .Name , zorder = 11 )
1298
1316
1299
1317
1318
+ def unquote (field : str ):
1319
+ field = field .strip ()
1320
+ if field [0 ] == '"' and field [- 1 ] == '"' :
1321
+ return field [1 :- 1 ]
1322
+
1323
+ return field
1324
+
1325
+
1326
+ def dss_di_plot (DSS : IDSS , params ):
1327
+ caseYear , caseName , meterName = params ['CaseYear' ], params ['CaseName' ], params ['MeterName' ]
1328
+ plotRegisters , peakDay = params ['Registers' ], params ['PeakDay' ]
1329
+
1330
+ fn = os .path .join (DSS .DataPath , caseName , f'DI_yr_{ caseYear } ' , meterName + '.csv' )
1331
+
1332
+ if len (plotRegisters ) == 0 :
1333
+ raise RuntimeError ("No register indices were provided for DI_Plot" )
1334
+
1335
+ if not os .path .exists (fn ):
1336
+ fn = fn [:- 4 ] + '_1.csv'
1337
+
1338
+ # Whenever we add Pandas as a dependency, this could be
1339
+ # rewritten to avoid all the extra/slow work
1340
+ selected_data = []
1341
+ day_data = []
1342
+ mult = 1 if peakDay else 0.001
1343
+
1344
+ # If the file doesn't exist, let the exception raise
1345
+ with open (fn , 'r' ) as f :
1346
+ header = f .readline ().rstrip ()
1347
+ allRegisterNames = [unquote (field ) for field in header .strip ().strip (' \t ,' ).split (',' )]
1348
+ registerNames = [allRegisterNames [i ] for i in plotRegisters ]
1349
+
1350
+ if not len (registerNames ):
1351
+ raise RuntimeError ("Could not find any register name in the file" )
1352
+
1353
+ for line in f :
1354
+ if not line :
1355
+ continue
1356
+
1357
+ rawValues = line .split (',' )
1358
+ selValues = [float (rawValues [0 ]), * (float (rawValues [i ]) for i in plotRegisters )]
1359
+ if not peakDay :
1360
+ selected_data .append (selValues )
1361
+ else :
1362
+ day_data .append (selValues )
1363
+ if len (day_data ) == 24 :
1364
+ max_vals = [max (x ) for x in zip (* day_data )]
1365
+ max_vals [0 ] = day_data [0 ][0 ]
1366
+ day_data = []
1367
+ selected_data .append (max_vals )
1368
+
1369
+ if day_data :
1370
+ max_vals = [max (x ) for x in zip (* day_data )]
1371
+ max_vals [0 ] = day_data [0 ][0 ]
1372
+ day_data = []
1373
+ selected_data .append (max_vals )
1374
+
1375
+ vals = np .asarray (selected_data , dtype = float )
1376
+ fig , ax = plt .subplots (1 )
1377
+ icolor = - 1
1378
+ for idx , name in enumerate (registerNames , start = 1 ):
1379
+ icolor += 1
1380
+ ax .plot (vals [:, 0 ], vals [:, idx ] * mult , label = name , color = Colors [icolor % len (Colors )])
1381
+
1382
+ ax .set_title (f'{ caseName } , Yr={ caseYear } ' )
1383
+ ax .set_xlabel ('Hour' )
1384
+ ax .set_ylabel ('MW, MWh or MVA' )
1385
+ ax .legend ()
1386
+ ax .grid ()
1387
+
1388
+
1389
+ def _plot_yearly_case (DSS : IDSS , caseName : str , meterName : str , plotRegisters : List [int ], icolor : int , ax , registerNames : List [str ]):
1390
+ anyData = True
1391
+ xvalues = []
1392
+ all_yvalues = [[] for _ in plotRegisters ]
1393
+ for caseYear in range (0 , 21 ):
1394
+ fn = os .path .join (DSS .DataPath , caseName , f'DI_yr_{ caseYear } ' , 'Totals_1.csv' )
1395
+ if not os .path .exists (fn ):
1396
+ continue
1397
+
1398
+ with open (fn , 'r' ) as f :
1399
+ f .readline () # Skip the header
1400
+ # Get started - initialize Registers 1
1401
+ registerVals = [float (x ) * 0.001 for x in f .readline ().split (',' )]
1402
+ if len (registerVals ):
1403
+ xvalues .append (registerVals [7 ])
1404
+
1405
+ if len (xvalues ) == 0 :
1406
+ raise RuntimeError ('No data to plot' )
1407
+
1408
+ for caseYear in range (0 , 21 ):
1409
+ if meterName .lower () in ('totals' , 'systemmeter' , 'totals_1' , 'systemmeter_1' ):
1410
+ suffix = '' if meterName .endswith ('_1' ) else '_1'
1411
+ meterName = meterName .lower ().replace ('totals' , 'Totals' ).replace ('systemmeter' , 'SystemMeter' )
1412
+ fn = os .path .join (DSS .DataPath , caseName , f'DI_yr_{ caseYear } ' , f'{ meterName } { suffix } .csv' )
1413
+ searchForMeterLine = False
1414
+ else :
1415
+ fn = os .path .join (DSS .DataPath , caseName , f'DI_yr_{ caseYear } ' , 'EnergyMeterTotals_1.csv' )
1416
+ searchForMeterLine = True
1417
+
1418
+ if not os .path .exists (fn ):
1419
+ continue
1420
+
1421
+ with open (fn , 'r' ) as f :
1422
+ header = f .readline ()
1423
+ if len (registerNames ) == 0 :
1424
+ allRegisterNames = [unquote (field ) for field in header .strip (' \t ,' ).split (',' )]
1425
+ registerNames .extend (allRegisterNames [i ] for i in plotRegisters )
1426
+
1427
+ if not searchForMeterLine :
1428
+ line = f .readline ()
1429
+ else :
1430
+ for line in f :
1431
+ label , rest = line .split (',' , 1 )
1432
+ if label .strip ().lower () == meterName .lower ():
1433
+ line = f'{ caseYear } ,{ rest } '
1434
+ else :
1435
+ raise RuntimeError ("Meter not found" )
1436
+
1437
+ registerVals = [float (x ) * 0.001 for x in line .strip (' \t ,' ).split (',' )]
1438
+ if len (registerVals ):
1439
+ for yvalues , idx in zip (all_yvalues , plotRegisters ):
1440
+ yvalues .append (registerVals [idx ])
1441
+
1442
+ for yvalues , idx , regName in zip (all_yvalues , plotRegisters , registerNames ):
1443
+ marker_code = MARKER_SEQ [icolor % len (MARKER_SEQ )]
1444
+ ax .plot (xvalues , yvalues , label = f'{ caseName } :{ meterName } :{ regName } ' , color = Colors [icolor % len (Colors )], ** get_marker_dict (marker_code ))
1445
+ icolor += 1
1446
+
1447
+ return icolor
1448
+
1449
+
1450
+ def dss_yearly_curve_plot (DSS : IDSS , params ):
1451
+ caseNames , meterName , plotRegisters = params ['CaseNames' ], params ['MeterName' ], params ['Registers' ]
1452
+
1453
+ fig , ax = plt .subplots (1 )
1454
+ icolor = 0
1455
+ registerNames = []
1456
+ for caseName in caseNames :
1457
+ icolor = _plot_yearly_case (DSS , caseName , meterName , plotRegisters , icolor , ax , registerNames )
1458
+
1459
+ if icolor == 0 :
1460
+ plt .close (fig )
1461
+ raise RuntimeError ('No files found' )
1462
+
1463
+ fig .suptitle (f"Yearly Curves for case(s): { ', ' .join (caseNames )} " )
1464
+ ax .set_title (f"Meter: { meterName } ; Registers: { ', ' .join (registerNames )} " , fontsize = 'small' )
1465
+ ax .set_xlabel ('Total Area MW' )
1466
+ ax .set_ylabel ('MW, MWh or MVA' )
1467
+ ax .legend ()
1468
+ ax .grid ()
1469
+
1470
+
1471
+ def dss_comparecases_plot (DSS : IDSS , params ):
1472
+ print ('TODO: dss_comparecases_plot' , params )
1473
+
1474
+
1300
1475
dss_plot_funcs = {
1301
1476
'Scatter' : dss_scatter_plot ,
1302
1477
'Daisy' : dss_daisy_plot ,
@@ -1309,11 +1484,26 @@ def dss_daisy_plot(DSS, params):
1309
1484
'Visualize' : dss_visualize_plot ,
1310
1485
'YearlyCurve' : dss_yearly_curve_plot ,
1311
1486
'Matrix' : dss_matrix_plot ,
1312
- 'GeneralData' : dss_general_data_plot
1487
+ 'GeneralData' : dss_general_data_plot ,
1488
+ 'DI' : dss_di_plot ,
1489
+ 'CompareCases' : dss_comparecases_plot ,
1313
1490
}
1314
1491
1315
1492
def dss_plot (DSS , params ):
1316
- dss_plot_funcs .get (params ['PlotType' ])(DSS , params )
1493
+ try :
1494
+ ptype = params ['PlotType' ]
1495
+ if ptype not in dss_plot_funcs :
1496
+ print ('ERROR: not implemented plot type:' , ptype )
1497
+ return - 1
1498
+
1499
+ dss_plot_funcs .get (ptype )(DSS , params )
1500
+ except Exception as ex :
1501
+ DSS ._errorPtr [0 ] = 777
1502
+ DSS ._lib .Error_Set_Description (f"Error in the plot backend: { ex } " .encode ())
1503
+ return 777
1504
+
1505
+ return 0
1506
+
1317
1507
1318
1508
1319
1509
def ctx2dss (ctx , instances = {}):
@@ -1408,23 +1598,24 @@ def dss_python_cb_write(ctx, message_str, message_type):
1408
1598
@api_util .ffi .def_extern ()
1409
1599
def dss_python_cb_plot (ctx , paramsStr ):
1410
1600
params = json .loads (api_util .ffi .string (paramsStr ))
1601
+ result = 0
1411
1602
try :
1412
1603
DSS = ctx2dss (ctx )
1413
- dss_plot (DSS , params )
1604
+ result = dss_plot (DSS , params )
1414
1605
if _do_show :
1415
1606
plt .show ()
1416
1607
except :
1417
1608
from traceback import print_exc
1418
1609
print ('DSS: Error while plotting. Parameters:' , params , file = sys .stderr )
1419
1610
print_exc ()
1420
- return 0
1611
+ return 0 if result is None else result
1421
1612
1422
1613
_original_allow_forms = None
1423
1614
_do_show = True
1424
1615
1425
1616
def enable (plot3d : bool = False , plot2d : bool = True , show : bool = True ):
1426
1617
"""
1427
- Enables the experimental plotting subsystem from DSS Extensions.
1618
+ Enables the plotting subsystem from DSS Extensions.
1428
1619
1429
1620
Set plot3d to `True` to try to reproduce some of the plots from the
1430
1621
alternative OpenDSS Visualization Tool / OpenDSS Viewer addition
@@ -1442,8 +1633,6 @@ def enable(plot3d: bool = False, plot2d: bool = True, show: bool = True):
1442
1633
1443
1634
_do_show = show
1444
1635
1445
- warnings .warn ('This is still an initial, work-in-progress implementation of plotting for DSS Extensions' )
1446
-
1447
1636
if plot3d and plot2d :
1448
1637
include_3d = 'both'
1449
1638
elif plot3d and not plot2d :
0 commit comments