11"""
22This 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.
44
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.
67"""
78from . import api_util
89from . import DSS as DSSPrime
910from ._cffi_api_util import CffiApiUtil
1011from .IDSS import IDSS
1112from .IBus import IBus
13+ from typing import List
1214import os
1315try :
1416 import numpy as np
3638
3739 def link_file (fn ):
3840 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 )} "): ' ))
4046
4147 def show (text ):
4248 display (text )
@@ -94,7 +100,7 @@ def show(text):
94100DSS_MARKER_22 = Path ([(- 0.23 , 0.147 ), (0.0 , - 0.13 ), (0.23 , 0.147 )], [1 , 2 , 2 ])
95101DSS_MARKER_23 = Path ([(- 0.28 , 0.147 ), (0.0 , - 0.13 ), (0.28 , 0.147 )], [1 , 2 , 2 ])
96102
97- marker_map = {
103+ MARKER_MAP = {
98104 # marker, size multipler (1=normal, 2=small, 3=tiny), fill
99105 0 : (',' , 1 , 1 ),
100106 1 : ('+' , 3 , 1 ),
@@ -168,8 +174,10 @@ def show(text):
168174
169175sizes = np .array ([0 , 9 , 6 , 4 ], dtype = float ) * 0.7
170176
177+ MARKER_SEQ = (5 , 15 , 2 , 8 , 26 , 36 , 39 , 19 , 18 )
178+
171179def get_marker_dict (dss_code ):
172- marker , size , fill = marker_map [dss_code ]
180+ marker , size , fill = MARKER_MAP [dss_code ]
173181 res = dict (
174182 marker = marker ,
175183 markersize = sizes [size ],
@@ -187,35 +195,48 @@ def get_marker_dict(dss_code):
187195def nodot (b ):
188196 return b .split ('.' , 1 )[0 ]
189197
190- def dss_monitor_plot (DSS , params ):
198+ def dss_monitor_plot (DSS : IDSS , params ):
191199 monitor = DSS .ActiveCircuit .Monitors
192200 monitor .Name = params ['ObjectName' ]
193201 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" )
194205
206+ bases = params ['Bases' ]
195207 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' )
197211 xlabel = 'Frequency (Hz)'
198212 h = data [:, 0 ]
199213 else :
200- xlabel = 'Time (s)'
214+ header .insert (0 , 'Hour' )
215+ header .insert (1 , 'Seconds' )
201216 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+
203224 separate = False
204225 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 ))
206227 icolor = - 1
207- for ax , base , ch in zip (axs , params [ 'Bases' ], params [ 'Channels' ] ):
228+ for ax , base , ch in zip (axs , bases , channels ):
208229 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 )])
210231 ax .grid ()
211- ax .set_ylabel (header [ch - 1 ])
212-
232+ ax .set_ylabel (header [ch ])
233+
213234 else :
214235 fig , ax = plt .subplots (1 )
215236 icolor = - 1
216- for base , ch in zip (params [ 'Bases' ], params [ 'Channels' ] ):
237+ for base , ch in zip (bases , channels ):
217238 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 )])
219240
220241 ax .grid ()
221242 ax .legend ()
@@ -265,7 +286,7 @@ def dss_tshape_plot(DSS, params):
265286
266287
267288def 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
269290 name = params ['ObjectName' ]
270291 DSS .Text .Command = f'? priceshape.{ name } .price'
271292 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):
885906
886907 points = get_point_data (DSS , objs , bus_coords )
887908
888- # if marker_code not in marker_map :
909+ # if marker_code not in MARKER_MAP :
889910 #marker_code = 25
890911
891912 marker_dict = get_marker_dict (marker_code )
@@ -1097,9 +1118,6 @@ def get_text():
10971118 ax .set_ylim (- 15 , y + 5 )
10981119
10991120
1100- def dss_yearly_curve_plot (DSS , params ):
1101- print ("TODO: YearCurveplot" )#, params)
1102-
11031121def dss_general_data_plot (DSS , params ):
11041122 is_general = params ['PlotType' ] == 'GeneralData'
11051123 ValueIndex = max (1 , params ['ValueIndex' ] - 1 )
@@ -1297,6 +1315,163 @@ def dss_daisy_plot(DSS, params):
12971315 ax .text (bus .x , bus .y , bus .Name , zorder = 11 )
12981316
12991317
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+
13001475dss_plot_funcs = {
13011476 'Scatter' : dss_scatter_plot ,
13021477 'Daisy' : dss_daisy_plot ,
@@ -1309,11 +1484,26 @@ def dss_daisy_plot(DSS, params):
13091484 'Visualize' : dss_visualize_plot ,
13101485 'YearlyCurve' : dss_yearly_curve_plot ,
13111486 '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 ,
13131490}
13141491
13151492def 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+
13171507
13181508
13191509def ctx2dss (ctx , instances = {}):
@@ -1408,23 +1598,24 @@ def dss_python_cb_write(ctx, message_str, message_type):
14081598@api_util .ffi .def_extern ()
14091599def dss_python_cb_plot (ctx , paramsStr ):
14101600 params = json .loads (api_util .ffi .string (paramsStr ))
1601+ result = 0
14111602 try :
14121603 DSS = ctx2dss (ctx )
1413- dss_plot (DSS , params )
1604+ result = dss_plot (DSS , params )
14141605 if _do_show :
14151606 plt .show ()
14161607 except :
14171608 from traceback import print_exc
14181609 print ('DSS: Error while plotting. Parameters:' , params , file = sys .stderr )
14191610 print_exc ()
1420- return 0
1611+ return 0 if result is None else result
14211612
14221613_original_allow_forms = None
14231614_do_show = True
14241615
14251616def enable (plot3d : bool = False , plot2d : bool = True , show : bool = True ):
14261617 """
1427- Enables the experimental plotting subsystem from DSS Extensions.
1618+ Enables the plotting subsystem from DSS Extensions.
14281619
14291620 Set plot3d to `True` to try to reproduce some of the plots from the
14301621 alternative OpenDSS Visualization Tool / OpenDSS Viewer addition
@@ -1442,8 +1633,6 @@ def enable(plot3d: bool = False, plot2d: bool = True, show: bool = True):
14421633
14431634 _do_show = show
14441635
1445- warnings .warn ('This is still an initial, work-in-progress implementation of plotting for DSS Extensions' )
1446-
14471636 if plot3d and plot2d :
14481637 include_3d = 'both'
14491638 elif plot3d and not plot2d :
0 commit comments