Getting Started#

This document provides an overview of some of the AltDSS-Python’s features, especially for new users. It is exposed as the altdss package.

Notebook requirements

This is a Jupyter notebook. Running it as-is will try to download the required files.

You can open and then run this notebook on Google Colab for a quick overview if you don’t want to set up a local environment: Open in Colab.

# When running via Colab, install the package first
import os, subprocess
if os.getenv("COLAB_RELEASE_TAG"):
    print(subprocess.check_output('pip install altdss[all]', shell=True).decode())
# Download the sample circuits and test cases if they do not exist already
from dss.examples import download_repo_snapshot
BASE_PATH = download_repo_snapshot('.', repo_name='electricdss-tst', use_version=False)
IEEE13_PATH = BASE_PATH / 'Version8/Distrib/IEEETestCases/13Bus/IEEE13Nodeckt.dss'
assert IEEE13_PATH.exists()

IEEE8500_PATH = BASE_PATH / 'Version8/Distrib/IEEETestCases/8500-Node/Master.dss'
assert IEEE8500_PATH.exists()

If you are reading this notebook online and would like to run in your local Python installation, you can download both the AltDSS-Python repository (which contains this notebook) and the sample circuits with download_examples.

On Windows, run on a command prompt in your Python environment to install all optional dependencies:

pip install altdss[all]
python -c "from dss.examples import download_examples; download_examples(r'c:\temp', repo_name='AltDSS-Python')"
cd c:\temp\AltDSS-Python\docs\examples
jupyter lab

Basics#

AltDSS-Python is an effort in the DSS-Extensions project. As such, it doesn’t require EPRI’s OpenDSS to be installed. It provides its own customized engine, which in turn enables us to run the DSS engine on Windows, Linux and macOS (including newer Apple ARM processors, a.k.a. “Apple Silicon”).

To use it, after installation, open a Python interpreter and type the following command:

from altdss import altdss

This imports the default instance, which is bound to the default DSS engine. It collapses the ActiveCircuit into the main class, so it’s closer to OpenDSSDirect.py in this aspect.

Which DSS-Python tries to be very compatible with the official OpenDSS COM object organization of classes and properties, and OpenDSSDirect.py prefers using functions/methods for property getters/setters, AltDSS-Python uses a different approach.

Like the modern versions of our other Python packages, you can pass multiple commands in a big string (without negative performance impacts):

altdss(f'''
    Clear
    Redirect "{IEEE13_PATH}"
''')

Some classes, properties and methods are still present. They may be adjusted for future versions, if required:

# This was AllBusVmag. The "All" was kinda redundant, capitalization was adjusted.
altdss.BusVMag()
array([66393.52539552, 66394.8697918 , 66391.9679481 ,  2401.56281971,
        2401.70707917,  2401.61163242,  2536.35618639,  2491.56916753,
        2536.39597012,  2428.91682366,  2466.67017218,  2405.47534155,
         273.56904918,   279.46191055,   272.263543  ,  2360.45472436,
        2498.51583289,  2317.41727729,  2449.16595034,  2407.23753262,
        2445.04461469,  2402.35182009,  2317.4172594 ,  2360.45470186,
        2498.51583016,  2344.78388971,  2504.17602597,  2312.77119003,
        2307.69876166,  2342.53046877,  2411.44105223,  2478.32688291,
        2377.14162241,  2436.22416628,  2471.29925961,  2411.83511918,
        2360.45475448,  2498.51586854,  2317.41731041,  2355.83530743,
        2312.54108638])
from dss import SolveModes
altdss.Solution.Mode = SolveModes.SnapShot
altdss.Solution.Solve()

If you need DSS-Python or OpenDSSDirect.py to complement something:

dss = altdss.to_dss_python()
print(dss.Version)
DSS C-API Library version 0.14.4 revision 65fe268a473b5361d56f090be64e1dd623acfd51 based on OpenDSS SVN 3723 [FPC 3.2.2] (64-bit build) MVMULT INCREMENTAL_Y CONTEXT_API PM 20240319022204; License Status: Open 
DSS-Python version: 0.15.6
odd = altdss.to_opendssdirect()
print(odd.Version())
DSS C-API Library version 0.14.4 revision 65fe268a473b5361d56f090be64e1dd623acfd51 based on OpenDSS SVN 3723 [FPC 3.2.2] (64-bit build) MVMULT INCREMENTAL_Y CONTEXT_API PM 20240319022204; License Status: Open 
DSS-Python version: 0.15.6
OpenDSSDirect.py version: 0.9.3
altdss.TotalPower()
(-3567.022201208932-1736.4085202212455j)

Multiple instances are also supported, so you can use multithreading (see ODD.py or DSS-Python’s examples):

altdss.Settings.AllowChangeDir = False
altdss2 = altdss.NewContext()

altdss2(f'Redirect "{IEEE13_PATH}"')

altdss2.Solution.LoadMult = 1.2
altdss2.Solution.Solve()

altdss.TotalPower(), altdss2.TotalPower()
((-3567.022201208932-1736.4085202212455j),
 (-4306.902075243575-2318.447275626252j))

