Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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
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
Loading