Getting Started#

This document provides an overview of some of DSS-Python’s features, especially for new users.

There are other documents for specific features, and a lot of other features still need documentation for new users.

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 dss-python[all]', shell=True).decode())
# Download the sample circuits and test cases if they do not exist already
from dss.examples import download_repo_snapshot
IEEE13_PATH = download_repo_snapshot('.', repo_name='electricdss-tst', use_version=False) / 'Version8/Distrib/IEEETestCases/13Bus/IEEE13Nodeckt.dss'
assert IEEE13_PATH.exists()

If you are reading this notebook online and would like to run in your local Python installation, you can download both the DSS-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 dss-python[all]
python -c "from dss.examples import download_examples; download_examples(r'c:\temp')"
cd c:\temp\dss_python\docs\examples
jupyter lab

Introduction#

DSS-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”).

For a comparison of the general Python-level API, including a list of our extra functions, please check DSS-Extensions — OpenDSS: Overview of Python APIs. That documents introduces and compares DSS-Python, OpenDSSDirect.py, and the official COM implementation.

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

from dss import dss

The package exposes the lower level API functions from AltDSS/DSS C-API mimicking the organization and behavior of the official COM implementation, as used in Python. This allows an easier migration, or even toggling which interface is used if the user avoids using API Extensions (which are marked as such in the documentation).

To keep the code compatible with the COM implementation, users can feed commands through the text interface:

dss.Text.Command = f'Redirect "{IEEE13_PATH}"'

Alternatively, if there is no need to maintain this compatibility, there is a shortcut function that allows both single commands and multiple commands (passed through multiline strings):

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

After a DSS circuit is loaded, the interaction can be done as with the official COM module:

dssCircuit = dss.ActiveCircuit
dssCircuit.AllBusNames
['sourcebus',
 '650',
 'rg60',
 '633',
 '634',
 '671',
 '645',
 '646',
 '692',
 '675',
 '611',
 '652',
 '670',
 '632',
 '680',
 '684']
dssLoads = dssCircuit.Loads
idx = dssLoads.First

while idx:
    print(dssLoads.Name)
    idx = dssLoads.Next

print(f'We have {dssLoads.Count} loads in this circuit')
671
634a
634b
634c
645
646
692
675a
675b
675c
611
652
670a
670b
670c
We have 15 loads in this circuit

You can also use more Pythonic iteration. General OpenDSS caveats related to the classic OpenDSS API, which only allows one active object for each class, still apply.

for load in dssLoads:
    print(load.Name)

print(f'We have {len(dssLoads)} loads in this circuit')
671
634a
634b
634c
645
646
692
675a
675b
675c
611
652
670a
670b
670c
We have 15 loads in this circuit

Use the enums#

Instead of using magic numbers like in dss.ActiveCircuit.Solution.Mode = 1, use the enums. Import the whole enums module, or separate enums you use. You can import the enums from the dss for short:

from dss.enums import SolveModes
dss.ActiveCircuit.Solution.Mode = SolveModes.Daily

from dss import SolveModes
dss.ActiveCircuit.Solution.Mode = SolveModes.Daily

Using magic numbers is bad for multiple reasons:

  • Harder to read for other users. Each user has to each memorize every value or constantly check the reference.

  • The values can actually change throughout the releases. It already happened in the past in OpenDSS and some bugs persistent for about 15 years!

  • Using the provided enum classes ensure, in most cases, that you are passing a valid value. Currently, DSS-Python or most Python APIs for OpenDSS do not enforce or check the values, so using the correct enum can reduce the chance of accidents.

See the list of enumerations, including important comments, in dss.enums page.

Migrating from the COM implementation#

A lot of users start using OpenDSS using the COM implementation, which is the official OpenDSS version, limited to Windows as of February 2024.

That is fine and could even be the recommended path. Later, users can either migrate or toggle which version is used. Using DSS-Python should make that easier, since it is API-compatible with the COM implementation (but does not use COM at all). Users could include code that use OpenDSSDirect.py or AltDSS-Python to complement aspect that the COM-like approach is not sufficient.

For example, suppose you had the following code running in the official OpenDSS via COM, using win32com or comtypes (Windows only, etc.):

# Load OpenDSS
import win32com.client
dss = win32com.client.Dispatch('OpenDSSengine.DSS')

