Python to OpenDSS Interface
Python to OpenDSS Interface for Modeling Control Systems
The detailed syntax for interfacing with the Open-Source Distribution System Simulator (OpenDSS) is here presented in Python scripting language, emphasizing Distribution Management System (DMS) control applications. These tools are designed to form the building blocks of a sandbox module, to facilitate
simulating custom control methods.
Set up the OpenDSS Interface for Simulation
The Python module win32com.client provides access to the OpenDSS COM module.
import win32com.client
dssObj = win32com.client.Dispatch(“OpenDSSEngine.DSS”)
The COM module has a variety of interfaces, and creating variables to access them directly is handy.
dssText = dssObj.Text
dssCircuit = dssObj.ActiveCircuit
dssSolution = dssCircuit.Solution
dssElem = dssCircuit.ActiveCktElement
dssBus = dssCircuit.ActiveBus
To load a circuit file into OpenDSS, use the text interface. This interface can also be used to execute any ordinary OpenDSS command via the COM interface.
dssText.Command = ”compile ‘testCircuit.dss’”
Get and Set the States of Controlled Elements
Capacitors
A capacitor’s full name will be “capacitor.capacitorname1”. To access a capacitor, send the second part of its name to the capacitor interface to make it the active capacitor. Then any action taken in the capacitor interface will refer to that capacitor. These commands access the current state of a capacitor:
dssCircuit.Capacitors.Name = “capacitorname1”
currentState = dssCircuit.Capacitors.States[0]
The zero index is here used because the states function returns an array of capacitor states, for each step. If steps are not used, the returned value will be a tuple, or array, of length 1. Note that this array is not always accurate for versions of OpenDSS before v7.6.4.53.
To set the state of another capacitor to 1, use the following lines of code:
dssCircuit.Capacitors.Name = “capacitorname2”
dssCircuit.Capacitors.States = (1,)
The states variable must take a tuple and not just an integer.
Regulators
Controlling a regulator really means controlling a transformer tap. The transformer will be named in the format “transformer.transformername1”. To access this transformer, use the transformer interface to make it active by passing the second part of the full name. Then any action taken to the transformer interface will affect that particular transformer, including changing the regulator tap.
Some arithmetic is required, to get and set the tap as an integer in the range [-16, 16]. The following code finds the current tap value and then sets the tap to 6, using the common 0.00625 tap width.
dssCircuit.Transformers.Name = “transformername1”
dssCircuit.Transformers.wdg = 2
currentTap = float(dssCircuit.Transformers.Tap - 1) / 0.00625
newTap = 6
dssCircuit.Transformers.Tap = 1 + newTap * 0.00625
A regulator can be controlled less directly by just controlling the forward vreg setting on an ordinary regulator controller and allowing the regcontrol element to do the control work itself. This can be done using the regcontrol interface, as shown by the following code. Normally the vreg setting is given on a 120 V base.
dssCircuit.RegControls.Name = “regcontrol3c”
dssCircuit.RegControls.ForwardVreg = 124
Switches
To use a line as an open switch, change “bus2” to some new bus name. To close it again, restore the old bus name.
switchName = “Line.L234”
dssCircuit.Lines.Name = switchName.split(".")[1]
# Open the switch
oldBusName = dssCircuit.Lines.Bus2
dssCircuit.Lines.Bus2 = "__opened__" + oldBusName
# Close the switch
oldBusName = dssCircuit.Lines.Bus2
dssCircuit.Lines.Bus2 = \
oldBusName[oldBusName.index("__opened__") + 10:]
In the previous example, __opened__ is prepended to the bus name and removed when the switch is reclosed.
Loads
The loads interface works identically to that of the capacitors, transformers, and switches. Set the active element with the Name field, and change kW, kVAr, and pf as required. The following section of code shows doubling the kVAr of a given load.
loadName = “Load.Load1”
dssCircuit.Loads.Name = loadName.split(“.”)[1]
oldkvar = dssCircuit.Loads.kvar
dssCircuit.Loads.kvar = 2 * oldkvar
Generators
Generators also have a COM interface. For variables for this and other elements that are not accessible via the COM interface, see “Elements without a COM interface” below. Again, set the active element with the Name field, and change kW, kVAr, and pf as required. The following code shows increasing the generation by 5 kW.
genName = “Generator.gen1”
dssCircuit.Generators.Name = genName.split(“.”)[1]
oldkw = dssCircuit.Generators.kw
dssCircuit.Generators.kw = oldkw + 5
Elements without a COM interface
Any property from any element can be accessed using the properties interface. For example, to change the model of a generator, execute the following code.
genName = “Generator.gen1”
dssCircuit.setActiveElement(genName)
dssElem.Properties(“model”).Val = 3
Note that PV systems and energy storage systems do not yet have a COM interface, but their properties can be changed in this way.
PV systems
Active and reactive power are the primary control variables for PV systems. Active power is set as the property pctPmpp, which caps the kW output as a percentage of rated. Reactive power can be changed by setting the kvar value, for constant var mode, or by setting the PF property, for constant power factor mode. Since this element does not have a COM interface, set these properties as described in “Elements without a COM interface” above.
pvName = “PVsystem.pv1”
dssCircuit.setActiveElement(pvName)
previousQ = dssElem.Properties(“kvar”).Val
dssElem.Properties(“kvar”).Val = 25
PV systems can be controlled by InvControl elements to perform some basic control functions. These must be disabled for manual control, as described below in “Existing control elements,” or they may have their properties adjusted similarly to PV systems for a higher-level control paradigm.
Energy storage systems
The state of the storage element, which may be charging, idling, or discharging, is the key controlled variable. Other important variables are charging and discharging rates and reactive power control. Another method is to specify the kW flow directly, which may be positive, negative, or zero, to indicate discharging, charging, or idling, respectively. To monitor in these low level ways, it is essential to disable any associated energy storage controllers, as described in “Existing control elements” below, and set the dispatch mode of the storage element to external. Following is example code which sets dispatch mode to external, specifies the state to discharge and discharge rate to 81%, and reads the percent of energy which is presently stored.
storageName = “storage.st1”
dssCircuit.setActiveElement(storageName)
dssElem.Properties(“DispMode”).Val = “EXTERNAL”
dssElem.Properties(“state”).Val = “DISCHARGING”
dssElem.Properties(“%Discharge”).Val = 0.81
energyStored = dssElem.Properties(“%stored”).Val
Other options for energy storage elements involve editing the parameters of built-in dispatch modes or those of an associated storage controller element. However, if these modes are active, they will override any changes to the low-level parameters such as kW and pf.
Even if an energy storage controller is disabled, it may have already changed some variables in the storage element. To avoid this, just remove the storage controller from the original OpenDSS file or manually reset the parameters that have been changed.
For reactive power control, as of OpenDSS 7.6.4.44, the kvar setting for storage elements does not work as consistently as the power factor setting.
Existing control elements
A variety of control elements exist built-in to OpenDSS. So long as they remain enabled, they will continue to execute their control functions, overriding any manual changes. Disable them by changing the “enabled” property just like any other. This example shows disabling a regulator controller element, so that the user can control the transformer tap without interference.
dssCircuit.setActiveElement(“regcontrol.regctr1”)
dssElem.Properties(“enabled”).Val = False
It is also possible to disable all control elements by turning control mode of the simulator off.
dssSolution.ControlMode = -1
Measure and Monitor the System State
Control decisions will often be based on measured voltage, current, and active and reactive power at various places in the system.
Monitor Elements
Creating a monitor element is an excellent way to keep a record of important values throughout a time-based simulation, and save these to a file. See the OpenDSS manual for more information about setting these up in the OpenDSS script.
The following Python code is used to save all monitors and export monitorname1 to a csv file:
mName = “monitorname1”
dssCircuit.Monitors.SaveAll()
dssText.Command = "export monitors " + mName
However, it is not possible to read the values of a monitor in the middle of a simulation; use another method below for control decisions.
Bus Voltages
To measure a bus voltage for control purposes, first make it the active bus using the circuit interface. Then use the ActiveBus interface to retrieve the phase or sequence voltage magnitudes and phases.
dssCircuit.SetActiveBus(“sub_lsb”)
puList = dssBus.puVmagAngle
Following is a table of bus voltage commands and what exactly is returned:
Command |
Returned |
dssBus.Voltages |
Real and imaginary voltages per phase |
dssBus.puVoltages |
Real and imaginary voltages per phase, per-unitized |
dssBus.VLL |
Real and imaginary line-to-line voltages |
dssBus.puVLL |
Real and imaginary line-to-line voltages, per-unitized |
dssBus.VMagAngle |
Magnitude and angle of voltages per phase |
dssBus.puVmagAngle |
Magnitude and angle of voltages per phase, per-unitized |
dssBus.SeqVoltages |
Sequence voltages |
dssBus.CplxSeqVoltages |
Real and imaginary sequence voltages |
Element Currents, Powers, and Voltages
To measure currents and powers for control purposes, use the circuit interface to make a given power delivery or power conversion element the active circuit element. Then use one or more commands on the ActiveElement interface to measure the appropriate quantities.
dssCircuit.SetActiveElement(“Line.L3”)
cplxPowers = dssElem.Powers
seqCurrents = dssElem.SeqCurrents
The following table shows many available commands to measure different current and power values. Some of the above voltage commands also apply to the terminals of a circuit element.
Command |
Returned |
dssElem.Currents |
Real and imaginary currents into each element terminal |
dssElem.CurrentsMagAng |
Magnitude and angle of currents into each element terminal |
dssElem.SeqCurrents |
Sequence currents into each 3-phase terminal |
dssElem.CplxSeqCurrents |
Real and imaginary sequence currents into each 3-phase terminal |
dssElem.Powers |
Active and reactive powers into each conductor of each terminal |
dssElem.SeqPowers |
Active and reactive powers into each 3-phase terminal |
Execute System Simulation
A time-based simulation must be set up with an OpenDSS command in the original script, or with a COM interface text command.
set mode=daily stepsize=15m number=96
Once the solution is set up, the time-based simulation can be executed in OpenDSS with a single call to the Solution interface:
dssSolution.Solve()
However, DMS control applications require custom actions to be performed at varying places within the OpenDSS solution process. To avoid confusion, a discussion of the OpenDSS solution process is next, followed by example interface scripts to insert custom controls in various points of the process.
Overview of OpenDSS simulation process
The OpenDSS simulation process is illustrated in the flow chart of Figure 1. Each time step consists of three main parts. First, a solution with controls is completed unto convergence, shown by the large yellow box. Next, clean-up operations are done such as sampling monitors and updating the charge percentage of a storage element. Finally, if there is another time step, time is incremented and the loop begins again. Within the solve-with-control block, the small blue blocks depict the controller operations in OpenDSS. For the necessary number of control iterations, each control element is allowed to measure the results of the power flow and push actions to the control queue for operation after a delay. When all actions on the control queue have been completed and no further ones are added, or if the number of control iterations has exceeded the maximum, the solution with controls is considered finished.
Figure 1: Flow chart of OpenDSS simulation process
Figure 1 also shows the two locations, marked by red X’s, where a user may want to insert control actions. Point (1) is the place for DMS-style control optimization settings, where the parameters for each control element can be set. Management tools can use the entire large solve-with-control block as a testing function to see what the results will be from any number of configurations.
To implement true customized control elements, which can push actions to the control queue, iterate over each control cycle, and interact with other controllers through timing setup, the user must insert functions at point (2) on the chart of Figure 1. This point is inside the control function and has the benefit of customized controller iterations, but decisions here cannot use the entire yellow box as a test unit without causing infinite recursion.
Management actions and control optimization
This example assumes that some sort of time-based simulation is already set up in the OpenDSS script file, such as a daily load shape with mode set to daily. The key is to find the original number of time steps and then run them one at a time, calling the custom control function once at each step.
originalSteps = dssSolution.Number
dssSolution.Number = 1
for ii in range(0, originalSteps):
controlFunction()
dssSolution.SolveSnap()
dssSolution.FinishTimeStep()
Setting the solution number to one causes the solve function to only run a single time step, and the new FinishTimeStep() function places the cleanup and increment actions at the end. Now the custom control function can make measurements and change control settings, even using SolveSnap() to
analyze possible configurations before setting the controls for the time step.
An important note about OpenDSS solutions
OpenDSS does not in any way reset to a previous configuration. The ending state of a circuit after a simulation, even if time does not advance, is retained every time. So if at 1 PM a capacitor controller operates to close a capacitor, running the simulation again, even at 1 PM, will not have the same effect unless the user manually opens the capacitor to restore it to its original state before running the simulation. This way of operation is critical to the performance of OpenDSS, but must be kept in mind when doing control optimizations. Care must be taken to reset the circuit to its original state after doing a test solution.
Emulate the control iteration loop
To implement a true custom control element, it is necessary to replace the SolveSnap() command above, which corresponds to the large yellow block in Figure 1, with an emulation to allow access at point (2). This is done with code similar to below.
dssSolution.InitSnap()
iteration = 0
while not dssSolution.ControlActionsDone:
dssSolution.SolveNoControl()
customControlLoop()
dssSolution.CheckControls()
iteration += 1
if iteration > dssSolution.MaxControlIterations:
break
Now at each control iteration, the custom control loop will be run before any OpenDSS controls. A discussion of adding control queue functionality follows.
Access the control queue
The control queue provides a higher-detailed level management of the control iterations. OpenDSS performs many control iterations, solving the power flow each time, until all pending actions have been completed and no control devices push new actions to the control queue. For more information about the control queue and how it interfaces with COM, see the document “OpenDSS CtrlQueue Interface.pdf” in the OpenDSS docs. Here is an example of pushing two control actions to the control queue. In Python, it may be helpful to map the device handle to a Python function using a dictionary. In this example, actionFunction1 and actionFunction2 are two functions that take actionCode as a single argument.
def actionFunction1(actionCode):
print “function 1: action code is “ + str(actionCode)
def actionFunction2(actionCode):
print “function 2: action code is “ + str(actionCode)
actionMap = {0: actionFunction1, 1: actionFunction2}
h = 1
s = 15
actionCode = deviceHandle = 0
dssCircuit.CtrlQueue.Push(h, s, actionCode, deviceHandle)
s = 30
actionCode = deviceHandle = 1
dssCircuit.CtrlQueue.Push(h, s, actionCode, deviceHandle)
However, OpenDSS will not handle these actions themselves. OpenDSS will simply push the device handle and action code to the action list when the time comes for them to be executed. To catch these actions, after CheckControls() has been run, use the dssCircuit.CtrlQueue class on the COM module, as demonstrated below. Assume that the previous block of code is inside customControlLoop() and that this code would be part of the block with the while loop on the previous page.
customControlLoop()
dssSolution.CheckControls()
while dssCircuit.CtrlQueue.PopAction != 0:
deviceHandle = dssCircuit.CtrlQueue.DeviceHandle
actionCode = self.dssCircuit.CtrlQueue.ActionCode
actionFunction = actionMap[deviceHandle]
actionFunction(actionCode)
By stepping through the control iterations, the user can call his custom function at each control iteration, push actions to the control queue, and handle them as they are pushed back by OpenDSS. In this framework, custom control actions happen alongside internal OpenDSS controls. The framework also allows for adding delays to control actions in the custom loop, which is typical behavior for conventional control schemes.
Full Simulation Example
This example shows the code to load a circuit file, set up a daily simulation, and perform custom control functions at both points in Figure 1.
# Simulation example using OpenDSS COM interface
import win32com.client
dssObj = win32com.client.Dispatch("OpenDSSEngine.DSS")
dssText = dssObj.Text
dssCircuit = dssObj.ActiveCircuit
dssSolution = dssCircuit.Solution
dssElem = dssCircuit.ActiveCktElement
# Load the circuit and set up the daily simulation
dssText.Command = r"compile 'master.dss'"
dssText.Command = r"compile 'setupDaily.dss'"
dssSolution.MaxControlIterations = 20
# Iterate through the time steps
originalSteps = dssSolution.Number
dssSolution.Number = 1
for stepNumber in range(originalSteps):
print "-- step " + str(stepNumber)
# as an example, measure the bus voltage and decide
# if cap control should be enabled or not.
# running a solve with control would also be possible here
# this is point (1) on Figure 1
dssCircuit.SetActiveBus("cap_bus")
capVolt = dssBus.puVmagAngle[0]
dssCircuit.setActiveElement("capcontrol.cc1")
if capVolt > 0.94:
dssElem.Properties("enabled").Val = True
else:
dssElem.Properties("enabled").Val = False
# Run the solve with control loop
dssSolution.InitSnap()
iteration = 0
while not dssSolution.ControlActionsDone:
dssSolution.SolveNoControl()
# This is point (2) on Figure 2
# as an example, push two action functions to the
# control queue. They will both update the kVAr of
# PV systems. The first will be delayed by 15 seconds,
# and the second by 30 seconds.
# These are only pushed on the first iteration,
# otherwise the solution will never converge!
def actionFunction1(actionCode):
dssCircuit.setActiveElement("pv1")
dssElem.Properties(“kVAr”).Val = 25 + 4 * actionCode
def actionFunction2(actionCode):
dssCircuit.setActiveElement("pv2")
dssElem.Properties(“kVAr”).Val = 15 + 4 * actionCode
actionMap = {0: actionFunction1, 1: actionFunction2}
h = 1
s = 15
actionCode = deviceHandle = 0
if iteration == 1:
dssCircuit.CtrlQueue.Push(h, s, actionCode,deviceHandle)
s = 30
actionCode = deviceHandle = 1
if iteration == 1:
dssCircuit.CtrlQueue.Push(h, s, actionCode,deviceHandle)
# with this command, OpenDSS pushes active to queue
dssSolution.CheckControls()
# if there are any actions which OpenDSS is calling to
# be handled now, this loop will do so.
while dssCircuit.CtrlQueue.PopAction != 0:
deviceHandle = dssCircuit.CtrlQueue.DeviceHandle
actionCode = self.dssCircuit.CtrlQueue.ActionCode
actionFunction = actionMap[deviceHandle]
actionFunction(actionCode)
iteration += 1
if iteration >= dssSolution.MaxControlIterations:
print "exceeded max iterations!"
break
dssSolution.FinishTimeStep() # cleanup and increment time
Early Binding on COM Interface
Python by default uses late binding for COM interfaces, which means that each property and method will be found via a time-consuming lookup process. In contrast, early binding greatly improves time performance of the COM interface and may be helpful for complex simulations. Note that all object properties become casesensitive when early binding is used.
To use early binding in the following two first lines of code:
import win32com.client
dssObj = win32com.client.Dispatch("OpenDSSEngine.DSS")
Instead use the following code, which takes advantage of the makepy package to do the early binding on the OpenDSSEngine interface.
import win32com.client
from win32com.client import makepy
import sys
sys.argv = ["makepy", "OpenDSSEngine.DSS"]
makepy.main()
dssObj = win32com.client.Dispatch("OpenDSSEngine.DSS")
Explore Other Resources
Information about the capabilities of OpenDSS can be found in the OpenDSS help, as well as the manual and other documentation and examples available at https://www.epri.com/pages/sa/opendss?lang=en
Once the OpenDSS COM server is registered on a computer, Microsoft Excel’s VBA object browser is an excellent way to explore the possibilities in the OpenDSS COM server. From Excel, press Alt+F11, then choose Tools > References, and select the OpenDSSEngine. Press F2 to open the object browser, and select the OpenDSSEngine from the project library drop-down combo box. The available classes will be on the left, and the members will be listed to the right.
For more information about Python, see https://docs.python.org/2/