From a1136923694b987571bcee13496439fac2de3f76 Mon Sep 17 00:00:00 2001 From: Andrew R Fisher Date: Fri, 19 Dec 2025 16:07:03 -0800 Subject: [PATCH 1/3] adding multi-model run capabilities in gridappsd-python --- gridappsd-python-lib/gridappsd/simulation.py | 19 +++- gridappsd-python-lib/tests/test_simulation.py | 88 ++++++++++--------- 2 files changed, 60 insertions(+), 47 deletions(-) diff --git a/gridappsd-python-lib/gridappsd/simulation.py b/gridappsd-python-lib/gridappsd/simulation.py index 8f21815..aa4f66b 100644 --- a/gridappsd-python-lib/gridappsd/simulation.py +++ b/gridappsd-python-lib/gridappsd/simulation.py @@ -27,6 +27,13 @@ def asdict(self): for k, v in self.__dict__.items(): if isinstance(v, ConfigBase): built[k] = v.asdict() + elif isinstance(v, list): + built[k] = [] + for item in v: + if isinstance(item, ConfigBase): + built[k].append(item.asdict()) + else: + built[k].append(item) else: built[k] = v return built @@ -50,15 +57,18 @@ class ModelCreationConfig(ConfigBase): class SimulationArgs(ConfigBase): start_time: str = field(default = "1655321830") duration: str = field(default = "300") - simulator: str = field(default = "GridLAB-D") timestep_frequency: str = field(default = "1000") timestep_increment: str = field(default = "1000") run_realtime: bool = field(default = True) pause_after_measurements: bool = field(default = False) simulation_name: str = field(default = "ieee13nodeckt") - power_flow_solver_method: str = field(default = "NR") + + +@dataclass +class SimulatorArgs(ConfigBase): + simulator: str = field(default = "GridLAB-D") model_creation_config: ModelCreationConfig = field(default_factory = ModelCreationConfig) - + power_flow_solver_method: str = field(default = "NR") # __default_simulation_args__ = SimulationArgs() @@ -95,11 +105,12 @@ class PowerSystemConfig(ConfigBase): Line_name: str GeographicalRegion_name: str = field(default = None) SubGeographicalRegion_name: str = field(default = None) + simulator_config: SimulatorArgs = field(default_factory=SimulatorArgs) @dataclass class SimulationConfig(ConfigBase): - power_system_config: PowerSystemConfig + power_system_configs: List[PowerSystemConfig] = field(default_factory=list) application_configs: List[ApplicationConfig] = field(default_factory=list) simulation_config: SimulationArgs = field(default_factory=SimulationArgs) service_configs: List[ServiceConfig] = field(default_factory=list) diff --git a/gridappsd-python-lib/tests/test_simulation.py b/gridappsd-python-lib/tests/test_simulation.py index b6c9365..2e50f0a 100644 --- a/gridappsd-python-lib/tests/test_simulation.py +++ b/gridappsd-python-lib/tests/test_simulation.py @@ -1,43 +1,45 @@ -# import json -# # from pprint import pprint -# import logging -# import os -# import sys -# import time -# import pytest - -# logging.basicConfig(stream=sys.stdout, level=logging.DEBUG) - -# from gridappsd import GridAPPSD, topics as t -# from gridappsd.simulation import Simulation - -# # The directory containing this file -# HERE = os.path.dirname(__file__) - -# def base_config(): -# data = {"power_system_config":{"SubGeographicalRegion_name":"_ABEB635F-729D-24BF-B8A4-E2EF268D8B9E","GeographicalRegion_name":"_73C512BD-7249-4F50-50DA-D93849B89C43","Line_name":"_49AD8E07-3BF9-A4E2-CB8F-C3722F837B62"},"simulation_config":{"power_flow_solver_method":"NR","duration":120,"simulation_name":"ieee13nodeckt","simulator":"GridLAB-D","start_time":1605418946,"run_realtime":False,"simulation_output":{},"model_creation_config":{"load_scaling_factor":1.0,"triplex":"y","encoding":"u","system_frequency":60,"voltage_multiplier":1.0,"power_unit_conversion":1.0,"unique_names":"y","schedule_name":"ieeezipload","z_fraction":0.0,"i_fraction":1.0,"p_fraction":0.0,"randomize_zipload_fractions":False,"use_houses":False},"simulation_broker_port":51044,"simulation_broker_location":"127.0.0.1"},"application_config":{"applications":[]},"service_configs":[],"test_config":{"randomNum":{"seed":{"value":185213303967438},"nextNextGaussian":0.0,"haveNextNextGaussian":False},"events":[],"testInput":True,"testOutput":True,"appId":"","testId":"1468836560","testType":"simulation_vs_expected","storeMatches":False},"simulation_request_type":"NEW"} -# # with open("{HERE}/simulation_fixtures/13_node_2_min_base.json".format(HERE=HERE)) as fp: -# # data = json.load(fp) -# return data - -# def test_simulation_no_duplicate_measurement_timestamps(gridappsd_client: GridAPPSD): -# num_measurements = 0 -# timestamps = set() - -# def measurement(sim, timestamp, measurement): -# nonlocal num_measurements -# num_measurements += 1 -# assert timestamp not in timestamps -# timestamps.add(timestamp) - -# gapps = gridappsd_client -# sim = Simulation(gapps, base_config()) -# sim.add_onmeasurement_callback(measurement) -# sim.start_simulation() -# sim.run_loop() - -# # did we get a measurement? -# assert num_measurements > 0 - -# # if empty then we know the simulation did not work. -# assert timestamps +import json +import logging +import os +import sys +import time +import pytest +from datetime import datetime, timezone + +logging.basicConfig(stream=sys.stdout, level=logging.DEBUG) + +from gridappsd import GridAPPSD, topics as t +from gridappsd.simulation import Simulation, PowerSystemConfig, SimulationArgs, SimulationConfig + +@pytest.fixture +def createGadObject(): + gad_user = os.environ.get('GRIDAPPSD_USER') + if gad_user is None: + os.environ['GRIDAPPSD_USER'] = 'system' + gad_password = os.environ.get('GRIDAPPSD_PASSWORD') + if gad_password is None: + os.environ['GRIDAPPSD_PASSWORD'] = 'manager' + return GridAPPSD() + + +def test_createSimulations(createGadObject): + gadObj = createGadObject + response = gadObj.query_model_info() + models = response.get("data", {}).get("models", {}) + start_time = int(datetime(year=2025, month=1, day=1, hour=0, minute=0, second=0, microsecond=0, tzinfo=timezone.utc).timestamp()) + simulationArgs = SimulationArgs(start_time=f"{start_time}", + duration="120", + run_realtime=True, + pause_after_measurements=False) + sim_config = SimulationConfig(simulation_config=simulationArgs) + for m in models: + line_name = m.get("modelId") + subregion_name = m.get("subRegionId") + region_name = m.get("regionId") + psc = PowerSystemConfig(Line_name=line_name, + SubGeographicalRegion_name=subregion_name, + GeographicalRegion_name=region_name) + sim_config.power_system_configs.append(psc) + sim_obj = Simulation(gapps=gadObj, run_config=sim_config) + rvStr = json.dumps(sim_obj._run_config, indent=4, sort_keys=True) + pass \ No newline at end of file From efa93b67c5657784c1814c0dc933b54e2a41217b Mon Sep 17 00:00:00 2001 From: Andrew R Fisher Date: Mon, 2 Feb 2026 13:26:00 -0800 Subject: [PATCH 2/3] Adding multi-feeder simulation functionality --- gridappsd-python-lib/tests/test_simulation.py | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/gridappsd-python-lib/tests/test_simulation.py b/gridappsd-python-lib/tests/test_simulation.py index 2e50f0a..e831848 100644 --- a/gridappsd-python-lib/tests/test_simulation.py +++ b/gridappsd-python-lib/tests/test_simulation.py @@ -11,6 +11,8 @@ from gridappsd import GridAPPSD, topics as t from gridappsd.simulation import Simulation, PowerSystemConfig, SimulationArgs, SimulationConfig +simulation_is_complete = False + @pytest.fixture def createGadObject(): gad_user = os.environ.get('GRIDAPPSD_USER') @@ -21,7 +23,6 @@ def createGadObject(): os.environ['GRIDAPPSD_PASSWORD'] = 'manager' return GridAPPSD() - def test_createSimulations(createGadObject): gadObj = createGadObject response = gadObj.query_model_info() @@ -32,7 +33,13 @@ def test_createSimulations(createGadObject): run_realtime=True, pause_after_measurements=False) sim_config = SimulationConfig(simulation_config=simulationArgs) + modelsToRun = [ + "49AD8E07-3BF9-A4E2-CB8F-C3722F837B62", # IEEE 13 Node Test Feeder + "C1C3E687-6FFD-C753-582B-632A27E28507" # IEEE 123 Node Test Feeder + ] for m in models: + if m.get("modelId") not in modelsToRun: + continue line_name = m.get("modelId") subregion_name = m.get("subRegionId") region_name = m.get("regionId") @@ -41,5 +48,12 @@ def test_createSimulations(createGadObject): GeographicalRegion_name=region_name) sim_config.power_system_configs.append(psc) sim_obj = Simulation(gapps=gadObj, run_config=sim_config) - rvStr = json.dumps(sim_obj._run_config, indent=4, sort_keys=True) - pass \ No newline at end of file + def on_simulation_complete(sim): + global simulation_is_complete + simulation_is_complete = True + sim_obj.add_oncomplete_callback(on_simulation_complete) + sim_obj.start_simulation() + while not simulation_is_complete: + time.sleep(1) + print("Simulation completed successfully.") + gadObj.disconnect() \ No newline at end of file From c1fed63c86f290c958df86c1d377e30797b3a7e9 Mon Sep 17 00:00:00 2001 From: Andrew R Fisher Date: Tue, 3 Feb 2026 10:24:28 -0800 Subject: [PATCH 3/3] modified test_simulation.py to assert the number of measurement messages recieved after running the simulation. --- gridappsd-python-lib/tests/test_simulation.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/gridappsd-python-lib/tests/test_simulation.py b/gridappsd-python-lib/tests/test_simulation.py index e831848..4dbdcac 100644 --- a/gridappsd-python-lib/tests/test_simulation.py +++ b/gridappsd-python-lib/tests/test_simulation.py @@ -12,6 +12,7 @@ from gridappsd.simulation import Simulation, PowerSystemConfig, SimulationArgs, SimulationConfig simulation_is_complete = False +measurements_received = 0 @pytest.fixture def createGadObject(): @@ -30,7 +31,7 @@ def test_createSimulations(createGadObject): start_time = int(datetime(year=2025, month=1, day=1, hour=0, minute=0, second=0, microsecond=0, tzinfo=timezone.utc).timestamp()) simulationArgs = SimulationArgs(start_time=f"{start_time}", duration="120", - run_realtime=True, + run_realtime=False, pause_after_measurements=False) sim_config = SimulationConfig(simulation_config=simulationArgs) modelsToRun = [ @@ -48,12 +49,16 @@ def test_createSimulations(createGadObject): GeographicalRegion_name=region_name) sim_config.power_system_configs.append(psc) sim_obj = Simulation(gapps=gadObj, run_config=sim_config) + def on_measurement(sim, ts, m): + global measurements_received + measurements_received += 1 def on_simulation_complete(sim): global simulation_is_complete simulation_is_complete = True + sim_obj.add_onmeasurement_callback(on_measurement) sim_obj.add_oncomplete_callback(on_simulation_complete) sim_obj.start_simulation() while not simulation_is_complete: time.sleep(1) - print("Simulation completed successfully.") + assert measurements_received == 1 gadObj.disconnect() \ No newline at end of file