Buses in Python#

Buses are now dedicated objects, including batches.

len(altdss.Bus)
16
# NumNodes for all buses
altdss.Bus.NumNodes()
array([3, 3, 3, 3, 3, 3, 2, 2, 3, 3, 1, 1, 3, 3, 3, 2], dtype=int32)
bus0 = altdss.Bus[0]
bus0.ComplexSeqVoltages
array([-3.63797881e-12+4.31951048e-06j,  5.75034626e+04+3.31879898e+04j,
       -7.75926487e-01+1.48598276e+00j])
bus0.NumNodes
3
bus0.Name
'sourcebus'
bus0.kVBase
66.39528095680697
import json
json.loads(bus0.to_json())
{'Name': 'sourcebus', 'X': 200.0, 'Y': 400.0, 'kVLN': 66.39528095680697}

DSS Objects in Python#

For example, let’s get the list of DSS classes from the classic API.

dss = altdss.to_dss_python()
for cls_name in dss.Classes:
    cls = getattr(altdss, cls_name)
    print(cls_name, len(cls))
LineCode 36
LoadShape 1
TShape 0
PriceShape 0
XYcurve 0
GrowthShape 1
TCC_Curve 10
Spectrum 7
WireData 0
CNData 0
TSData 0
LineSpacing 0
LineGeometry 0
XfmrCode 0
Line 12
Vsource 1
Isource 0
VCCS 0
Load 15
Transformer 5
RegControl 3
Capacitor 2
Reactor 0
CapControl 0
Fault 0
DynamicExp 0
Generator 0
GenDispatcher 0
Storage 0
StorageController 0
Relay 0
Recloser 0
Fuse 0
SwtControl 0
PVSystem 0
UPFC 0
UPFCControl 0
ESPVLControl 0
IndMach012 0
GICsource 0
AutoTrans 0
InvControl 0
ExpControl 0
GICLine 0
GICTransformer 0
VSConverter 0
Monitor 0
EnergyMeter 0
Sensor 0

Let’s try of the classes. Load, for example:

altdss.Load
<altdss.Load.ILoad at 0x7f543ceb74d0>
len(altdss.Load)
15
# this is an intrinsic property, so it's a plain Python property
altdss.Load.Name
['671',
 '634a',
 '634b',
 '634c',
 '645',
 '646',
 '692',
 '675a',
 '675b',
 '675c',
 '611',
 '652',
 '670a',
 '670b',
 '670c']
# this is a derived property, so by convention it's a function
altdss.Load.FullName()
['Load.671',
 'Load.634a',
 'Load.634b',
 'Load.634c',
 'Load.645',
 'Load.646',
 'Load.692',
 'Load.675a',
 'Load.675b',
 'Load.675c',
 'Load.611',
 'Load.652',
 'Load.670a',
 'Load.670b',
 'Load.670c']
altdss.Load.to_list() # it's a list of Python objects
[<Load.671>,
 <Load.634a>,
 <Load.634b>,
 <Load.634c>,
 <Load.645>,
 <Load.646>,
 <Load.692>,
 <Load.675a>,
 <Load.675b>,
 <Load.675c>,
 <Load.611>,
 <Load.652>,
 <Load.670a>,
 <Load.670b>,
 <Load.670c>]
for l in altdss.Load:
    print(l.Name, l.kW, l.kvar)
671 1155.0 660.0
634a 160.0 110.0
634b 120.0 90.0
634c 120.0 90.0
645 170.0 125.0
646 230.0 132.0
692 170.0 151.0
675a 485.0 190.0
675b 68.0 60.0
675c 290.0 212.0
611 170.0 80.0
652 128.0 86.0
670a 17.0 10.0
670b 66.0 38.0
670c 117.0 68.0
l = altdss.Load[0]
l
<Load.671>
l.to_json()
'{"Name":"671","Bus1":"671.1.2.3","Phases":3,"Conn":"delta","Model":1,"kV":4.1600000000000001E+000,"kW":1.1550000000000000E+003,"kvar":6.6000000000000000E+002}'
l.YPrim()
array([ 0.04449427-0.0254253j , -0.02224713+0.01271265j,
       -0.02224713+0.01271265j, -0.02224713+0.01271265j,
        0.04449427-0.0254253j , -0.02224713+0.01271265j,
       -0.02224713+0.01271265j, -0.02224713+0.01271265j,
        0.04449427-0.0254253j ])

Most of the API functions are encapsulated on each object. There isn’t a separate ActiveCktElement or CktElement anymore:

l.Voltages()
array([ 2350.08179214 -221.06105284j, -1338.40993775-2109.78765406j,
       -1015.44476495+2083.1416145j ])

You can enable AdvancedTypes like in DSS-Python to enable matrices. But all complex data are represented as complex numbers, always.

altdss.Settings.AdvancedTypes = True
l.Voltages()
array([[ 2350.08179214 -221.06105284j],
       [-1338.40993775-2109.78765406j],
       [-1015.44476495+2083.1416145j ]])
l.IsIsolated()
False

Most DSS properties that refer to DSS objects return the object directly:

