Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
c665a55
Update README.md
xiefuhong Dec 3, 2025
5c38664
update the motor load and PV ridethrough
xiefuhong Dec 3, 2025
442bd87
Updating pythin version in docker file.
HaleyRoss Dec 30, 2025
3403d13
Correcting ride through bug
HaleyRoss Feb 5, 2026
c58e558
update deprecated cascaded_union to unary_union
Feb 5, 2026
e305be8
Correct Vbase between model changes
HaleyRoss Mar 26, 2026
07fdcac
Merge branch 'develop_cosim_bug_fix' of https://github.com/NREL/PyDSS…
Mar 30, 2026
5cb92e0
Merge branch 'master' of https://github.com/NREL/PyDSS into develop_c…
Mar 30, 2026
ea42551
removed divide by 3 which was added to emulate coiteration but is unn…
Apr 1, 2026
ddc19b7
fix Nan to nan
Apr 7, 2026
a78a9e0
add DynamicVoltageSupport to enums
Apr 8, 2026
c20d5bb
remove extraneous log comments add export for dynamic voltage test
Apr 8, 2026
6b5ed15
add report and log dirs for dynamic voltage test
Apr 8, 2026
aa94b23
remove extra logs update from deprecated lsim2 to lsim
Apr 15, 2026
88888bf
make sure new control types are registered before testing
Apr 15, 2026
b4b15e4
allow multi-inputs
Apr 17, 2026
bc06920
include multi-input in pydantic check
Apr 17, 2026
32c8e5a
commits while testing cosim to work with uv
Apr 17, 2026
b267027
editing multiinput interface
Apr 20, 2026
1266032
editing multiinput interface
Apr 20, 2026
5250a01
editing multiinput interface
Apr 20, 2026
7e3fe67
allow line outages
Jul 9, 2026
158d99a
property is just Switch
Jul 9, 2026
a17445c
directly modify switch to be open in helics interface
Jul 9, 2026
6f05279
debug open switch
Jul 9, 2026
5929c6d
need to create new switch control before telling it to be open
Jul 9, 2026
37b53a5
this version of opendssdirect uses run_command
Jul 9, 2026
bd4eaa4
include existing switches
Jul 9, 2026
2f060b6
remove switch delay
Jul 9, 2026
1cddc00
this version uses NormalState
Jul 9, 2026
164d613
clean up commented out lines
Jul 9, 2026
8bc4e84
make sure state subscriptions are integers
Jul 9, 2026
c0ccae1
skip updates if subscription value is invalid
Jul 10, 2026
6450e84
add batch controllers
HaleyRoss Jul 13, 2026
ef7d7a1
Merge branch 'develop_cosim_bug_fix' of https://github.com/NREL/PyDSS…
HaleyRoss Jul 13, 2026
f43d4fc
added more logging
Jul 13, 2026
6063999
try with keyword switchstate
Jul 13, 2026
cbd5cc8
tell the switch to perform the open action
Jul 13, 2026
b2e1604
add debug logging to switch opening
Jul 14, 2026
1753379
move the switch handling to the dssElement instead of helics interface
Jul 14, 2026
8f7c3e2
add logging to debug switch control
Jul 14, 2026
45f26fb
IsSwitch is used in some cases Switch in others
Jul 15, 2026
31687fe
line not getting updated to switch
Jul 15, 2026
1787190
try to force switch conversion with .IsSwitch(True)
Jul 15, 2026
704cb04
remove file causing failure to merge
HaleyRoss Aug 4, 2026
c8d01b4
Merge branch 'master' into develop_cosim_bug_fix
HaleyRoss Aug 7, 2026
cbe5432
Remove trailing-space duplicate docs path
HaleyRoss Aug 7, 2026
16d5f56
Merge pull request #162 from NatLabRockies/threat_federate
HaleyRoss Sep 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM python:3.7.0-slim
FROM python:3.11.0-slim

RUN apt-get update

Expand Down
11 changes: 0 additions & 11 deletions docs/source/co-simulation_support.rst

This file was deleted.

3 changes: 2 additions & 1 deletion src/pydss/SolveMode.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@


def GetSolver(settings: SimulationSettingsModel, dssInstance):
logger.info('Setting solver to %s mode.', settings.project.simulation_type.value)
# logger.info('Setting solver to %s mode.', settings.project.simulation_type.value)
logger.info(f'Setting solver to {settings.project.simulation_type.value} mode.')
return get_solver_from_simulation_type(settings.project)


