Skip to content

Commit 2d59d80

Browse files
committed
add batch controllers
1 parent c58d1fb commit 2d59d80

12 files changed

Lines changed: 4243 additions & 87 deletions

src/pydss/dssInstance.py

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -167,24 +167,47 @@ def _CreateControllers(self, ControllerDict):
167167
if controller_name not in self._pyControls_types:
168168
self._pyControls_types[controller_name] = class_name
169169
logger.info('Created pyController -> Controller.' + ElmName)
170+
171+
# --- Batch MotorStall controllers ---
172+
from pydss.pyControllers.Controllers.MotorStall import MotorStall
173+
from pydss.pyControllers.Controllers.MotorStallBatch import MotorStallBatch
174+
motor_stall_keys = [k for k, v in self._pyControls.items()
175+
if isinstance(v, MotorStall)]
176+
if len(motor_stall_keys) > 0:
177+
motor_stall_ctrls = [self._pyControls[k] for k in motor_stall_keys]
178+
batch = MotorStallBatch(motor_stall_ctrls)
179+
# Remove individual controllers, add the batch
180+
for k in motor_stall_keys:
181+
del self._pyControls[k]
182+
self._pyControls['Controller.MotorStallBatch'] = batch
183+
logger.info(f"Batched {len(motor_stall_keys)} MotorStall controllers into MotorStallBatch")
184+
185+
# --- Batch PvVoltageRideThru controllers ---
186+
from pydss.pyControllers.Controllers.PvVoltageRideThru import PvVoltageRideThru
187+
from pydss.pyControllers.Controllers.PvVoltageRideThruBatch import PvVoltageRideThruBatch
188+
pv_rt_keys = [k for k, v in self._pyControls.items()
189+
if isinstance(v, PvVoltageRideThru)]
190+
if len(pv_rt_keys) > 0:
191+
pv_rt_ctrls = [self._pyControls[k] for k in pv_rt_keys]
192+
pv_batch = PvVoltageRideThruBatch(pv_rt_ctrls)
193+
for k in pv_rt_keys:
194+
del self._pyControls[k]
195+
self._pyControls['Controller.PvVoltageRideThruBatch'] = pv_batch
196+
logger.info(f"Batched {len(pv_rt_keys)} PvVoltageRideThru controllers into PvVoltageRideThruBatch")
197+
198+
self._controller_list = list(self._pyControls.values())
199+
self._controllers_by_priority = {p: [] for p in range(CONTROLLER_PRIORITIES)}
200+
for controller in self._controller_list:
201+
for p in getattr(controller, 'ACTIVE_PRIORITIES', range(CONTROLLER_PRIORITIES)):
202+
self._controllers_by_priority[p].append(controller)
170203
return
171204

172205
def _update_controllers(self, Priority, Time, Iteration, UpdateResults):
173-
errors = []
174206
maxError = 0
175-
_pyControls_types = set(self._pyControls_types.values())
176-
177-
for class_name in _pyControls_types:
178-
self._dssInstance.Basic.SetActiveClass(class_name)
179-
elm = self._dssInstance.ActiveClass.First()
180-
while elm:
181-
element_name = self._dssInstance.CktElement.Name()
182-
controller_name = 'Controller.' + element_name
183-
if controller_name in self._pyControls:
184-
controller = self._pyControls[controller_name]
185-
error = controller.Update(Priority, Time, UpdateResults)
186-
maxError = error if error > maxError else maxError
187-
elm = self._dssInstance.ActiveClass.Next()
207+
for controller in self._controllers_by_priority[Priority]:
208+
error = controller.Update(Priority, Time, UpdateResults)
209+
if error > maxError:
210+
maxError = error
188211
return maxError < self._settings.project.error_tolerance, maxError
189212

190213
@staticmethod
@@ -239,6 +262,7 @@ def _get_relavent_object_dict(self, key):
239262

240263
@track_timing(timer_stats_collector)
241264
def RunStep(self, step, updateObjects=None):
265+
242266
# updating parameters before simulation run
243267
if self._settings.logging.log_time_step_updates:
244268
logger.info(f'Pydss datetime - {self._dssSolver.GetDateTime()}')
@@ -279,7 +303,6 @@ def RunStep(self, step, updateObjects=None):
279303
logger.warning('Control Loop {} no convergence @ {} '.format(priority, step))
280304
self._HandleConvergenceErrorChecks(step, error)
281305

282-
283306
if self._settings.frequency.enable_frequency_sweep and \
284307
self._settings.project.simulation_type != SimulationType.DYNAMIC:
285308
self._dssSolver.setMode('Harmonic')
@@ -426,6 +449,7 @@ def RunSimulation(self, project, scenario, MC_scenario_number=None):
426449

427450
if self._settings.exports.export_results:
428451
current_results = self.ResultContainer.CurrentResults
452+
429453
yield False, step, has_converged, current_results
430454

431455
finally:

src/pydss/helics_interface.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,18 @@ def _registerFederatePublications(self, publications:Publications = None):
313313
logger.info(str(self.publications.publications))
314314
for publication in self.publications.publications:
315315
logger.info(f"pubscription created: {publication}")
316+
317+
# Register an aggregate generator total-power publication so that
318+
# the co-simulation launcher can subscribe PSSE machines to it.
319+
# Published as [P_kW, Q_kvar] with POSITIVE values for generation.
320+
self._gen_total_pub = None
321+
gen_class = self._objects_by_class.get("Generators", {})
322+
if gen_class:
323+
gen_pub_name = f"{self._settings.helics.federate_name}.Generators.Total.TotalPower"
324+
self._gen_total_pub = helics.helicsFederateRegisterGlobalTypePublication(
325+
self._federate, gen_pub_name, "vector", ""
326+
)
327+
logger.info(f"Registered generator aggregate publication: {gen_pub_name}")
316328
return
317329

318330
def updateHelicsPublications(self):
@@ -333,6 +345,26 @@ def updateHelicsPublications(self):
333345
else:
334346
raise ValueError("Unsupported data type forr teh HELICS interface")
335347
logger.info(f"{publication} - {value}")
348+
349+
# Publish aggregate generator total power [P_kW, Q_kvar].
350+
# Sign convention: POSITIVE = generation (negated from OpenDSS
351+
# CktElement.Powers which uses load convention, i.e. negative for
352+
# power injected by generators).
353+
if self._gen_total_pub is not None:
354+
total_gen_p = 0.0
355+
total_gen_q = 0.0
356+
for gen_name, gen_obj in self._objects_by_class.get("Generators", {}).items():
357+
powers = gen_obj.GetValue("Powers")
358+
if powers is not None and isinstance(powers, list):
359+
for i in range(0, len(powers), 2):
360+
total_gen_p += powers[i]
361+
if i + 1 < len(powers):
362+
total_gen_q += powers[i + 1]
363+
# Negate: OpenDSS Powers are negative for gen injection
364+
total_gen_p = -total_gen_p
365+
total_gen_q = -total_gen_q
366+
helics.helicsPublicationPublishVector(self._gen_total_pub, [total_gen_p, total_gen_q])
367+
logger.debug(f"Published generator total power: [{total_gen_p:.4f}, {total_gen_q:.4f}]")
336368
return
337369

338370
def request_time_increment(self):

0 commit comments

Comments
 (0)