l.Spectrum
<Spectrum.defaultload>
l.Spectrum.Harmonic
array([ 1.,  3.,  5.,  7.,  9., 11., 13.])
l.Spectrum.pctMag
array([100. ,   1.5,  20. ,  14. ,   1. ,   9. ,   7. ])

Sometimes you need just the name, so separate properties or functions, with the suffix _str also exist:

l.Spectrum_str
'defaultload'

Batches#

altdss.Load (and all other DSS classes represented there) is the container for all loads. At the same time, it’s a batch container!

altdss.Load.kW
<altdss.ArrayProxy.BatchFloat64ArrayProxy at 0x7f543cb284a0>
altdss.Load.kW.to_array()
array([1155.,  160.,  120.,  120.,  170.,  230.,  170.,  485.,   68.,
        290.,  170.,  128.,   17.,   66.,  117.])

Some operations are pushed down to the engine. For example, this doesn’t run a loop in Python:

altdss.Load.kW *= 1.1

Let’s load a larger circuit:

altdss(f'redirect "{IEEE8500_PATH}"')
altdss.Name, altdss.NumNodes
('ieee8500', 8531)

and multiple all kWs by 1.0 just to show some timings (and not change the values):

%%timeit
altdss.Load.kW *= 1.0
97.2 µs ± 4.03 µs per loop (mean ± std. dev. of 7 runs, 10,000 loops each)

And now doing this via the classic API (via DSS-Python here):

%%timeit 
for l in dss.ActiveCircuit.Loads: 
    l.kW *= 1.0
546 µs ± 1.65 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)

We can filter the objects based on integer and boolean properties, besides regular expressions.

loads_2ph = altdss.Load.batch(Phases=2)
assert all(l.Phases == 2 for l in loads_2ph)
sum(loads_2ph.kW)
10773.169999999998
l2010x = altdss.Load.batch(re='^2010.*$')
l2010x.to_list()
[<Load.20107636a0>, <Load.20107693b0>]

Let’s set the loadshape to these 2-phase loads:

loads_2ph.Daily = 'default'
loads_2ph[0].Daily
<LoadShape.default>

A lot of enum classes were introduced to complement the basic enums used in our implementation of the classic API. For example, any DSS property that is represented as an enum has a dedicated type:

line = altdss.Line[0]
line.Units
<LengthUnit.none: 0>
from altdss.enums import LengthUnit

LengthUnit._value2member_map_
{0: <LengthUnit.none: 0>,
 1: <LengthUnit.mi: 1>,
 2: <LengthUnit.kft: 2>,
 3: <LengthUnit.km: 3>,
 4: <LengthUnit.m: 4>,
 5: <LengthUnit.ft: 5>,
 6: <LengthUnit.inch: 6>,
 7: <LengthUnit.cm: 7>,
 8: <LengthUnit.mm: 8>}

Creating objects#

We can also create a new loadshape and assign the object directly, either to objects or batches:

loadshape = altdss.LoadShape.new('sample_shape')
loadshape.NPts = 5
loadshape.PMult = [1, 2, 3, 4, 5]
loadshape.end_edit()
l2010x.Daily = loadshape
l2010x.Daily_str # checking
['sample_shape', 'sample_shape']

We’ll probalby tweak this end_edit() idiom in later versions.

We are also considering making NPts and most of the other size properties not required when creating/manipulating data from Python (i.e. this doesn’t affect DSS scripts), as it will follow the formats derived from the upcoming AltDSS Schema.

The new methods also accept the DSS properties as keyword arguments. Check the tests folder in the repository for more examples.

Automatic invalidation#

The bus0 we got before was for another circuit. If we try to use it, it is marked with InvalidatedDSSBus.

bus0.Name
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[50], line 1
----> 1 bus0.Name

File /opt/python/lib/python3.12/site-packages/altdss/Bus.py:168, in Bus.Name(self)
    165 @property
    166 def Name(self) -> str:
    167     '''Name of this bus'''
--> 168     return self._get_string(self._lib.Alt_Bus_Get_Name(self._ptr))

File /opt/python/lib/python3.12/site-packages/dss/_cffi_api_util.py:82, in CtxLib._error_checked(self, _errorPtr, f, *args)
     81 def _error_checked(self, _errorPtr, f, *args):
---> 82     result = f(*args)
     83     if _errorPtr[0] and Base._use_exceptions:
     84         error_num = _errorPtr[0]

TypeError: initializer for ctype 'void *' must be a cdata pointer, not InvalidatedDSSBus

Similarly, the load l is marked InvalidatedDSSObject:

l.kW
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[51], line 1
----> 1 l.kW

File /opt/python/lib/python3.12/site-packages/altdss/Load.py:170, in Load._get_kW(self)
    169 def _get_kW(self) -> float:
--> 170     return self._lib.Obj_GetFloat64(self._ptr, 4)

TypeError: initializer for ctype 'void *' must be a cdata pointer, not InvalidatedDSSObject

TO BE CONTINUED!#

We can also:

  • create objects and batches of new objects. See the tests on the repository for some samples.

  • control some side-effects when updating properties