# ...or for win32com with ensured early bindings:
# import win32com.client
# dss = win32com.client.gencache.EnsureDispatch('OpenDSSengine.DSS')

# ...or:
# import comtypes.client
# dss = comtypes.client.CreateObject('OpenDSSengine.DSS')

# Run a DSS script to load a circuit
# (NOTE: typically you would use either the full path here since the official OpenDSS implementation changes the current working directory of the process)
dss.Text.Command = f'Redirect "{IEEE13_PATH}"'

# Select a load and update its kW
dss.ActiveCircuit.Loads.Name = "675c"
dss.ActiveCircuit.Loads.kW = 320

# Solve
dss.ActiveCircuit.Solution.Solve()

# Get the voltage magnitudes
voltages = dss.ActiveCircuit.AllBusVmag

You could use DSS-Python to keep the code with a few changes.

# Load DSS-Python
from dss import dss

# Run a DSS script to load a circuit
# (DSS-Extensions do not change the CWD when importing, relative paths are fine)
dss.Text.Command = f'Redirect "{IEEE13_PATH}"'

# Select a load and update its kW
dss.ActiveCircuit.Loads.Name = "675c"
dss.ActiveCircuit.Loads.kW = 320

# Solve
dss.ActiveCircuit.Solution.Solve()

# Get the voltage magnitudes
voltages = dss.ActiveCircuit.AllBusVmag

Only the first lines of the original code were changed.

Capitalization#

Sometimes, users are not aware of early bindings and the attribute lookup on win32com becomes case-insensitive.

To make it easier to migrate in those situations, DSS-Python includes a special setup that makes if case-insensitive, with the extra option to allow it to warn when the name capitalization does not match the expected, set_case_insensitive_attributes.

Since set_case_insensitive_attributes has a small performance overhead, it is important to not overuse it. The warnings allow the user to track and fix capitalization issues, and finally remove the use of set_case_insensitive_attributes when no more warnings remain.

from dss import set_case_insensitive_attributes

set_case_insensitive_attributes(use=True, warn=True)

print(dss.activecircuit.Loads.kW) # this produces a warning
print(dss.ActiveCircuit.Loads.kvar) # this one works fine since the capitalization is correct
320.0
233.9310344827586
/tmp/ipykernel_17371/1763084407.py:5: UserWarning: Wrong capitalization for attribute (getter) IDSS.ActiveCircuit: activecircuit
  print(dss.activecircuit.Loads.kW) # this produces a warning

Bringing some Python back to the COM version#

Sometimes keeping compatibility with both implementations (DSS-Extensions and EPRI) is useful or even required. Users can be spoiled by some simple quality of life improvements from DSS-Python, like the iterators.

To make things easier, DSS-Python provides a function, patch_dss_com, to patch the COM classes with some extras. This function does not change any aspect of the official OpenDSS engine, it just provides some Python functionality. It will, of course, require DSS-Python to be installed, but this will use the COM DLL and can be a quick way to try a script in both versions:

import comtypes, dss
dss_com = dss.patch_dss_com(comtypes.client.CreateObject("OpenDSSEngine.DSS"))
print(dss_com.Version)

# ...compile a circuit, etc.

for l in dss_com.ActiveCircuit.Loads:
    print(l.Name, l.kva)

for b in dss_com.ActiveCircuit.ActiveBus:
    print(b.Name, b.x, b.y)

This works with both comtypes and win32com.

There is a related effort to provide a lower level implementation to be shared across all DSS-Extensions. In the mean time, patch_dss_com can help.

Better numeric types#

For backwards compatibility, including with the COM implementation, DSS-Python uses simple numeric types and arrays for results, float, int and 1d arrays.

There is an toggle for AdvancedTypes that enable complex numbers and matrices:

# Setting numpy to avoid wrapping the text output
import numpy as np
np.set_printoptions(linewidth=200)
dss.AdvancedTypes = True