Expand Down
7 changes: 5 additions & 2 deletions src/pydss/cli/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from pathlib import Path
import ast
import sys
import os

from loguru import logger
import click
Expand Down Expand Up @@ -83,7 +84,10 @@ def run(project_path, options=None, tar_project=False, zip_project=False, verbos

logger.level(console_level)
if filename:
logger.add(filename)
if settings.logging.clear_old_log_file:
logger.add(filename, mode="w")
else:
logger.add(filename)

logger.info(f"CLI: [{get_cli_string()}]",)

Expand All @@ -92,7 +96,6 @@ def run(project_path, options=None, tar_project=False, zip_project=False, verbos
if not isinstance(options, dict):
logger.error("options are invalid: %s", options)
sys.exit(1)

project = PyDssProject.load_project(project_path, options=options, simulation_file=simulations_file)
project.run(tar_project=tar_project, zip_project=zip_project, dry_run=dry_run)

Expand Down
1 change: 1 addition & 0 deletions src/pydss/dataset_buffer.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ def __init__(
max_chunk_bytes=None, attributes=None, names=None,
column_ranges_per_name=None, data=None
):

if max_chunk_bytes is None:
max_chunk_bytes = DEFAULT_MAX_CHUNK_BYTES
self._buf_index = 0
Expand Down
34 changes: 32 additions & 2 deletions src/pydss/dssElement.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import ast

from loguru import logger
from opendssdirect import DSSException

from pydss.dssBus import dssBus
Expand Down Expand Up @@ -60,9 +60,10 @@ def __init__(self, dssInstance):
super(dssElement, self).__init__(dssInstance, name, fullName)
self._Enabled = dssInstance.CktElement.Enabled()
if not self._Enabled:
logger.debug(f"Element isn't defined: {fullName}")
return

self._Parameters = {}
logger.debug(fullName)
self._NumTerminals = dssInstance.CktElement.NumTerminals()
self._NumConductors = dssInstance.CktElement.NumConductors()

Expand Down Expand Up @@ -135,8 +136,12 @@ def DataLength(self, VarName):
return 0, None

def GetValue(self, VarName, convert=False):

if self._dssInstance.Element.Name() != self._FullName:
self.SetActiveObject()
fullName = self._dssInstance.Element.Name()
# logger.debug("123456789123456789123456789123456789123456789123456789")
# logger.debug(fullName)
if VarName in self._Variables:
VarValue = self.GetVariable(VarName, convert=convert)
elif VarName in self._Parameters:
Expand All @@ -154,6 +159,31 @@ def SetActiveObject(self):

def SetParameter(self, Param, Value):
reply = self._dssInstance.utils.run_command(self._FullName + '.' + Param + ' = ' + str(Value))
logger.info(f'parameter update reply: {reply}')
if Param == 'IsSwitch' or Param == 'Switch':
self._dssInstance.Lines.Name(self._Name)
self._dssInstance.Lines.IsSwitch(True)
switch_name = self._FullName.replace('Line','SwtControl')
if switch_name.replace('SwtControl.','') in self._dssInstance.SwtControls.AllNames():
logger.info(f'{switch_name} already exists. Opening existing switch')
self._dssInstance.SwtControls.Name(self._Name)
self._dssInstance.SwtControls.Delay(0)
self._dssInstance.SwtControls.State(int(Value)) # 1 is open and 2 is closed
self._dssInstance.SwtControls.NormalState(int(Value))
#dss.SwtControls.Action(state_value)
else:
#if it's not already created, then create the switch and set params with one command line
new_switch_command = f'New SwtControl.{self._Name} Delay=0 enabled=Yes Normal=Open State=Open SwitchedObj={self._FullName}'
self._dssInstance.run_command(new_switch_command)
self._dssInstance.SwtControls.IsLocked(True)
#dss.SwtControls.Action(state_value)
logger.info(f'{switch_name} created with {new_switch_command}')
self._dssInstance.Lines.Name(self._Name)
line_status = self._dssInstance.Lines.IsSwitch()
self._dssInstance.SwtControls.Name(self._Name)
swt_status = self._dssInstance.SwtControls.State()
logger.info(f'{self._FullName} switch status: {line_status} and switch position: {swt_status}')

if reply != "":
raise Exception(f"SetParameter failed: {reply}")
return self.GetParameter(Param)
Expand Down
84 changes: 62 additions & 22 deletions src/pydss/dssInstance.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,11 @@ def _compile_model(self):
def _CreateControllers(self, ControllerDict):
self._pyControls = {}
self._pyControls_types = {}
# logger.info(f'self._dssObjects -> {self._dssObjects}')
# os.system("PAUSE")
for ControllerType, ElementsDict in ControllerDict.items():
# logger.info(f'ControllerType -> {ControllerType}, ElementsDict -> {ElementsDict}')
# os.system("PAUSE")
for ElmName, SettingsDict in ElementsDict.items():
Controller = pyControllers.pyController.Create(ElmName, ControllerType, SettingsDict, self._dssObjects,
self._dssInstance, self._dssSolver)
Expand All @@ -167,24 +171,47 @@ def _CreateControllers(self, ControllerDict):
if controller_name not in self._pyControls_types:
self._pyControls_types[controller_name] = class_name
logger.info('Created pyController -> Controller.' + ElmName)

# --- Batch MotorStall controllers ---
from pydss.pyControllers.Controllers.MotorStall import MotorStall
from pydss.pyControllers.Controllers.MotorStallBatch import MotorStallBatch
motor_stall_keys = [k for k, v in self._pyControls.items()
if isinstance(v, MotorStall)]
if len(motor_stall_keys) > 0:
motor_stall_ctrls = [self._pyControls[k] for k in motor_stall_keys]
batch = MotorStallBatch(motor_stall_ctrls)
# Remove individual controllers, add the batch
for k in motor_stall_keys:
del self._pyControls[k]
self._pyControls['Controller.MotorStallBatch'] = batch
logger.info(f"Batched {len(motor_stall_keys)} MotorStall controllers into MotorStallBatch")

# --- Batch PvVoltageRideThru controllers ---
from pydss.pyControllers.Controllers.PvVoltageRideThru import PvVoltageRideThru
from pydss.pyControllers.Controllers.PvVoltageRideThruBatch import PvVoltageRideThruBatch
pv_rt_keys = [k for k, v in self._pyControls.items()
if isinstance(v, PvVoltageRideThru)]
if len(pv_rt_keys) > 0:
pv_rt_ctrls = [self._pyControls[k] for k in pv_rt_keys]
pv_batch = PvVoltageRideThruBatch(pv_rt_ctrls)
for k in pv_rt_keys:
del self._pyControls[k]
self._pyControls['Controller.PvVoltageRideThruBatch'] = pv_batch
logger.info(f"Batched {len(pv_rt_keys)} PvVoltageRideThru controllers into PvVoltageRideThruBatch")

self._controller_list = list(self._pyControls.values())
self._controllers_by_priority = {p: [] for p in range(CONTROLLER_PRIORITIES)}
for controller in self._controller_list:
for p in getattr(controller, 'ACTIVE_PRIORITIES', range(CONTROLLER_PRIORITIES)):
self._controllers_by_priority[p].append(controller)
return

def _update_controllers(self, Priority, Time, Iteration, UpdateResults):
errors = []
maxError = 0
_pyControls_types = set(self._pyControls_types.values())

for class_name in _pyControls_types:
self._dssInstance.Basic.SetActiveClass(class_name)
elm = self._dssInstance.ActiveClass.First()
while elm:
element_name = self._dssInstance.CktElement.Name()
controller_name = 'Controller.' + element_name
if controller_name in self._pyControls:
controller = self._pyControls[controller_name]
error = controller.Update(Priority, Time, UpdateResults)
maxError = error if error > maxError else maxError
elm = self._dssInstance.ActiveClass.Next()
for controller in self._controllers_by_priority[Priority]:
error = controller.Update(Priority, Time, UpdateResults)
if error > maxError:
maxError = error
return maxError < self._settings.project.error_tolerance, maxError

@staticmethod
Expand All @@ -205,7 +232,8 @@ def CreateDssObjects(dssBuses):
InvalidSelection = ['Settings', 'ActiveClass', 'dss', 'utils', 'PDElements', 'XYCurves', 'Bus', 'Properties']
# TODO: this causes a segmentation fault. Aadil says it may not be needed.
#self._dssObjectsByClass={'LoadShape': self._get_relavent_object_dict('LoadShape')}

# logger.info(f"dss.Circuit.AllElementNames() -> {dss.Circuit.AllElementNames()}")
# os.system("PAUSE")
for ElmName in dss.Circuit.AllElementNames():
Class, Name = ElmName.split('.', 1)
ClassName = Class + 's'
Expand Down Expand Up @@ -239,13 +267,18 @@ def _get_relavent_object_dict(self, key):

@track_timing(timer_stats_collector)
def RunStep(self, step, updateObjects=None):

# updating parameters before simulation run
if self._settings.logging.log_time_step_updates:
logger.info(f'Pydss datetime - {self._dssSolver.GetDateTime()}')
logger.info(f'OpenDSS time [h] - {self._dssSolver.GetOpenDSSTime()}')
if self._settings.profiles.use_profile_manager:
self.profileStore.update()

if self._settings.helics.co_simulation_mode:
# self._heilcs_interface.updateHelicsPublications()
self._increment_flag, helics_time = self._heilcs_interface.request_time_increment()

if self._settings.helics.co_simulation_mode:
self._heilcs_interface.updateHelicsSubscriptions()
else:
Expand All @@ -263,6 +296,7 @@ def RunStep(self, step, updateObjects=None):
for i in range(self._settings.project.max_control_iterations):
has_converged, error = self._update_controllers(priority, step, i, UpdateResults=False)
logger.debug('Control Loop {} convergence error: {}'.format(priority, error))
logger.debug('Control Loop {} convergence @step {} '.format(priority, step))
if has_converged:
priority_has_converged = True
break
Expand All @@ -279,7 +313,6 @@ def RunStep(self, step, updateObjects=None):
logger.warning('Control Loop {} no convergence @ {} '.format(priority, step))
self._HandleConvergenceErrorChecks(step, error)


if self._settings.frequency.enable_frequency_sweep and \
self._settings.project.simulation_type != SimulationType.DYNAMIC:
self._dssSolver.setMode('Harmonic')
Expand All @@ -297,8 +330,10 @@ def RunStep(self, step, updateObjects=None):

if self._settings.helics.co_simulation_mode:
self._heilcs_interface.updateHelicsPublications()
self._increment_flag, helics_time = self._heilcs_interface.request_time_increment()
# if step < 1:
# self._increment_flag, helics_time = self._heilcs_interface.request_time_increment_2()

# os.system("PAUSE")
return time_step_has_converged

def _HandleConvergenceErrorChecks(self, step, error):
Expand All @@ -316,7 +351,7 @@ def _HandleOpenDSSConvergenceErrorChecks(self, step):
self._convergenceErrorsOpenDSS += 1

if self._maxConvergenceErrorCount is not None and self._convergenceErrorsOpenDSS > self._maxConvergenceErrorCount:
logger.error("Exceeded OpenDSS convergence error count threshold at step %s", step)
logger.error(f"Exceeded OpenDSS convergence error count threshold at step {step}")
raise OpenDssConvergenceErrorCountExceeded(f"{self._convergenceErrorsOpenDSS} errors occurred")

def DryRunSimulation(self, project, scenario):
Expand Down Expand Up @@ -357,7 +392,7 @@ def RunSimulation(self, project, scenario, MC_scenario_number=None):
dss.Solution.Convergence(self._settings.project.error_tolerance)
logger.info('Running simulation from {} till {}.'.format(sTime, eTime))
logger.info('Simulation time step {}.'.format(Steps))
logger.info("Set OpenDSS convergence to %s", dss.Solution.Convergence())
logger.info(f"Set OpenDSS convergence to {dss.Solution.Convergence()}")
logger.info('Max convergence error count {}.'.format(self._maxConvergenceErrorCount))
logger.info("initializing store")
self.ResultContainer.InitializeDataStore(project.hdf_store, Steps, MC_scenario_number)
Expand Down Expand Up @@ -390,10 +425,14 @@ def RunSimulation(self, project, scenario, MC_scenario_number=None):
pydss_has_converged = self.RunStep(step)
opendss_has_converged = dss.Solution.Converged()
if not opendss_has_converged:
logger.error("OpenDSS did not converge at step=%s pydss_converged=%s",
step, pydss_has_converged)
logger.error(f"OpenDSS did not converge at step={step} pydss_converged={pydss_has_converged}")
self._HandleOpenDSSConvergenceErrorChecks(step)
has_converged = pydss_has_converged and opendss_has_converged

if pydss_has_converged:
has_converged = True
else:
has_converged = pydss_has_converged and opendss_has_converged

if step == 0 and self.ResultContainer is not None:
size = make_human_readable_size(self.ResultContainer.max_num_bytes())
logger.info('Storage requirement estimation: %s, estimated based on first time step run.', size)
Expand Down Expand Up @@ -426,6 +465,7 @@ def RunSimulation(self, project, scenario, MC_scenario_number=None):

if self._settings.exports.export_results:
current_results = self.ResultContainer.CurrentResults

yield False, step, has_converged, current_results

finally:
Expand Down
Loading
Loading