Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Welcome to the pydss Repository!


**PyDSS** is a high level python interface for **OpenDSS** and provides the following functionalities

Documentation on installation, setup and examples can be found here https://natlabrockies.github.io/PyDSS/index.html
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ dependencies = [
"numpy",
"OpenDSSDirect.py==0.8.4",
"pandas",
"pssepath",
"pvder",
"pydantic~=2.5.2",
"pymongo",
Expand Down
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
9 changes: 7 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 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
74 changes: 70 additions & 4 deletions src/pydss/helics_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import helics
import os
import re

import pandas as pd
from loguru import logger

from pydss.simulation_input_models import SimulationSettingsModel
Expand Down Expand Up @@ -208,6 +208,7 @@ def _create_helics_federate(self):
IP = self._settings.helics.broker
Port = self._settings.helics.broker_port
logger.info("Connecting to broker @ {}".format(f"{IP}:{Port}" if Port else IP))
logger.info(f"Connecting to broker @ {self._settings.helics}")
if self._settings.helics.broker:
helics.helicsFederateInfoSetBroker(self.fedinfo, str(self._settings.helics.broker))
if self._settings.helics.broker_port:
Expand Down Expand Up @@ -261,8 +262,9 @@ def updateHelicsSubscriptions(self):
value = helics.helicsInputGetInteger(subscription.sub)

if value and value != 0:
if value > 1e6 or value < -1e6:
value = 1.0
logger.info(f"value is {value}")
if value > 1e6 or value < -1e6 or pd.isna(value):
value = 1.0

value = value * subscription.multiplier
subscription.object.SetParameter(subscription.property, value)
Expand Down Expand Up @@ -313,6 +315,18 @@ def _registerFederatePublications(self, publications:Publications = None):
logger.info(str(self.publications.publications))
for publication in self.publications.publications:
logger.info(f"pubscription created: {publication}")

# Register an aggregate generator total-power publication so that
# the co-simulation launcher can subscribe PSSE machines to it.
# Published as [P_kW, Q_kvar] with POSITIVE values for generation.
self._gen_total_pub = None
gen_class = self._objects_by_class.get("Generators", {})
if gen_class:
gen_pub_name = f"{self._settings.helics.federate_name}.Generators.Total.TotalPower"
self._gen_total_pub = helics.helicsFederateRegisterGlobalTypePublication(
self._federate, gen_pub_name, "vector", ""
)
logger.info(f"Registered generator aggregate publication: {gen_pub_name}")
return

def updateHelicsPublications(self):
Expand All @@ -333,11 +347,63 @@ def updateHelicsPublications(self):
else:
raise ValueError("Unsupported data type forr teh HELICS interface")
logger.info(f"{publication} - {value}")

# Publish aggregate generator total power [P_kW, Q_kvar].
# Sign convention: POSITIVE = generation (negated from OpenDSS
# CktElement.Powers which uses load convention, i.e. negative for
# power injected by generators).
if self._gen_total_pub is not None:
total_gen_p = 0.0
total_gen_q = 0.0
for gen_name, gen_obj in self._objects_by_class.get("Generators", {}).items():
powers = gen_obj.GetValue("Powers")
if powers is not None and isinstance(powers, list):
for i in range(0, len(powers), 2):
total_gen_p += powers[i]
if i + 1 < len(powers):
total_gen_q += powers[i + 1]
# Negate: OpenDSS Powers are negative for gen injection
total_gen_p = -total_gen_p
total_gen_q = -total_gen_q
helics.helicsPublicationPublishVector(self._gen_total_pub, [total_gen_p, total_gen_q])
logger.debug(f"Published generator total power: [{total_gen_p:.4f}, {total_gen_q:.4f}]")
return

def request_time_increment(self):
error = sum([abs(sub.states[0] - sub.states[1]) for sub in self.subscriptions.subscriptions])
r_seconds = self._dss_solver.GetTotalSeconds() #- self._dss_solver.GetStepResolutionSeconds()
# r_seconds = self._dss_solver.GetTotalSeconds() # parallel
r_seconds = self._dss_solver.GetTotalSeconds() + self._dss_solver.GetStepResolutionSeconds() # transmission priority
# if r_seconds > 0:
# r_seconds = self._dss_solver.GetTotalSeconds() + self._dss_solver.GetStepResolutionSeconds()
# logger.info(f"self._dss_solver.GetTotalSeconds(): {self._dss_solver.GetTotalSeconds()}, self._dss_solver.GetStepResolutionSeconds(): {self._dss_solver.GetStepResolutionSeconds()}")
# os.system('PAUSE')
if not self._settings.helics.iterative_mode:
while self.c_seconds < r_seconds:
self.c_seconds = helics.helicsFederateRequestTime(self._federate, r_seconds)
logger.info('Time requested: {} - time granted: {} '.format(r_seconds, self.c_seconds))
return True, self.c_seconds
else:

self.c_seconds, iteration_state = helics.helicsFederateRequestTimeIterative(
self._federate,
r_seconds,
helics.helics_iteration_request_iterate_if_needed
)

logger.info('Time requested: {} - time granted: {} error: {} it: {}'.format(
r_seconds, self.c_seconds, error, self.itr))
if error > -1 and self.itr < self._co_convergance_max_iterations - 1:
self.itr += 1
return False, self.c_seconds
else:
self.itr = 0
return True, self.c_seconds

def request_time_increment_2(self):
error = sum([abs(sub.states[0] - sub.states[1]) for sub in self.subscriptions.subscriptions])
r_seconds = self._dss_solver.GetTotalSeconds() + self._dss_solver.GetStepResolutionSeconds()/2
# logger.info('Time requested: {} - self._dss_solver.GetStepResolutionSeconds(): {} '.format(r_seconds, self._dss_solver.GetStepResolutionSeconds()))
# os.system('PAUSE')
if not self._settings.helics.iterative_mode:
while self.c_seconds < r_seconds:
self.c_seconds = helics.helicsFederateRequestTime(self._federate, r_seconds)
Expand Down
2 changes: 1 addition & 1 deletion src/pydss/modes/solver_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def __init__(self, dssInstance, settings: ProjectModel):

#self._dssSolution.DblHour()
self.reSolve()
logger.info("%s solver setup complete", settings.simulation_type)
logger.info(f"{settings.simulation_type} solver setup complete")

def setFrequency(self, frequency):
self._dssSolution.Frequency(frequency)
Expand Down
Loading