Skip to content

Commit d57b67e

Browse files
committed
Plot: add some missing plot types (almost done)
1 parent 376fa80 commit d57b67e

File tree

1 file changed

+217
-28
lines changed

1 file changed

+217
-28
lines changed

dss/plot.py

Lines changed: 217 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
"""
22
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.
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
"""
78
from . import api_util
89
from . import DSS as DSSPrime
910
from ._cffi_api_util import CffiApiUtil
1011
from .IDSS import IDSS
1112
from .IBus import IBus
13+
from typing import List
1214
import os
1315
try:
1416
import numpy as np
@@ -36,7 +38,11 @@
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)}"):&nbsp;'))
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)}"):&nbsp;'))
4046

4147
def show(text):
4248
display(text)
@@ -94,7 +100,7 @@ def show(text):
94100
DSS_MARKER_22 = Path([(-0.23, 0.147), (0.0, -0.13), (0.23, 0.147)], [1, 2, 2])
95101
DSS_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

169175
sizes = np.array([0, 9, 6, 4], dtype=float) * 0.7
170176

177+
MARKER_SEQ = (5, 15, 2, 8, 26, 36, 39, 19, 18)
178+
171179
def 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):
187195
def 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

267288
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
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-
11031121
def 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+
13001475
dss_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

13151492
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+
13171507

13181508

13191509
def ctx2dss(ctx, instances={}):
@@ -1408,23 +1598,24 @@ def dss_python_cb_write(ctx, message_str, message_type):
14081598
@api_util.ffi.def_extern()
14091599
def 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

14251616
def 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

Comments
 (0)