From 9cba9f809b8220130d6de928a3d18e24386d4d0d Mon Sep 17 00:00:00 2001 From: Joseph McKinsey Date: Tue, 13 Aug 2024 16:36:09 -0600 Subject: [PATCH 1/3] Add modified measuring_federate and sensors publication on feeder --- LocalFeeder/FeederSimulator.py | 26 ++++ LocalFeeder/component_definition.json | 4 + LocalFeeder/sender_cosim.py | 20 ++- measuring_federate/component_definition.json | 6 +- measuring_federate/measuring_federate.py | 13 +- recorder/record_subscription.py | 1 - scenarios/system.json | 121 ++++++++++++------- 7 files changed, 135 insertions(+), 56 deletions(-) diff --git a/LocalFeeder/FeederSimulator.py b/LocalFeeder/FeederSimulator.py index 99caa63..e13ec6d 100644 --- a/LocalFeeder/FeederSimulator.py +++ b/LocalFeeder/FeederSimulator.py @@ -99,6 +99,12 @@ class OpenDSSState(Enum): DISABLED = 7 +class SensorLocations(BaseModel): + voltage_sensors: List[str] + active_sensors: List[str] + reactive_sensors: List[str] + + class FeederSimulator(object): """A simple class that handles publishing the solar forecast.""" @@ -158,6 +164,15 @@ def __init__(self, config: FeederConfig): if self._sensor_location is None: self.create_measurement_lists() + else: + sensor_location = os.path.join( + "sensors", os.path.basename(self._sensor_location) + ) + with open(sensor_location, "r") as fp: + sensor_config = json.load(fp) + self.voltage_sensors = sensor_config + self.active_sensors = sensor_config + self.reactive_sensors = sensor_config self.snapshot_run() assert self._state == OpenDSSState.SNAPSHOT_RUN, f"{self._state}" @@ -282,6 +297,7 @@ def create_measurement_lists( self._AllNodeNames, math.floor(len(self._AllNodeNames) * float(percent_voltage) / 100), ) + self.voltage_sensors = voltage_subset with open(os.path.join("sensors", "voltage_ids.json"), "w") as fp: json.dump(voltage_subset, fp, indent=4) @@ -290,6 +306,7 @@ def create_measurement_lists( self._AllNodeNames, math.floor(len(self._AllNodeNames) * float(percent_real) / 100), ) + self.active_sensors = real_subset with open(os.path.join("sensors", "real_ids.json"), "w") as fp: json.dump(real_subset, fp, indent=4) @@ -298,9 +315,18 @@ def create_measurement_lists( self._AllNodeNames, math.floor(len(self._AllNodeNames) * float(percent_voltage) / 100), ) + self.reactive_sensors = reactive_subset with open(os.path.join("sensors", "reactive_ids.json"), "w") as fp: json.dump(reactive_subset, fp, indent=4) + def get_sensors(self): + """Get sensor locations.""" + return SensorLocations( + voltage_sensors=self.voltage_sensors, + active_sensors=self.active_sensors, + reactive_sensors=self.reactive_sensors, + ) + def get_circuit_name(self): """Get name of current opendss circuit.""" return self._circuit.Name() diff --git a/LocalFeeder/component_definition.json b/LocalFeeder/component_definition.json index bbdc4b3..922671e 100644 --- a/LocalFeeder/component_definition.json +++ b/LocalFeeder/component_definition.json @@ -80,6 +80,10 @@ { "type": "", "port_id": "pv_forecast" + }, + { + "type": "", + "port_id": "sensors" } ] } \ No newline at end of file diff --git a/LocalFeeder/sender_cosim.py b/LocalFeeder/sender_cosim.py index c2112fc..b23b6ea 100644 --- a/LocalFeeder/sender_cosim.py +++ b/LocalFeeder/sender_cosim.py @@ -322,6 +322,9 @@ def go_cosim( pub_pv_forecast = h.helicsFederateRegisterPublication( vfed, "pv_forecast", h.HELICS_DATA_TYPE_STRING, "" ) + pub_sensors = h.helicsFederateRegisterPublication( + vfed, "sensors", h.HELICS_DATA_TYPE_STRING, "" + ) command_set_key = ( "unused/change_commands" @@ -363,15 +366,17 @@ def go_cosim( # Publish the forecasted PV outputs as a list of MeasurementArray logger.info("Evaluating the forecasted PV") forecast_data = sim.forcast_pv(int(config.number_of_timesteps)) - PVforecast = [MeasurementArray(**xarray_to_dict(forecast), - units="kW").json() for forecast in forecast_data] + PVforecast = [ + MeasurementArray(**xarray_to_dict(forecast), units="kW").json() + for forecast in forecast_data + ] pub_pv_forecast.publish(json.dumps(PVforecast)) + pub_sensors.publish(sim.get_sensors().json()) + granted_time = -1 request_time = 0 - initial_timestamp = datetime.strptime( - config.start_date, "%Y-%m-%d %H:%M:%S" - ) + initial_timestamp = datetime.strptime(config.start_date, "%Y-%m-%d %H:%M:%S") while request_time < int(config.number_of_timesteps): granted_time = h.helicsFederateRequestTime(vfed, request_time) @@ -400,7 +405,10 @@ def go_cosim( for pv_set in pv_sets: sim.set_pv_output(pv_set[0].split(".")[1], pv_set[1], pv_set[2]) - current_hour = 24*(floored_timestamp.date() - initial_timestamp.date()).days + floored_timestamp.hour + current_hour = ( + 24 * (floored_timestamp.date() - initial_timestamp.date()).days + + floored_timestamp.hour + ) logger.info( f"Solve at hour {current_hour} second " f"{60*floored_timestamp.minute + floored_timestamp.second}" diff --git a/measuring_federate/component_definition.json b/measuring_federate/component_definition.json index 6812a7a..2d791b3 100644 --- a/measuring_federate/component_definition.json +++ b/measuring_federate/component_definition.json @@ -12,13 +12,17 @@ }, { "type": "", - "port_id": "measurement_file" + "port_id": "measurement_type" } ], "dynamic_inputs": [ { "type": "MeasurementArray", "port_id": "subscription" + }, + { + "type": "SensorDescription", + "port_id": "sensors" } ], "dynamic_outputs": [ diff --git a/measuring_federate/measuring_federate.py b/measuring_federate/measuring_federate.py index e5742ac..cab68ca 100644 --- a/measuring_federate/measuring_federate.py +++ b/measuring_federate/measuring_federate.py @@ -17,7 +17,7 @@ class MeasurementConfig(BaseModel): name: str additive_noise_stddev: float = 0.0 multiplicative_noise_stddev: float = 0.0 - measurement_file: str + measurement_type: str run_freq_time_step: float = 1.0 @@ -94,6 +94,7 @@ def __init__( self.sub_measurement = self.vfed.register_subscription( input_mapping["subscription"], "" ) + self.sub_sensors = self.vfed.register_subscription(input_mapping["sensors"], "") # TODO: find better way to determine what the name of this federate instance is than looking at the subscription self.pub_measurement = self.vfed.register_publication( @@ -102,7 +103,7 @@ def __init__( self.additive_noise_stddev = config.additive_noise_stddev self.multiplicative_noise_stddev = config.multiplicative_noise_stddev - self.measurement_file = config.measurement_file + self.measurement_type = config.measurement_type def transform(self, measurement_array: MeasurementArray, unique_ids): new_array = reindex(measurement_array, unique_ids) @@ -126,9 +127,11 @@ def run(self): else: measurement = MeasurementArray.parse_obj(json_data) - with open(self.measurement_file, "r") as fp: - self.measurement = json.load(fp) - measurement_transformed = self.transform(measurement, self.measurement) + self.sensors = self.sub_sensors.json + assert self.measurement_type in self.sensors + measurement_transformed = self.transform( + measurement, self.sensors[self.measurement_type] + ) logger.debug("measured transformed") logger.debug(measurement_transformed) diff --git a/recorder/record_subscription.py b/recorder/record_subscription.py index abd5e38..517b484 100644 --- a/recorder/record_subscription.py +++ b/recorder/record_subscription.py @@ -2,7 +2,6 @@ import json import logging from datetime import datetime -import time import helics as h import numpy as np diff --git a/scenarios/system.json b/scenarios/system.json index cc5f25d..7ab9042 100644 --- a/scenarios/system.json +++ b/scenarios/system.json @@ -4,8 +4,8 @@ { "name": "feeder", "type": "LocalFeeder", - "host": "feeder", - "container_port": 5678, + "host": "feeder", + "container_port": 5678, "parameters": { "use_smartds": false, "user_uploads_model": false, @@ -18,107 +18,112 @@ "topology_output": "topology.json" } }, - { + { "name": "recorder_voltage_real", "type": "Recorder", - "host": "recorder-voltage-real", - "container_port": 5679, - "parameters": {"feather_filename": "voltage_real.feather", - "csv_filename": "voltage_real.csv" - } + "host": "recorder-voltage-real", + "container_port": 5679, + "parameters": { + "feather_filename": "voltage_real.feather", + "csv_filename": "voltage_real.csv" + } }, { "name": "recorder_voltage_imag", "type": "Recorder", - "host": "recorder-voltage-imag", - "container_port": 5680, - "parameters": {"feather_filename": "voltage_imag.feather", - "csv_filename": "voltage_imag.csv" - } + "host": "recorder-voltage-imag", + "container_port": 5680, + "parameters": { + "feather_filename": "voltage_imag.feather", + "csv_filename": "voltage_imag.csv" + } }, { "name": "recorder_voltage_mag", "type": "Recorder", - "host": "recorder-voltage-mag", - "container_port": 5681, - "parameters": {"feather_filename": "voltage_mag.feather", - "csv_filename": "voltage_mag.csv" - } + "host": "recorder-voltage-mag", + "container_port": 5681, + "parameters": { + "feather_filename": "voltage_mag.feather", + "csv_filename": "voltage_mag.csv" + } }, { "name": "recorder_voltage_angle", "type": "Recorder", - "host": "recorder-voltage-angle", - "container_port": 5682, - "parameters": {"feather_filename": "voltage_angle.feather", - "csv_filename": "voltage_angle.csv" - } + "host": "recorder-voltage-angle", + "container_port": 5682, + "parameters": { + "feather_filename": "voltage_angle.feather", + "csv_filename": "voltage_angle.csv" + } }, { "name": "state_estimator", "type": "StateEstimatorComponent", - "host": "state-estimator", - "container_port": 5683, + "host": "state-estimator", + "container_port": 5683, "parameters": { - "algorithm_parameters": {"tol": 1e-5} + "algorithm_parameters": { + "tol": 1e-5 + } } }, { "name": "sensor_voltage_real", "type": "MeasurementComponent", - "host": "sensor-voltage-real", - "container_port": 5684, + "host": "sensor-voltage-real", + "container_port": 5684, "parameters": { "gaussian_variance": 0.0, "random_percent": 0.0, - "measurement_file": "sensors.json" + "measurement_type": "voltage_sensors" } }, { "name": "sensor_voltage_magnitude", "type": "MeasurementComponent", - "host": "sensor-voltage-magnitude", - "container_port": 5685, + "host": "sensor-voltage-magnitude", + "container_port": 5685, "parameters": { "gaussian_variance": 0.0, "random_percent": 0.0, - "measurement_file": "sensors.json" + "measurement_type": "voltage_sensors" } }, { "name": "sensor_voltage_imaginary", "type": "MeasurementComponent", - "host": "sensor-voltage-imaginary", - "container_port": 5686, + "host": "sensor-voltage-imaginary", + "container_port": 5686, "parameters": { "gaussian_variance": 0.0, "random_percent": 0.0, - "measurement_file": "sensors.json" + "measurement_type": "voltage_sensors" } }, { "name": "sensor_power_real", "type": "MeasurementComponent", - "host": "sensor-power-real", - "container_port": 5687, + "host": "sensor-power-real", + "container_port": 5687, "parameters": { "gaussian_variance": 0.0, "random_percent": 0.0, - "measurement_file": "sensors.json" + "measurement_type": "active_sensors" } }, { "name": "sensor_power_imaginary", "type": "MeasurementComponent", - "host": "sensor-power-imaginary", - "container_port": 5688, + "host": "sensor-power-imaginary", + "container_port": 5688, "parameters": { "gaussian_variance": 0.0, "random_percent": 0.0, - "measurement_file": "sensors.json" + "measurement_type": "reactive_sensors" } } - ], "links": [ { @@ -198,6 +203,36 @@ "source_port": "voltage_mag", "target": "recorder_voltage_mag", "target_port": "subscription" + }, + { + "source": "feeder", + "source_port": "sensors", + "target": "sensor_voltage_real", + "target_port": "sensors" + }, + { + "source": "feeder", + "source_port": "sensors", + "target": "sensor_voltage_imaginary", + "target_port": "sensors" + }, + { + "source": "feeder", + "source_port": "sensors", + "target": "sensor_voltage_magnitude", + "target_port": "sensors" + }, + { + "source": "feeder", + "source_port": "sensors", + "target": "sensor_power_real", + "target_port": "sensors" + }, + { + "source": "feeder", + "source_port": "sensors", + "target": "sensor_power_imaginary", + "target_port": "sensors" } ] -} +} \ No newline at end of file From 8a1ede4369ba8b90d595a53ff68e45458103dac4 Mon Sep 17 00:00:00 2001 From: Joseph McKinsey Date: Tue, 13 Aug 2024 16:42:42 -0600 Subject: [PATCH 2/3] Make subscription to sensors optional. Add backwards compatability --- measuring_federate/component_definition.json | 4 ++++ measuring_federate/measuring_federate.py | 24 ++++++++++++++------ 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/measuring_federate/component_definition.json b/measuring_federate/component_definition.json index 2d791b3..dcc345c 100644 --- a/measuring_federate/component_definition.json +++ b/measuring_federate/component_definition.json @@ -13,6 +13,10 @@ { "type": "", "port_id": "measurement_type" + }, + { + "type": "", + "port_id": "measurement_file" } ], "dynamic_inputs": [ diff --git a/measuring_federate/measuring_federate.py b/measuring_federate/measuring_federate.py index cab68ca..1c03540 100644 --- a/measuring_federate/measuring_federate.py +++ b/measuring_federate/measuring_federate.py @@ -17,7 +17,8 @@ class MeasurementConfig(BaseModel): name: str additive_noise_stddev: float = 0.0 multiplicative_noise_stddev: float = 0.0 - measurement_type: str + measurement_type: str | None = None + measurement_file: str | None = None run_freq_time_step: float = 1.0 @@ -94,7 +95,10 @@ def __init__( self.sub_measurement = self.vfed.register_subscription( input_mapping["subscription"], "" ) - self.sub_sensors = self.vfed.register_subscription(input_mapping["sensors"], "") + if "sensors" in input_mapping: + self.sub_sensors = self.vfed.register_subscription( + input_mapping["sensors"], "" + ) # TODO: find better way to determine what the name of this federate instance is than looking at the subscription self.pub_measurement = self.vfed.register_publication( @@ -104,6 +108,7 @@ def __init__( self.additive_noise_stddev = config.additive_noise_stddev self.multiplicative_noise_stddev = config.multiplicative_noise_stddev self.measurement_type = config.measurement_type + self.measurement_file = config.measurement_file def transform(self, measurement_array: MeasurementArray, unique_ids): new_array = reindex(measurement_array, unique_ids) @@ -127,11 +132,16 @@ def run(self): else: measurement = MeasurementArray.parse_obj(json_data) - self.sensors = self.sub_sensors.json - assert self.measurement_type in self.sensors - measurement_transformed = self.transform( - measurement, self.sensors[self.measurement_type] - ) + if self.measurement_type is not None: + self.sensors = self.sub_sensors.json + assert self.measurement_type in self.sensors + measurement_transformed = self.transform( + measurement, self.sensors[self.measurement_type] + ) + else: + with open(self.measurement_file, "r") as fp: + self.measurement = json.load(fp) + measurement_transformed = self.transform(measurement, self.measurement) logger.debug("measured transformed") logger.debug(measurement_transformed) From 859505146f187639335338002a52b912c15ff12b Mon Sep 17 00:00:00 2001 From: Joseph McKinsey Date: Tue, 13 Aug 2024 16:54:58 -0600 Subject: [PATCH 3/3] Fix omoo system and guard against sensor location not existing --- LocalFeeder/FeederSimulator.py | 15 ++++++++++----- scenarios/omoo_system.json | 3 +-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/LocalFeeder/FeederSimulator.py b/LocalFeeder/FeederSimulator.py index e13ec6d..ecc3f52 100644 --- a/LocalFeeder/FeederSimulator.py +++ b/LocalFeeder/FeederSimulator.py @@ -168,11 +168,16 @@ def __init__(self, config: FeederConfig): sensor_location = os.path.join( "sensors", os.path.basename(self._sensor_location) ) - with open(sensor_location, "r") as fp: - sensor_config = json.load(fp) - self.voltage_sensors = sensor_config - self.active_sensors = sensor_config - self.reactive_sensors = sensor_config + if os.path.exists(sensor_location): + with open(sensor_location, "r") as fp: + sensor_config = json.load(fp) + self.voltage_sensors = sensor_config + self.active_sensors = sensor_config + self.reactive_sensors = sensor_config + else: + self.voltage_sensors = [] + self.active_sensors = [] + self.reactive_sensors = [] self.snapshot_run() assert self._state == OpenDSSState.SNAPSHOT_RUN, f"{self._state}" diff --git a/scenarios/omoo_system.json b/scenarios/omoo_system.json index 92309d7..5d2d16b 100644 --- a/scenarios/omoo_system.json +++ b/scenarios/omoo_system.json @@ -49,7 +49,6 @@ "use_smartds": false, "profile_location": "gadal_ieee123/profiles", "opendss_location": "gadal_ieee123/qsts", - "sensor_location": "gadal_ieee123/sensors.json", "existing_feeder_file": "opendss/master.dss", "start_date": "2017-01-01 00:00:00", "number_of_timesteps": 96, @@ -65,7 +64,7 @@ "use_smartds": false, "profile_location": "gadal_ieee123/profiles", "opendss_location": "gadal_ieee123/qsts", - "sensor_location": "gadal_ieee123/sensors.json", + "sensor_location": "sensors.json", "existing_feeder_file": "opendss/master.dss", "start_date": "2017-01-01 00:00:00", "number_of_timesteps": 96,