dss.ActiveCircuit.AllBusVolts
array([ 5.75026851e+04+3.31894826e+04j, -1.12251392e+01-6.63947381e+04j, -5.74914599e+04+3.32052555e+04j,  2.40155775e+03-4.63741199e-01j, -1.20124259e+03-2.07971493e+03j,
       -1.20029666e+03+2.08013428e+03j,  2.53635090e+03-5.75741250e-01j, -1.24626498e+03-2.15748503e+03j, -1.26756693e+03+2.19691872e+03j,  2.42899248e+03-1.11597640e+02j,
       -1.29771549e+03-2.09792327e+03j, -1.11401469e+03+2.12446915e+03j,  2.73423163e+02-1.58340595e+01j, -1.48961657e+02-2.36476486e+02j, -1.23992541e+02+2.41513099e+02j,
        2.35517413e+03-2.24335375e+02j, -1.33376180e+03-2.11303980e+03j, -1.00266766e+03+2.07501550e+03j, -1.29338670e+03-2.08000147e+03j, -1.11600118e+03+2.12545127e+03j,
       -1.29393835e+03-2.07479731e+03j, -1.11540831e+03+2.12024529e+03j, -1.00266765e+03+2.07501548e+03j,  2.35517411e+03-2.24335367e+02j, -1.33376180e+03-2.11303979e+03j,
        2.33849627e+03-2.33459387e+02j, -1.34340724e+03-2.11419663e+03j, -1.00080096e+03+2.06908907e+03j, -9.89420786e+02+2.07055770e+03j,  2.33748463e+03-2.20547176e+02j,
        2.41046051e+03-1.47553611e+02j, -1.30921513e+03-2.10455052e+03j, -1.07447308e+03+2.11064692e+03j,  2.43640430e+03-1.09156610e+02j, -1.29845599e+03-2.10290634e+03j,
       -1.11716339e+03+2.13002444e+03j,  2.35517416e+03-2.24335392e+02j, -1.33376182e+03-2.11303983e+03j, -1.00266766e+03+2.07501554e+03j,  2.35046981e+03-2.24862636e+02j,
       -9.96848971e+02+2.07238469e+03j])
dss.ActiveCircuit.Lines.idx = 6
dss.ActiveCircuit.ActiveCktElement.Yprim
array([[ 4.30504102-4.00525331j, -1.44464011+0.59960561j, -4.30504102+4.00525336j,  1.44464011-0.59960562j],
       [-1.44464011+0.59960561j,  4.33496284-3.98696602j,  1.44464011-0.59960562j, -4.33496284+3.98696607j],
       [-4.30504102+4.00525336j,  1.44464011-0.59960562j,  4.30504102-4.00525331j, -1.44464011+0.59960561j],
       [ 1.44464011-0.59960562j, -4.33496284+3.98696607j, -1.44464011+0.59960561j,  4.33496284-3.98696602j]])

There is a lot of code that doesn’t expect this modern output, so it can be toggled as required:

dss.AdvancedTypes = False
dss.ActiveCircuit.ActiveCktElement.Yprim
array([ 4.30504102, -4.00525331, -1.44464011,  0.59960561, -4.30504102,  4.00525336,  1.44464011, -0.59960562, -1.44464011,  0.59960561,  4.33496284, -3.98696602,  1.44464011, -0.59960562,
       -4.33496284,  3.98696607, -4.30504102,  4.00525336,  1.44464011, -0.59960562,  4.30504102, -4.00525331, -1.44464011,  0.59960561,  1.44464011, -0.59960562, -4.33496284,  3.98696607,
       -1.44464011,  0.59960561,  4.33496284, -3.98696602])

Multiple engines in the same process#

For a full example, visit Multiple DSS engines, multithreading vs. multiprocessing.

A short example without multithreading:

from dss import dss
import numpy as np
dss.AllowChangeDir = False

NUM_ENGINES = 5
load_mults = np.linspace(0.8, 1.2, NUM_ENGINES)
engines = [dss.NewContext() for _ in load_mults]

for load_mult, engine in zip(load_mults, engines):
    engine.Text.Command = f'Redirect "{IEEE13_PATH}"'
    engine.ActiveCircuit.Solution.LoadMult = load_mult
    engine.ActiveCircuit.Solution.Solve()

for engine in engines:
    lm = engine.ActiveCircuit.Solution.LoadMult
    losses = engine.ActiveCircuit.Losses[0]
    print(f'{lm:5.3} {losses:10.1f}')
  0.8    67850.4
  0.9    88479.8
  1.0   112391.7
  1.1   138239.7
  1.2   166892.0

Plotting and notebook integration#

For more examples on how to use the plot commands from OpenDSS, including an extensive gallery, see Integrated plotting in Python.

A short example:

from dss import dss, plot
plot.enable()

