diff --git a/.github/workflows/docker-test.yml b/.github/workflows/docker-test.yml index d834fa5..dc1a6ba 100644 --- a/.github/workflows/docker-test.yml +++ b/.github/workflows/docker-test.yml @@ -33,7 +33,7 @@ jobs: shell: bash -l {0} run: | pip install matplotlib pyarrow numpy matplotlib pandas - pip install oedisi==1.0.0 + pip install oedisi==1.2.1 python post_analysis.py outputs_build - name: Archive logs diff --git a/.github/workflows/test-omoo.yml b/.github/workflows/test-omoo.yml new file mode 100644 index 0000000..d3a0791 --- /dev/null +++ b/.github/workflows/test-omoo.yml @@ -0,0 +1,46 @@ +name: RunOMOO + +on: [push] +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + python-version: ['3.10'] + #include: + #- os: ubuntu-latest + #python-version: 3.10 + + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: conda-incubator/setup-miniconda@v2 + with: + auto-update-conda: true + python-version: ${{ matrix.python-version }} + - name: Install python dependencies + shell: bash -l {0} + run: | + pip install -r requirements.txt + pip install plotille + pip install click + - name: Run example + shell: bash -l {0} + run: | + git clone https://github.com/openEDI/oedisi-ieee123 + mv oedisi-ieee123/profiles LocalFeeder/profiles + mv oedisi-ieee123/qsts LocalFeeder/opendss + # Change every kVA=50 and Pmpp=50 to kVA=200 and Pmpp=200 in LocalFeeder/opendss/IEEE123Pv.dss + sed -i 's/kVA=50/kVA=200/g; s/Pmpp=50/Pmpp=200/g' LocalFeeder/opendss/IEEE123Pv.dss + oedisi build --system scenarios/omoo_system.json + oedisi run + python opf_analysis.py + - name: Archive logs + uses: actions/upload-artifact@v2 + if: always() + with: + name: test_logs + path: | + build/*.log diff --git a/Dockerfile b/Dockerfile index fbfc6a2..f43362a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,6 +13,7 @@ COPY README.md . COPY measuring_federate measuring_federate COPY wls_federate wls_federate COPY recorder recorder +COPY omoo_federate omoo_federate RUN mkdir -p outputs build diff --git a/LocalFeeder/FeederSimulator.py b/LocalFeeder/FeederSimulator.py index 219fb20..187c3de 100644 --- a/LocalFeeder/FeederSimulator.py +++ b/LocalFeeder/FeederSimulator.py @@ -1,4 +1,5 @@ """Core class to abstract OpenDSS into Feeder class.""" + import json import logging import math @@ -15,10 +16,20 @@ import xarray as xr from botocore import UNSIGNED from botocore.config import Config -from dss_functions import (get_capacitors, get_generators, get_loads, - get_pvsystems, get_voltages) -from oedisi.types.data_types import (Command, InverterControl, - InverterControlMode) +from dss_functions import ( + get_capacitors, + get_generators, + get_loads, + get_pvsystems, + get_voltages, +) + +from oedisi.types.data_types import ( + Command, + InverterControl, + InverterControlMode, + IncidenceList, +) from pydantic import BaseModel from scipy.sparse import coo_matrix, csc_matrix @@ -53,11 +64,12 @@ class FeederConfig(BaseModel): existing_feeder_file: Optional[str] = None sensor_location: Optional[str] = None start_date: str - number_of_timesteps: float + number_of_timesteps: int run_freq_sec: float = 15 * 60 start_time_index: int = 0 topology_output: str = "topology.json" use_sparse_admittance: bool = False + tap_setting: Optional[int] = None class FeederMapping(BaseModel): @@ -115,6 +127,8 @@ def __init__(self, config: FeederConfig): self._number_of_timesteps = config.number_of_timesteps self._vmult = 0.001 + self.tap_setting = config.tap_setting + self._simulation_time_step = "15m" if config.existing_feeder_file is None: if self._use_smartds: @@ -286,12 +300,15 @@ def load_feeder(self): self._pvsystems = set() for PV in get_pvsystems(dss): self._pvsystems.add("PVSystem." + PV["name"]) + + if self.tap_setting is not None: + # Doesn't work with AutoTrans or 3-winding transformers. + dss.Text.Command(f"batchedit transformer..* wdg=2 tap={self.tap_setting}") self._state = OpenDSSState.LOADED def disable_elements(self): """Disable most elements. Used in disabled_run.""" assert self._state != OpenDSSState.UNLOADED, f"{self._state}" - # dss.Text.Command("batchedit transformer..* wdg=2 tap=1") dss.Text.Command("batchedit regcontrol..* enabled=false") dss.Text.Command("batchedit vsource..* enabled=false") dss.Text.Command("batchedit isource..* enabled=false") @@ -580,9 +597,9 @@ def _get_voltages(self): name_voltage_dict = get_voltages(self._circuit) res_feeder_voltages = np.zeros((len(self._AllNodeNames)), dtype=np.complex_) for voltage_name in name_voltage_dict.keys(): - res_feeder_voltages[ - self._name_index_dict[voltage_name] - ] = name_voltage_dict[voltage_name] + res_feeder_voltages[self._name_index_dict[voltage_name]] = ( + name_voltage_dict[voltage_name] + ) return xr.DataArray( res_feeder_voltages, {"ids": list(name_voltage_dict.keys())} @@ -701,7 +718,8 @@ def set_properties_to_inverter(self, inverter: str, inv_control: InverterControl ) if inv_control.vwcontrol is not None: vw_curve = self.create_xy_curve( - inv_control.vwcontrol.voltage, inv_control.vwcontrol.power_response, + inv_control.vwcontrol.voltage, + inv_control.vwcontrol.power_response, ) dss.Text.Command(f"{inverter}.voltwatt_curve={vw_curve.split('.')[1]}") dss.Text.Command( @@ -713,49 +731,51 @@ def set_properties_to_inverter(self, inverter: str, inv_control: InverterControl dss.Text.Command(f"{inverter}.Mode = {inv_control.mode.value}") def set_pv_output(self, pv_system, p, q): - """Sets the P and Q values for a PV system in OpenDSS - """ - + """Sets the P and Q values for a PV system in OpenDSS""" max_pv = self.get_max_pv_available(pv_system) - #pf = q / ((p**2 + q **2)**0.5) + # pf = q / ((p**2 + q **2)**0.5) obj_name = f"PVSystem.{pv_system}" - if max_pv <=0 or p == 0: + if max_pv <= 0 or p == 0: Warning("Maximum PV Value is 0") obj_val = 100 - q=0 + q = 0 elif p < max_pv: - obj_val = p/float(max_pv) *100 + obj_val = p / float(max_pv) * 100 else: obj_val = 100 - ratio = float(max_pv)/p - q = q*ratio #adjust q value to that it matches the kw output - command = [Command(obj_name=obj_name,obj_property="%Pmpp",val=str(obj_val)), Command(obj_name=obj_name,obj_property="kvar",val=str(q)), Command(obj_name=obj_name,obj_property="%Cutout", val="0"), Command(obj_name=obj_name,obj_property="%Cutin", val="0")] + ratio = float(max_pv) / p + q = q * ratio # adjust q value to that it matches the kw output + command = [ + Command(obj_name=obj_name, obj_property="%Pmpp", val=str(obj_val)), + Command(obj_name=obj_name, obj_property="kvar", val=str(q)), + Command(obj_name=obj_name, obj_property="%Cutout", val="0"), + Command(obj_name=obj_name, obj_property="%Cutin", val="0"), + ] self.change_obj(command) - - def get_pv_output(self,pv_system): - dss.PVsystems.First() - while True: - if dss.PVsystems.Name() == pv_system: - kw = dss.PVsystems.kW() - kvar = dss.PVsystems.kvar() - if not dss.PVsystems.Next() > 0: - break - return kw,kvar - - def get_max_pv_available(self,pv_system): - dss.PVsystems.First() + + def get_max_pv_available(self, pv_system): irradiance = None pmpp = None - while True: + flag = dss.PVsystems.First() + while flag: if dss.PVsystems.Name() == pv_system: - irradiance = dss.PVsystems.Irradiance() + irradiance = dss.PVsystems.IrradianceNow() pmpp = dss.PVsystems.Pmpp() - if not dss.PVsystems.Next() > 0: - break + flag = dss.PVsystems.Next() if irradiance is None or pmpp is None: raise ValueError(f"Irradiance or PMPP not found for {pv_system}") - return irradiance*pmpp + return irradiance * pmpp + + def get_available_pv(self): + pv_names = [] + powers = [] + flag = dss.PVsystems.First() + while flag: + pv_names.append(f"PVSystem.{dss.PVsystems.Name()}") + powers.append(dss.PVsystems.Pmpp() * dss.PVsystems.IrradianceNow()) + flag = dss.PVsystems.Next() + return xr.DataArray(powers, coords={"ids": pv_names}) def apply_inverter_control(self, inv_control: InverterControl): """Apply inverter control to OpenDSS. @@ -801,3 +821,50 @@ def apply_inverter_control(self, inv_control: InverterControl): self.set_properties_to_inverter(inverter, inv_control) return inverter + + def get_incidences(self) -> IncidenceList: + """Get Incidence from line names to buses.""" + assert self._state != OpenDSSState.UNLOADED, f"{self._state}" + from_list = [] + to_list = [] + equipment_ids = [] + equipment_types = [] + for line in dss.Lines.AllNames(): + dss.Circuit.SetActiveElement("Line." + line) + names = dss.CktElement.BusNames() + if len(names) != 2: + bus_names = map(lambda x: x.split(".")[0], names) + # dicts are insert-ordered in >=3.7 + names = list(dict.fromkeys(bus_names)) + if len(names) != 2: + logging.info( + f"Line {line} has {len(names)} terminals, skipping in incidence matrix" + ) + continue + from_bus, to_bus = names + from_list.append(from_bus.upper()) + to_list.append(to_bus.upper()) + equipment_ids.append(line) + equipment_types.append("Line") + for transformer in dss.Transformers.AllNames(): + dss.Circuit.SetActiveElement("Transformer." + transformer) + names = dss.CktElement.BusNames() + if len(names) != 2: + bus_names = map(lambda x: x.split(".")[0], names) + names = list(dict.fromkeys(bus_names)) + if len(names) != 2: + logging.info( + f"Transformer {transformer} has {len(names)} terminals, skipping in incidence matrix" + ) + continue + from_bus, to_bus = names + from_list.append(from_bus.upper()) + to_list.append(to_bus.upper()) + equipment_ids.append(transformer) + equipment_types.append("Transformer") + return IncidenceList( + from_equipment=from_list, + to_equipment=to_list, + ids=equipment_ids, + equipment_types=equipment_types, + ) diff --git a/LocalFeeder/component_definition.json b/LocalFeeder/component_definition.json index 2c88254..133ddf2 100644 --- a/LocalFeeder/component_definition.json +++ b/LocalFeeder/component_definition.json @@ -2,24 +2,84 @@ "directory": "LocalFeeder", "execute_function": "python sender_cosim.py", "static_inputs": [ - {"type": "", "port_id": "feeder_file"}, - {"type": "", "port_id": "start_date"}, - {"type": "", "port_id": "run_freq_sec"}, - {"type": "", "port_id": "number_of_timesteps"}, - {"type": "", "port_id": "start_time_index"} + { + "type": "", + "port_id": "feeder_file" + }, + { + "type": "", + "port_id": "start_date" + }, + { + "type": "", + "port_id": "run_freq_sec" + }, + { + "type": "", + "port_id": "number_of_timesteps" + }, + { + "type": "", + "port_id": "start_time_index" + }, + { + "type": "", + "port_id": "tap_setting" + } ], "dynamic_inputs": [ - {"type": "CommandList", "port_id": "change_commands", "optional": true}, - {"type": "InverterControlList", "port_id": "inv_control", "optional": true} + { + "type": "", + "port_id": "pv_set", + "optional": true + }, + { + "type": "CommandList", + "port_id": "change_commands", + "optional": true + }, + { + "type": "InverterControlList", + "port_id": "inv_control", + "optional": true + } ], "dynamic_outputs": [ - {"type": "VoltagesMagnitude", "port_id": "voltages_magnitude"}, - {"type": "VoltagesReal", "port_id": "voltages_real"}, - {"type": "VoltagesImaginary", "port_id": "voltages_imag"}, - {"type": "PowersReal", "port_id": "powers_real"}, - {"type": "PowersImaginary", "port_id": "powers_imag"}, - {"type": "Topology", "port_id": "topology"}, - {"type": "Injection", "port_id": "injections"}, - {"type": "", "port_id": "load_y_matrix"} + { + "type": "VoltagesMagnitude", + "port_id": "voltages_magnitude" + }, + { + "type": "VoltagesReal", + "port_id": "voltages_real" + }, + { + "type": "VoltagesImaginary", + "port_id": "voltages_imag" + }, + { + "type": "PowersReal", + "port_id": "powers_real" + }, + { + "type": "PowersImaginary", + "port_id": "powers_imag" + }, + { + "type": "Topology", + "port_id": "topology" + }, + { + "type": "Injection", + "port_id": "injections" + }, + { + "type": "", + "port_id": "load_y_matrix" + }, + { + "type": "PowersReal", + "port_id": "available_power" + } ] -} +} \ No newline at end of file diff --git a/LocalFeeder/sender_cosim.py b/LocalFeeder/sender_cosim.py index 07960a5..c05c6b1 100644 --- a/LocalFeeder/sender_cosim.py +++ b/LocalFeeder/sender_cosim.py @@ -1,4 +1,5 @@ """HELICS wrapper for OpenDSS feeder simulation.""" + import json import logging from dataclasses import dataclass @@ -11,13 +12,22 @@ import xarray as xr from FeederSimulator import FeederConfig, FeederSimulator from oedisi.types.common import BrokerConfig -from oedisi.types.data_types import (AdmittanceMatrix, AdmittanceSparse, - CommandList, EquipmentNodeArray, - Injection, InverterControlList, - MeasurementArray, PowersImaginary, - PowersReal, Topology, VoltagesAngle, - VoltagesImaginary, VoltagesMagnitude, - VoltagesReal) +from oedisi.types.data_types import ( + AdmittanceMatrix, + AdmittanceSparse, + CommandList, + EquipmentNodeArray, + Injection, + InverterControlList, + MeasurementArray, + PowersImaginary, + PowersReal, + Topology, + VoltagesAngle, + VoltagesImaginary, + VoltagesMagnitude, + VoltagesReal, +) from scipy.sparse import coo_matrix logger = logging.getLogger(__name__) @@ -135,6 +145,7 @@ class InitialData: def get_initial_data(sim: FeederSimulator, config: FeederConfig): """Get and calculate InitialData from simulation.""" + incidences = sim.get_incidences() Y = sim.get_y_matrix() unique_ids = sim._AllNodeNames @@ -178,6 +189,7 @@ def get_initial_data(sim: FeederSimulator, config: FeederConfig): injections=injections, base_voltage_magnitudes=base_voltagemagnitude, slack_bus=slack_ids, + incidences=incidences, ) return InitialData(Y=Y, topology=topology) @@ -215,7 +227,12 @@ def get_current_data(sim: FeederSimulator, Y): power_real, power_imaginary = get_powers(-PQ_load, -PQ_PV, -PQ_gen, -PQ_cap) injections = Injection(power_real=power_real, power_imaginary=power_imaginary) - ids = xr.DataArray(sim._AllNodeNames, coords={"ids": sim._AllNodeNames}) + ids = xr.DataArray( + sim._AllNodeNames, + coords={ + "ids": sim._AllNodeNames, + }, + ) PQ_injections_all = ( agg_to_ids(PQ_load, ids) + agg_to_ids(PQ_PV, ids) @@ -296,6 +313,9 @@ def go_cosim( pub_injections = h.helicsFederateRegisterPublication( vfed, "injections", h.HELICS_DATA_TYPE_STRING, "" ) + pub_available_power = h.helicsFederateRegisterPublication( + vfed, "available_power", h.HELICS_DATA_TYPE_STRING, "" + ) pub_load_y_matrix = h.helicsFederateRegisterPublication( vfed, "load_y_matrix", h.HELICS_DATA_TYPE_STRING, "" ) @@ -307,7 +327,7 @@ def go_cosim( ) sub_command_set = vfed.register_subscription(command_set_key, "") sub_command_set.set_default("[]") - sub_command_set.option["CONNECTION_OPTIONAL"] = 1 + sub_command_set.option["CONNECTION_OPTIONAL"] = True inv_control_key = ( "unused/inv_control" @@ -316,7 +336,15 @@ def go_cosim( ) sub_invcontrol = vfed.register_subscription(inv_control_key, "") sub_invcontrol.set_default("[]") - sub_invcontrol.option["CONNECTION_OPTIONAL"] = 1 + sub_invcontrol.option["CONNECTION_OPTIONAL"] = True + + pv_set_key = ( + "unused/pv_set" if "pv_set" not in input_mapping else input_mapping["pv_set"] + ) + + sub_pv_set = vfed.register_subscription(pv_set_key, "") + sub_pv_set.set_default("[]") + sub_pv_set.option["CONNECTION_OPTIONAL"] = True h.helicsFederateEnterExecutingMode(vfed) initial_data = get_initial_data(sim, config) @@ -327,8 +355,15 @@ def go_cosim( pub_topology.publish(initial_data.topology.json()) granted_time = -1 - for request_time in range(0, int(config.number_of_timesteps)): + request_time = 0 + + while request_time < int(config.number_of_timesteps): granted_time = h.helicsFederateRequestTime(vfed, request_time) + assert ( + granted_time <= request_time + deltat + ), f"granted_time: {granted_time} past {request_time}" + if granted_time >= request_time - deltat: + request_time += 1 current_index = int(granted_time) # floors current_timestamp = datetime.strptime( @@ -345,6 +380,10 @@ def go_cosim( for inv_control in inverter_controls.__root__: sim.apply_inverter_control(inv_control) + pv_sets = sub_pv_set.json + for pv_set in pv_sets: + sim.set_pv_output(pv_set[0].split(".")[1], pv_set[1], pv_set[2]) + logger.info( f"Solve at hour {floored_timestamp.hour} second " f"{60*floored_timestamp.minute + floored_timestamp.second}" @@ -381,7 +420,8 @@ def go_cosim( voltage_magnitudes = np.abs(current_data.feeder_voltages) pub_voltages_magnitude.publish( VoltagesMagnitude( - **xarray_to_dict(voltage_magnitudes), time=current_timestamp, + **xarray_to_dict(voltage_magnitudes), + time=current_timestamp, ).json() ) pub_voltages_real.publish( @@ -409,6 +449,13 @@ def go_cosim( ).json() ) pub_injections.publish(current_data.injections.json()) + pub_available_power.publish( + MeasurementArray( + **xarray_to_dict(sim.get_available_pv()), + time=current_timestamp, + units="kWA", + ).json() + ) if config.use_sparse_admittance: pub_load_y_matrix.publish( diff --git a/LocalFeeder/tests/test_feeder.py b/LocalFeeder/tests/test_feeder.py index ae15a29..4afffb6 100644 --- a/LocalFeeder/tests/test_feeder.py +++ b/LocalFeeder/tests/test_feeder.py @@ -7,8 +7,13 @@ import plotille import pytest import xarray as xr -from oedisi.types.data_types import (EquipmentNodeArray, InverterControl, - InverterControlMode, VVControl, VWControl) +from oedisi.types.data_types import ( + EquipmentNodeArray, + InverterControl, + InverterControlMode, + VVControl, + VWControl, +) sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) @@ -585,6 +590,7 @@ def test_inv_combined_control(federate_config): np.sum(np.abs(new_voltages.loc["87.3"] - old_voltages.loc["87.3"])) ) > 0.01 * float(np.abs(old_voltages.loc["87.3"])) + def test_pv_setpoints(federate_config): logging.info("Loading sim") sim = FeederSimulator.FeederSimulator(federate_config) @@ -593,32 +599,62 @@ def test_pv_setpoints(federate_config): FeederSimulator.Command( obj_name="PVSystem.113", obj_property="irradiance", - val="1", + val="1", ), FeederSimulator.Command( obj_name="PVSystem.113", obj_property="Pmpp", - val="40", - ) - + val="40", + ), ] ) - sim.set_pv_output("113",20,5) - kw,kvar = sim.get_pv_output("113") - assert kw == 20 - assert kvar == 5 + sim.set_pv_output("113", 20, 5) + sim.snapshot_run() + power = ( + -sim.get_PQs_pv(static=True) + .groupby("equipment_ids")["PVSystem.113"] + .sum() + .item() + ) + assert np.isclose(power.real, 20), f"Real power is {power.real}" + assert np.isclose(power.imag, 5), f"Reactive power is {power.imag}" + sim.change_obj( [ FeederSimulator.Command( obj_name="PVSystem.113", - obj_property="irradiance", - val="0.2", + obj_property="Pmpp", + val="8", ) ] ) - sim.set_pv_output("113",20,5) - kw,kvar = sim.get_pv_output("113") - assert kw == 8 - assert kvar == 2 + sim.snapshot_run() + sim.set_pv_output("113", 20, 5) + sim.snapshot_run() + power = ( + -sim.get_PQs_pv(static=True) + .groupby("equipment_ids")["PVSystem.113"] + .sum() + .item() + ) + assert np.isclose(power.real, 8), f"Real power is {power.real}" + assert np.isclose(power.imag, 2), f"Reactive power is {power.imag}" +def test_incidence_matrix(federate_config): + sim = FeederSimulator.FeederSimulator(federate_config) + incidences = sim.get_incidences() + + core_bus_names = set(name.split(".")[0] for name in sim._AllNodeNames) + assert all( + bus_name.split(".")[0] in core_bus_names + for bus_name in incidences.from_equipment + ), f"Could not find the following buses: {list(filter(lambda bus_name: bus_name.split('.')[0] not in core_bus_names, incidences.from_equipment))}" + assert all( + bus_name.split(".")[0] in core_bus_names for bus_name in incidences.to_equipment + ), f"Could not find the following buses: {list(filter(lambda bus_name: bus_name.split('.')[0] not in core_bus_names, incidences.to_equipment))}" + assert len(incidences.from_equipment) == len(incidences.to_equipment) + if incidences.equipment_type is not None: + assert len(incidences.equipment_type) == len(incidences.from_equipment) + if incidences.ids is not None: + assert len(incidences.ids) == len(incidences.from_equipment) diff --git a/README.md b/README.md index 3bcbc77..4e4f5bf 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ state estimation, and distributed OPF. # Install and Running Locally 1. To run the simulation, you'll need several libraries such as OpenDSSDirect.py and pyarrow. -``` +```bash pip install -r requirements.txt ``` 2. Run `oedisi build --system scenarios/docker_system.json` to initialize the system @@ -112,12 +112,12 @@ for each component with the right configuration. # Docker Container -``` +```bash docker build -t oedisi-example:0.0.0 . ``` To get a docker volume pointed at the right place locally, we have to run more commands -``` +```bash mkdir outputs_build docker volume create --name oedisi_output --opt type=none --opt device=$(PWD)/outputs_build --opt o=bind ``` @@ -126,8 +126,16 @@ If `pwd` is unavailable on your system, then you must specify the exact path. On being `/c/Users/.../outputs_builds/`. You must use forward slashes. Then we can run the docker image: -``` +```bash docker run --rm --mount source=oedisi_output,target=/simulation/outputs oedisi-example:0.0.0 ``` You can omit the docker volume parts as well as `--mount` if you do not care about the exact outputs. + +## Docker Containers on M1 or M2 + +Since HELICS does not have linux ARM builds, you have to run with + +```bash +export DOCKER_DEFAULT_PLATFORM=linux/amd64 +``` diff --git a/broker/server.py b/broker/server.py index e7fae10..662e7b5 100644 --- a/broker/server.py +++ b/broker/server.py @@ -13,14 +13,13 @@ import yaml import json import os - import json from oedisi.componentframework.system_configuration import WiringDiagram, ComponentStruct from oedisi.types.common import ServerReply, HeathCheck +from oedisi.tools.broker_utils import get_time_data logger = logging.getLogger('uvicorn.error') -#logger = logging.getLogger(__name__) app = FastAPI() @@ -173,7 +172,10 @@ def run_simulation(): initstring = f"-f {len(component_map)-1} --name=mainbroker --loglevel=trace --local_interface={broker_ip} --localport=23404" logger.info(f"Broker initaialization string: {initstring}") broker = h.helicsCreateBroker("zmq", "", initstring) - logger.info(broker) + + app.state.broker = broker + logging.info(broker) + isconnected = h.helicsBrokerIsConnected(broker) logger.info(f"Broker connected: {isconnected}") logger.info(str(component_map)) @@ -210,7 +212,7 @@ async def run_feeder(background_tasks: BackgroundTasks): except Exception as e: err = traceback.format_exc() raise HTTPException(status_code=404, detail=str(err)) - + @app.post("/configure") async def configure(wiring_diagram:WiringDiagram): global WIRING_DIAGRAM @@ -233,6 +235,21 @@ async def configure(wiring_diagram:WiringDiagram): assert r.status_code==200, f"POST request to update configuration failed for url - {url}" return JSONResponse(ServerReply(detail="Sucessfully updated config files for all containers").dict(), 200) +@app.get("/status/") +async def status(): + try: + name_2_timedata = {} + connected = h.helicsBrokerIsConnected(app.state.broker) + if connected: + for time_data in get_time_data(app.state.broker): + if (time_data.name not in name_2_timedata) or ( + name_2_timedata[time_data.name] != time_data + ): + name_2_timedata[time_data.name] = time_data + return {"connected": connected, "timedata": name_2_timedata, "error": False} + except AttributeError as e: + return {"reply": str(e), "error": True} + if __name__ == "__main__": uvicorn.run(app, host="0.0.0.0", port=int(os.environ['PORT'])) # test_function() diff --git a/components.json b/components.json index 599dfdf..c55d404 100644 --- a/components.json +++ b/components.json @@ -2,5 +2,6 @@ "Recorder": "recorder/component_definition.json", "LocalFeeder": "LocalFeeder/component_definition.json", "MeasurementComponent": "measuring_federate/component_definition.json", - "StateEstimatorComponent": "wls_federate/component_definition.json" -} + "StateEstimatorComponent": "wls_federate/component_definition.json", + "OMOOComponent": "omoo_federate/component_definition.json" +} \ No newline at end of file diff --git a/measuring_federate/requirements.txt b/measuring_federate/requirements.txt index 6a0e3f2..1a7e631 100644 --- a/measuring_federate/requirements.txt +++ b/measuring_federate/requirements.txt @@ -8,4 +8,5 @@ fastapi uvicorn requests grequests -oedisi \ No newline at end of file +oedisi + diff --git a/omoo_federate/OMOO.py b/omoo_federate/OMOO.py index d31c4f9..df7e35c 100644 --- a/omoo_federate/OMOO.py +++ b/omoo_federate/OMOO.py @@ -7,6 +7,7 @@ and `call_H` calculates a jacobian. Then `scipy.optimize.least_squares` is used to solve. """ + import cmath import warnings import logging @@ -32,16 +33,32 @@ PowersReal, PowersImaginary, AdmittanceSparse, - Command + Command, ) from scipy.sparse import csc_matrix, coo_matrix, diags, vstack, hstack from scipy.sparse.linalg import svds, inv +import xarray as xr logger = logging.getLogger(__name__) logger.addHandler(logging.StreamHandler()) logger.setLevel(logging.DEBUG) +def eqarray_to_xarray(eq: EquipmentNodeArray): + return xr.DataArray( + eq.values, + dims=("eqnode",), + coords={ + "equipment_ids": ("eqnode", eq.equipment_ids), + "ids": ("eqnode", eq.ids), + }, + ) + + +def measurement_to_xarray(eq: MeasurementArray): + return xr.DataArray(eq.values, coords={"ids": eq.ids}) + + def matrix_to_numpy(admittance: List[List[Complex]]): "Convert list of list of our Complex type into a numpy matrix" return np.array([[x[0] + 1j * x[1] for x in row] for row in admittance]) @@ -108,7 +125,7 @@ def Proj_inverter(xt, yt, Ux, Sx): # Sx = max capacity power if Ux - Sx > 0: # If avaialble power is larger than capacity, use capacity Ux = Sx - qx = np.sqrt(Sx ** 2 - Ux ** 2) + qx = np.sqrt(Sx**2 - Ux**2) theta = np.arcsin(qx / Sx) if xt < 0: @@ -116,24 +133,24 @@ def Proj_inverter(xt, yt, Ux, Sx): print(xt) try: - theta_t = np.arcsin(yt / np.sqrt(xt ** 2 + yt ** 2 + 1e-8)) + theta_t = np.arcsin(yt / np.sqrt(xt**2 + yt**2 + 1e-8)) except: theta_t = 0 - if xt ** 2 + yt ** 2 <= Sx ** 2 and xt <= Ux: + if xt**2 + yt**2 <= Sx**2 and xt <= Ux: # No violation x2 = xt y2 = yt else: # theta_t is current p, q angle if abs(theta_t) > theta: - x2 = xt * Sx / cmath.sqrt((xt ** 2 + yt ** 2)) - y2 = yt * Sx / cmath.sqrt((xt ** 2 + yt ** 2)) + x2 = xt * Sx / cmath.sqrt((xt**2 + yt**2)) + y2 = yt * Sx / cmath.sqrt((xt**2 + yt**2)) if abs(theta_t) <= theta: - if np.abs(yt) > cmath.sqrt(Sx ** 2 - Ux ** 2): + if np.abs(yt) > cmath.sqrt(Sx**2 - Ux**2): x2 = Ux - y2 = cmath.sqrt(Sx ** 2 - Ux ** 2) + y2 = cmath.sqrt(Sx**2 - Ux**2) else: x2 = Ux y2 = yt @@ -203,8 +220,8 @@ def cost_fun( Pck_v = pv_avail_v - P # Curtailment # cost = P curtailment + Q generation + deviation for P and Q tot_cost_v += ( - cost_P * Pck_v ** 2 - + cost_Q * Q ** 2 + cost_P * Pck_v**2 + + cost_Q * Q**2 + q_dev * (Q_set_v - Qk_v) ** 2 + p_dev * (P_set_v - Pk_v) ** 2 ) @@ -280,11 +297,10 @@ class UnitSystem(str, Enum): PER_UNIT = "PER_UNIT" - class OMOOParameters(BaseModel): - Vmax: float = 1.05 + 0.005 # Upper limit - Vmin_act: float = 0.945 # + 0.005 - Vmin: float = 0.945 + 0.005 + 0.002 # Lower limit\ + Vmax: float = 1.05 # + 0.005 # Upper limit + Vmin_act: float = 0.95 # + 0.005 + Vmin: float = 0.95 # + 0.005 + 0.002 # Lower limit\ # Linearized equation is overestimating. So increase the lower limit by 0.005. # The problem is not solved to the optimal, so increase another 0.002. alpha: float = 0.5 # learning rate for dual and primal @@ -370,9 +386,10 @@ def opf_run(self, V, P_wopv, Q_wopv): np.delete(P_wopv, self.slack_bus), np.delete(Q_wopv, self.slack_bus), ) + # Initial P and Q setpoint P_0, Q_0 = ( self.pv_frame["avai"].values / self.base_power, - self.pv_frame["avaiQ"].values / self.base_power, + np.zeros(len(self.pv_frame)), ) Ppv, Qpv = np.zeros(self.num_node), np.zeros(self.num_node) for pp, pv_ii in enumerate(self.pv_index): @@ -390,11 +407,11 @@ def opf_run(self, V, P_wopv, Q_wopv): ) if len(ind) == 0: P_0, Q_0 = P_0 * self.base_power, Q_0 * self.base_power - Flag = False + set_power = False logger.debug("Skip this step since no violation") logger.debug(f"minimum V_hat is {np.min(V_hat)}") logger.debug(f"maximum V_hat is {np.max(V_hat)}") - return P_0, Q_0, Flag, V_hat + return P_0, Q_0, set_power, V_hat # logger.debug('ind') # logger.debug(ind) else: @@ -467,11 +484,10 @@ def opf_run(self, V, P_wopv, Q_wopv): logger.debug( f"Target bounds are [{self.parameters.Vmax}, {self.parameters.Vmin}]" ) - Flag = True return ( Pk_last * self.base_power, Qk_last * self.base_power, - Flag, + True, V_hat_final, ) @@ -517,6 +533,9 @@ def __init__(self, federate_name, algorithm_parameters, input_mapping): self.injections = self.vfed.register_subscription( input_mapping["injections"], "" ) + self.sub_available_power = self.vfed.register_subscription( + input_mapping["available_power"], "" + ) self.pub_voltage_mag = self.vfed.register_publication( "voltage_mag", h.HELICS_DATA_TYPE_STRING, "" @@ -573,34 +592,18 @@ def run(self): self.YL0 = csc_matrix(np.delete(Y, slack_bus, axis=0)[:, slack_bus]) self.Y = csc_matrix(Y) del Y - import xarray as xr - - def eqarray_to_xarray(eq: EquipmentNodeArray): - return xr.DataArray( - eq.values, - dims=("eqnode",), - coords={ - "equipment_ids": ("eqnode", eq.equipment_ids), - "ids": ("eqnode", eq.ids), - } - ) - - def measurement_to_xarray(eq: MeasurementArray): - return xr.DataArray( - eq.values, - coords={ - "ids": eq.ids - } - ) - ratings = eqarray_to_xarray(topology.injections.power_real) + 1j * eqarray_to_xarray(topology.injections.power_imaginary) + ratings = eqarray_to_xarray( + topology.injections.power_real + ) + 1j * eqarray_to_xarray(topology.injections.power_imaginary) pv_ratings = ratings[ratings.equipment_ids.str.startswith("PVSystem")] - previous_power_factor = xr.ones_like(pv_ratings.real) - previous_pmpp = xr.ones_like(pv_ratings.real) - # while granted_time < h.HELICS_TIME_MAXTIME: v = measurement_to_xarray(topology.base_voltage_magnitudes) - while granted_time < 1000: + + voltages = None + power_P = None + power_Q = None + while granted_time < h.HELICS_TIME_MAXTIME: logger.debug("granted_time") logger.debug(granted_time) if not self.sub_voltages_real.is_updated(): @@ -610,30 +613,46 @@ def measurement_to_xarray(eq: MeasurementArray): continue voltages_real = VoltagesReal.parse_obj(self.sub_voltages_real.json) - voltages_imag = VoltagesImaginary.parse_obj(self.sub_voltages_imaginary.json) - voltages = measurement_to_xarray(voltages_real) + 1j * measurement_to_xarray(voltages_imag) - print(np.max(np.abs(voltages) / v)) + voltages_imag = VoltagesImaginary.parse_obj( + self.sub_voltages_imaginary.json + ) + voltages = measurement_to_xarray( + voltages_real + ) + 1j * measurement_to_xarray(voltages_imag) + logger.debug(np.max(np.abs(voltages) / v)) assert topology.base_voltage_magnitudes.ids == list(voltages.ids.data) injections = Injection.parse_obj(self.injections.json) - power_injections = eqarray_to_xarray(injections.power_real) + 1j * eqarray_to_xarray(injections.power_imaginary) - pv_injections = power_injections[power_injections.equipment_ids.str.startswith("PVSystem")] + power_injections = eqarray_to_xarray( + injections.power_real + ) + 1j * eqarray_to_xarray(injections.power_imaginary) + pv_injections = power_injections[ + power_injections.equipment_ids.str.startswith("PVSystem") + ] _, pv_injections = xr.align(pv_ratings, pv_injections) + available_power = measurement_to_xarray( + MeasurementArray.parse_obj(self.sub_available_power.json) + ) + + split_power = available_power / pv_injections.ids.groupby( + "equipment_ids" + ).count().rename({"equipment_ids": "ids"}) + available_power = split_power.loc[ + pv_injections.equipment_ids + ].assign_coords(ids=pv_injections.ids) pv = pd.DataFrame() pv["name"] = pv_ratings.equipment_ids.data pv["bus"] = pv_ratings.ids.data pv["kVarRated"] = pv_ratings.values.real - pv["pf"] = previous_power_factor.values.real - pv["avai"] = pv_injections.real / (previous_power_factor * previous_pmpp + 1e-6) - pv["avaiQ"] = pv_injections.imag / (previous_power_factor * previous_pmpp + 1e-6) - - - bus_to_index = {v: i for i, v in enumerate(topology.base_voltage_magnitudes.ids)} + # This needs to be fixed. + pv["avai"] = available_power + bus_to_index = { + v: i for i, v in enumerate(topology.base_voltage_magnitudes.ids) + } pv["index"] = [bus_to_index[v] for v in pv_injections.ids.data] - V0 = voltages[slack_bus].data self.V0 = ( V0 / np.array(topology.base_voltage_magnitudes.values)[slack_bus] @@ -660,43 +679,61 @@ def measurement_to_xarray(eq: MeasurementArray): self.H, self.w_mag, ) - P_set, Q_set, flag, V_hat = opf.opf_run(np.abs(voltages), power_P, power_Q) - power_set = P_set + 1j*Q_set + + P_set, Q_set, set_power, V_hat = opf.opf_run( + np.abs(voltages), power_P, power_Q + ) + power_set = P_set + 1j * Q_set power_factor = power_set.real / (np.abs(power_set) + 1e-7) pmpp = power_set.real / pv["kVarRated"] - assert np.all(np.abs(power_factor) <= 1) - assert np.all((pmpp <= 1) & (pmpp >= 0)) + assert np.all( + np.abs(power_factor) <= 1 + ), f"Invalid power factor at index {np.argmax(np.abs(power_factor) > 1)}: {power_factor[np.argmax(np.abs(power_factor) > 1)]}" + assert np.all( + (pmpp <= 1) & (pmpp >= 0) + ), f"Invalid pmpp at index {np.argmax((pmpp > 1) | (pmpp < 0))}: {pmpp[np.argmax((pmpp > 1) | (pmpp < 0))]}" te = time.time() logger.debug(f"OMOO takes {(te-ts)/60} (min)") + + power_set_xr = ( + xr.DataArray(power_set, coords={"equipment_ids": pv.loc[:, "name"]}) + .groupby("equipment_ids") + .sum() + ) + + available_total_xr = available_power.groupby("equipment_ids").sum() + + pv_settings = [] command_list = [] - for i in range(len(P_set)): - command_list.append( - Command( - obj_name=pv.loc[i, "name"], - obj_property="PF", - val=str(np.sign(Q_set[i]) * power_factor[i]) - # increases Q until it runs out of rating, - # and then decreases P - ) + # We should test against the new interface + for i in range(len(power_set_xr)): + assert ( + available_total_xr.equipment_ids[i] == power_set_xr.equipment_ids[i] ) - command_list.append( - Command( - obj_name=pv.loc[i, "name"], - obj_property="%Pmpp", - val=str(100*pmpp[i]) - # % of rating! + if np.isclose(available_total_xr[i], 0): + continue + pv_settings.append( + ( + power_set_xr.equipment_ids.data[i], + power_set_xr.values[i].real, + power_set_xr.values[i].imag, ) ) - logger.debug(command_list) + command_list_obj = CommandList(__root__=command_list) + logger.debug(command_list_obj) # Turn P_set and Q_set into commands - self.pub_P_set.publish(command_list) + if set_power: + self.pub_P_set.publish(json.dumps(pv_settings)) logger.info("end time: " + str(datetime.now())) - granted_time = h.helicsFederateRequestTime(self.vfed, 1000) - previous_power_factor = power_factor - previous_pmpp = pmpp + # There should be a HELICS way to do this? Set resolution? + previous_time = granted_time + while ( + granted_time <= np.floor(previous_time) + 1 + ): # This should avoid waiting a full 15 minutes + granted_time = h.helicsFederateRequestTime(self.vfed, 1000) self.destroy() diff --git a/omoo_federate/component_definition.json b/omoo_federate/component_definition.json index c357b76..0ab783c 100644 --- a/omoo_federate/component_definition.json +++ b/omoo_federate/component_definition.json @@ -3,16 +3,47 @@ "execute_function": "python OMOO.py", "static_inputs": [], "dynamic_inputs": [ - {"type": "VoltagesReal", "port_id": "voltages_real"}, - {"type": "VoltagesImaginary", "port_id": "voltages_imag"}, - {"type": "PowersReal", "port_id": "powers_real"}, - {"type": "PowersImaginary", "port_id": "powers_imag"}, - {"type": "Topology", "port_id": "topology"}, - {"type": "Injections", "port_id": "injections"} + { + "type": "VoltagesReal", + "port_id": "voltages_real" + }, + { + "type": "VoltagesImaginary", + "port_id": "voltages_imag" + }, + { + "type": "PowersReal", + "port_id": "powers_real" + }, + { + "type": "PowersImaginary", + "port_id": "powers_imag" + }, + { + "type": "Topology", + "port_id": "topology" + }, + { + "type": "Injections", + "port_id": "injections" + }, + { + "type": "MeasurementArray", + "port_id": "available_power" + } ], "dynamic_outputs": [ - {"type": "VoltagesMagnitude", "port_id": "voltage_mag"}, - {"type": "VoltagesAngle", "port_id": "voltage_angle"}, - {"type": "CommandList", "port_id": "P_set"} + { + "type": "VoltagesMagnitude", + "port_id": "voltage_mag" + }, + { + "type": "VoltagesAngle", + "port_id": "voltage_angle" + }, + { + "type": "CommandList", + "port_id": "P_set" + } ] -} +} \ No newline at end of file diff --git a/omoo_system.json b/omoo_system.json deleted file mode 100644 index 7b0cb67..0000000 --- a/omoo_system.json +++ /dev/null @@ -1,97 +0,0 @@ -{ - "name": "test_omoo", - "components": [ - { - "name": "recorder_voltage_real", - "type": "Recorder", - "parameters": {"feather_filename": "../../outputs/voltage_real.feather", - "csv_filename": "../../outputs/voltage_real.csv" - } - }, - { - "name": "recorder_voltage_imag", - "type": "Recorder", - "parameters": {"feather_filename": "../../outputs/voltage_imag.feather", - "csv_filename": "../../outputs/voltage_imag.csv" - } - }, - { - "name": "optimal_pf", - "type": "OMOOComponent", - "parameters": { - "algorithm_parameters": {"tol": 1e-5} - } - }, - { - "name": "feeder", - "type": "LocalFeeder", - "parameters": { - "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 11:00:00", - "number_of_timesteps": 10, - "run_freq_sec": 900, - "topology_output": "../../outputs/topology.json" - } - } - ], - "links": [ - { - "source": "feeder", - "source_port": "voltages_real", - "target": "recorder_voltage_real", - "target_port": "subscription" - }, - { - "source": "feeder", - "source_port": "voltages_imag", - "target": "recorder_voltage_imag", - "target_port": "subscription" - }, - { - "source": "feeder", - "source_port": "topology", - "target": "optimal_pf", - "target_port": "topology" - }, - { - "source": "feeder", - "source_port": "injections", - "target": "optimal_pf", - "target_port": "injections" - }, - { - "source": "feeder", - "source_port": "powers_real", - "target": "optimal_pf", - "target_port": "powers_real" - }, - { - "source": "feeder", - "source_port": "powers_imag", - "target": "optimal_pf", - "target_port": "powers_imag" - }, - { - "source": "feeder", - "source_port": "voltages_real", - "target": "optimal_pf", - "target_port": "voltages_real" - }, - { - "source": "feeder", - "source_port": "voltages_imag", - "target": "optimal_pf", - "target_port": "voltages_imag" - }, - { - "source": "optimal_pf", - "source_port": "P_set", - "target": "feeder", - "target_port": "change_commands" - } - ] -} diff --git a/opf_analysis.py b/opf_analysis.py new file mode 100644 index 0000000..3d5346b --- /dev/null +++ b/opf_analysis.py @@ -0,0 +1,119 @@ +import plotille +import pandas as pd +import numpy as np +from oedisi.types.data_types import Topology +import click +from os.path import join + + +def extract_data(path): + reference_power_real = pd.read_feather(join(path, "reference_power_real.feather")) + reference_power_imag = pd.read_feather(join(path, "reference_power_imag.feather")) + power_real = pd.read_feather(join(path, "power_real.feather")) + power_imag = pd.read_feather(join(path, "power_imag.feather")) + reference_voltage_real = pd.read_feather( + join(path, "reference_voltage_real.feather") + ) + reference_voltage_imag = pd.read_feather( + join(path, "reference_voltage_imag.feather") + ) + voltage_real = pd.read_feather(join(path, "voltage_real.feather")) + voltage_imag = pd.read_feather(join(path, "voltage_imag.feather")) + + topology = Topology.parse_file(join(path, "topology.json")) + + base_voltage_magnitudes = np.array(topology.base_voltage_magnitudes.values) + + reference_voltage = reference_voltage_real.drop( + "time", axis=1 + ) + 1j * reference_voltage_imag.drop("time", axis=1) + voltage = voltage_real.drop("time", axis=1) + 1j * voltage_imag.drop("time", axis=1) + + # Repeated for reference power + reference_power = reference_power_real.drop( + "time", axis=1 + ) + 1j * reference_power_imag.drop("time", axis=1) + power = power_real.drop("time", axis=1) + 1j * power_imag.drop("time", axis=1) + + reference_time = reference_voltage_real.time + time = voltage_real.time + return ( + time, + voltage, + power, + reference_time, + reference_voltage, + reference_power, + base_voltage_magnitudes, + ) + + +@click.command(help="Run opf analysis in directory") +@click.argument( + "path", + default="outputs", + type=click.Path(), +) +def run_analysis(path): + ( + time, + voltage, + power, + reference_time, + reference_voltage, + reference_power, + base_voltage_magnitudes, + ) = extract_data(path) + num_time, num_nodes = reference_voltage.shape + + voltage_magnitude = ( + np.abs(voltage[time.isin(reference_time)].reset_index(drop=True)) + / base_voltage_magnitudes + ) + reference_voltage_magnitude = np.abs(reference_voltage) / base_voltage_magnitudes + magnitude_difference = voltage_magnitude - reference_voltage_magnitude + + fig = plotille.Figure() + fig.width = 100 + fig.height = 70 + fig.plot(np.arange(num_nodes), magnitude_difference.iloc[40, :]) + print(fig.show()) + + fig = plotille.Figure() + fig.width = 100 + fig.height = 70 + fig.plot(np.arange(len(reference_time)), magnitude_difference.max(axis=1)) + print(fig.show()) + + for i in range(30, 70, 10): + fig = plotille.Figure() + fig.width = 100 + fig.height = 70 + fig.plot( + np.arange(num_nodes), + reference_voltage_magnitude.iloc[i, :], + label="Reference Voltage", + ) + fig.plot(np.arange(num_nodes), voltage_magnitude.iloc[i, :], label="Voltage") + fig.plot([0, num_nodes - 1], [0.95, 0.95]) + fig.plot([0, num_nodes - 1], [1.05, 1.05]) + print() + print("Voltage at Time:", time[i]) + print(fig.show(legend=True)) + # (np.abs(voltage[time.isin(reference_time)].reset_index(drop=True)) - np.abs(reference_voltage)) / base_voltage_magnitudes + + power_magnitude_difference = np.abs(reference_power) - np.abs( + power[time.isin(reference_time)].reset_index(drop=True) + ) + for i in range(30, 70, 10): + fig = plotille.Figure() + fig.width = 100 + fig.height = 70 + fig.plot(np.arange(num_nodes), power_magnitude_difference.iloc[i, :]) + print() + print("Power at Time:", time[i]) + print(fig.show()) + + +if __name__ == "__main__": + run_analysis() diff --git a/recorder/record_subscription.py b/recorder/record_subscription.py index afd2898..517b484 100644 --- a/recorder/record_subscription.py +++ b/recorder/record_subscription.py @@ -60,8 +60,11 @@ def run(self): start = True granted_time = h.helicsFederateRequestTime(self.vfed, h.HELICS_TIME_MAXTIME) - with pa.OSFile(self.feather_filename, "wb") as sink: + with pa.OSFile(self.feather_filename, "wb") as sink, pa.OSFile( + self.feather_filename + ".stream", "wb" + ) as streamsink: writer = None + streamwriter = None while granted_time < h.HELICS_TIME_MAXTIME: logger.info("start time: " + str(datetime.now())) logger.debug(granted_time) @@ -83,10 +86,12 @@ def run(self): schema_elements.append(("time", pa.string())) schema = pa.schema(schema_elements) writer = pa.ipc.new_file(sink, schema) + streamwriter = pa.ipc.new_stream(streamsink, schema) start = False - cnt = 0 - writer.write_batch(pa.RecordBatch.from_pylist([measurement_dict])) + record_batch = pa.RecordBatch.from_pylist([measurement_dict]) + writer.write_batch(record_batch) + streamwriter.write_batch(record_batch) granted_time = h.helicsFederateRequestTime( self.vfed, h.HELICS_TIME_MAXTIME @@ -95,6 +100,7 @@ def run(self): if writer is not None: writer.close() + streamwriter.close() data = pd.read_feather(self.feather_filename) data.to_csv(self.csv_filename, header=True, index=False) self.destroy() diff --git a/recorder/requirements.txt b/recorder/requirements.txt index 019b05f..a30c5f4 100644 --- a/recorder/requirements.txt +++ b/recorder/requirements.txt @@ -6,4 +6,4 @@ numpy pandas fastapi uvicorn -oedisi \ No newline at end of file +oedisi diff --git a/requirements.txt b/requirements.txt index 3252875..49cc694 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,4 +9,4 @@ pandas OpenDSSDirect.py[extras]==0.7.0 boto3 xarray -oedisi==1.1.1 +oedisi==1.2.1 diff --git a/scenarios/omoo_system.json b/scenarios/omoo_system.json new file mode 100644 index 0000000..92309d7 --- /dev/null +++ b/scenarios/omoo_system.json @@ -0,0 +1,208 @@ +{ + "name": "test_omoo", + "components": [ + { + "name": "recorder_voltage_real", + "type": "Recorder", + "parameters": { + "feather_filename": "../../outputs/voltage_real.feather", + "csv_filename": "../../outputs/voltage_real.csv" + } + }, + { + "name": "recorder_voltage_imag", + "type": "Recorder", + "parameters": { + "feather_filename": "../../outputs/voltage_imag.feather", + "csv_filename": "../../outputs/voltage_imag.csv" + } + }, + { + "name": "recorder_power_real", + "type": "Recorder", + "parameters": { + "feather_filename": "../../outputs/power_real.feather", + "csv_filename": "../../outputs/power_real.csv" + } + }, + { + "name": "recorder_power_imag", + "type": "Recorder", + "parameters": { + "feather_filename": "../../outputs/power_imag.feather", + "csv_filename": "../../outputs/power_imag.csv" + } + }, + { + "name": "optimal_pf", + "type": "OMOOComponent", + "parameters": { + "algorithm_parameters": { + "tol": 1e-5 + } + } + }, + { + "name": "local_feeder", + "type": "LocalFeeder", + "parameters": { + "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, + "run_freq_sec": 900, + "topology_output": "../../outputs/topology.json", + "tap_setting": 1 + } + }, + { + "name": "reference_feeder", + "type": "LocalFeeder", + "parameters": { + "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, + "run_freq_sec": 900, + "topology_output": "topology.json", + "tap_setting": 1 + } + }, + { + "name": "recorder_reference_voltage_real", + "type": "Recorder", + "parameters": { + "feather_filename": "../../outputs/reference_voltage_real.feather", + "csv_filename": "../../outputs/reference_voltage_real.csv" + } + }, + { + "name": "recorder_reference_voltage_imag", + "type": "Recorder", + "parameters": { + "feather_filename": "../../outputs/reference_voltage_imag.feather", + "csv_filename": "../../outputs/reference_voltage_imag.csv" + } + }, + { + "name": "recorder_reference_power_real", + "type": "Recorder", + "parameters": { + "feather_filename": "../../outputs/reference_power_real.feather", + "csv_filename": "../../outputs/reference_power_real.csv" + } + }, + { + "name": "recorder_reference_power_imag", + "type": "Recorder", + "parameters": { + "feather_filename": "../../outputs/reference_power_imag.feather", + "csv_filename": "../../outputs/reference_power_imag.csv" + } + } + ], + "links": [ + { + "source": "local_feeder", + "source_port": "voltages_real", + "target": "recorder_voltage_real", + "target_port": "subscription" + }, + { + "source": "local_feeder", + "source_port": "voltages_imag", + "target": "recorder_voltage_imag", + "target_port": "subscription" + }, + { + "source": "local_feeder", + "source_port": "powers_real", + "target": "recorder_power_real", + "target_port": "subscription" + }, + { + "source": "local_feeder", + "source_port": "powers_imag", + "target": "recorder_power_imag", + "target_port": "subscription" + }, + { + "source": "reference_feeder", + "source_port": "voltages_real", + "target": "recorder_reference_voltage_real", + "target_port": "subscription" + }, + { + "source": "reference_feeder", + "source_port": "voltages_imag", + "target": "recorder_reference_voltage_imag", + "target_port": "subscription" + }, + { + "source": "reference_feeder", + "source_port": "powers_real", + "target": "recorder_reference_power_real", + "target_port": "subscription" + }, + { + "source": "reference_feeder", + "source_port": "powers_imag", + "target": "recorder_reference_power_imag", + "target_port": "subscription" + }, + { + "source": "local_feeder", + "source_port": "topology", + "target": "optimal_pf", + "target_port": "topology" + }, + { + "source": "local_feeder", + "source_port": "powers_real", + "target": "optimal_pf", + "target_port": "powers_real" + }, + { + "source": "local_feeder", + "source_port": "powers_imag", + "target": "optimal_pf", + "target_port": "powers_imag" + }, + { + "source": "local_feeder", + "source_port": "voltages_real", + "target": "optimal_pf", + "target_port": "voltages_real" + }, + { + "source": "local_feeder", + "source_port": "voltages_imag", + "target": "optimal_pf", + "target_port": "voltages_imag" + }, + { + "source": "local_feeder", + "source_port": "injections", + "target": "optimal_pf", + "target_port": "injections" + }, + { + "source": "local_feeder", + "source_port": "available_power", + "target": "optimal_pf", + "target_port": "available_power" + }, + { + "source": "optimal_pf", + "source_port": "P_set", + "target": "local_feeder", + "target_port": "pv_set" + } + ] +} \ No newline at end of file diff --git a/wls_federate/requirements.txt b/wls_federate/requirements.txt index 89dd33d..36b217e 100644 --- a/wls_federate/requirements.txt +++ b/wls_federate/requirements.txt @@ -5,4 +5,5 @@ scipy numpy fastapi uvicorn -oedisi \ No newline at end of file +oedisi +