dss.Text.Command = f'Redirect "{IEEE13_PATH}"'
dss.Text.Command = 'plot circuit'
../_images/7bfd8f1442f231cafc86125bba71529cbb47c29e1a8cada8627ee04d66cdf8a8.png

This enables the cell magic function and other enhancements. There is still a lot of room for improvement here.

%%dss
visualize voltages element=transformer.sub 
../_images/237cc9ce3d5892ad75c7aae4e74512ada5a29904a3e85baaeca18f809fcb1e0f.png

JSON exports and imports#

Another on-going effort is support for JSON export and import: https://dss-extensions.org/dss_python/examples/JSON.html

Some highlights:

import json
from dss import dss
import numpy as np

dss(f'Redirect "{IEEE13_PATH}"')
dss.ActiveCircuit.Loads.First;
json.loads(dss.ActiveCircuit.ActiveDSSElement.ToJSON())
{'Name': '671',
 'Bus1': '671.1.2.3',
 'Phases': 3,
 'Conn': 'delta',
 'Model': 1,
 'kV': 4.16,
 'kW': 1155.0,
 'kvar': 660.0}
import pandas as pd
dss.ActiveCircuit.SetActiveClass('load')
df = pd.read_json(dss.ActiveCircuit.ActiveClass.ToJSON(), dtype_backend='pyarrow')
df.dtypes
/tmp/ipykernel_17371/3042529327.py:3: FutureWarning: Passing literal json to 'read_json' is deprecated and will be removed in a future version. To read from a literal string, wrap it in a 'StringIO' object.
  df = pd.read_json(dss.ActiveCircuit.ActiveClass.ToJSON(), dtype_backend='pyarrow')
Name      string[pyarrow]
Bus1      string[pyarrow]
Phases     int64[pyarrow]
Conn      string[pyarrow]
Model      int64[pyarrow]
kV        double[pyarrow]
kW        double[pyarrow]
kvar      double[pyarrow]
dtype: object
df
Name Bus1 Phases Conn Model kV kW kvar
0 671 671.1.2.3 3 delta 1 4.16 1155.0 660.0
1 634a 634.1 1 wye 1 0.277 160.0 110.0
2 634b 634.2 1 wye 1 0.277 120.0 90.0
3 634c 634.3 1 wye 1 0.277 120.0 90.0
4 645 645.2 1 wye 1 2.4 170.0 125.0
5 646 646.2.3 1 delta 2 4.16 230.0 132.0
6 692 692.3.1 1 delta 5 4.16 170.0 151.0
7 675a 675.1 1 wye 1 2.4 485.0 190.0
8 675b 675.2 1 wye 1 2.4 68.0 60.0
9 675c 675.3 1 wye 1 2.4 290.0 212.0
10 611 611.3 1 wye 5 2.4 170.0 80.0
11 652 652.1 1 wye 2 2.4 128.0 86.0
12 670a 670.1 1 wye 1 2.4 17.0 10.0
13 670b 670.2 1 wye 1 2.4 66.0 38.0
14 670c 670.3 1 wye 1 2.4 117.0 68.0

Integration with OpenDSSDirect.py and AltDSS-Python#

Assuming they are installed, there are two handy functions to map the DSS context to the other packages:

from dss import dss

dss(f'Redirect "{IEEE13_PATH}"')
odd = dss.to_opendssdirect()
print(odd.Version())
print()
alt = dss.to_altdss()
print(alt.Version())
DSS C-API Library version 0.14.3 revision eb03a63a86e287bc71312d7e50c30288ae946142 based on OpenDSS SVN 3723 [FPC 3.2.2] (64-bit build) MVMULT INCREMENTAL_Y CONTEXT_API PM 20240313054323; License Status: Open 
DSS-Python version: 0.15.4
OpenDSSDirect.py version: 0.9.1

DSS C-API Library version 0.14.3 revision eb03a63a86e287bc71312d7e50c30288ae946142 based on OpenDSS SVN 3723 [FPC 3.2.2] (64-bit build) MVMULT INCREMENTAL_Y CONTEXT_API PM 20240313054323; License Status: Open 
AltDSS-Python version: 0.2.2
load = alt.Load[0]
load, load.Powers(), load.to_json()
(<Load.671>,
 array([385.35511519+207.35015137j, 396.039193  +240.19800777j, 373.61959009+212.49007854j]),
 '{"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}')