From c464c29b17a58b7d987167c8cb85f872b2cb7704 Mon Sep 17 00:00:00 2001 From: Joseph McKinsey Date: Fri, 28 Jun 2024 11:43:44 -0600 Subject: [PATCH 1/9] Add bus coordinates to topology --- LocalFeeder/FeederSimulator.py | 16 +++++++++++++++- LocalFeeder/sender_cosim.py | 6 ++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/LocalFeeder/FeederSimulator.py b/LocalFeeder/FeederSimulator.py index f98ecd1..0fb1d0b 100644 --- a/LocalFeeder/FeederSimulator.py +++ b/LocalFeeder/FeederSimulator.py @@ -1,5 +1,6 @@ """Core class to abstract OpenDSS into Feeder class.""" +import csv import json import logging import math @@ -8,7 +9,7 @@ import time from enum import Enum from time import strptime -from typing import Dict, List, Optional, Set +from typing import Dict, List, Optional, Set, Tuple import boto3 import numpy as np @@ -302,6 +303,19 @@ def get_node_names(self): """Get node names in order.""" return self._AllNodeNames + def get_bus_coords(self) -> Dict[str, Tuple[float, float]]: + """Load bus coordinates from OpenDSS.""" + bus_path = os.path.join(os.path.dirname(self._feeder_file), "Buscoords.dss") + if not os.path.exists(bus_path): + self.bus_coords = None + with open(bus_path, "r") as f: + bus_coord_csv = csv.reader(f, delimiter=" ") + bus_coords = {} + for row in bus_coord_csv: + identifier, x, y = row + bus_coords[identifier] = (float(x), float(y)) + return bus_coords + def load_feeder(self): """Load feeder once downloaded. Relies on legacy mode.""" # Real solution is kvarlimit with kvarmax diff --git a/LocalFeeder/sender_cosim.py b/LocalFeeder/sender_cosim.py index 4fa5aaf..9794b14 100644 --- a/LocalFeeder/sender_cosim.py +++ b/LocalFeeder/sender_cosim.py @@ -352,10 +352,12 @@ def go_cosim( h.helicsFederateEnterExecutingMode(vfed) initial_data = get_initial_data(sim, config) + topology_json = initial_data.topology.json() + topology_json["bus_coords"] = sim.get_bus_coords() logger.info("Sending topology and saving to topology.json") with open(config.topology_output, "w") as f: - f.write(initial_data.topology.json()) - pub_topology.publish(initial_data.topology.json()) + f.write(topology_json) + pub_topology.publish(topology_json) # Publish the forecasted PV outputs as a list of MeasurementArray logger.info("Evaluating the forecasted PV") From 44885a8b84902fe0eeed491c8bf0ded318191b35 Mon Sep 17 00:00:00 2001 From: Joseph McKinsey Date: Fri, 28 Jun 2024 13:05:14 -0600 Subject: [PATCH 2/9] Add topology changes and suggested ones --- LocalFeeder/FeederSimulator.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/LocalFeeder/FeederSimulator.py b/LocalFeeder/FeederSimulator.py index 0fb1d0b..7b5a363 100644 --- a/LocalFeeder/FeederSimulator.py +++ b/LocalFeeder/FeederSimulator.py @@ -71,6 +71,15 @@ class FeederConfig(BaseModel): topology_output: str = "topology.json" use_sparse_admittance: bool = False tap_setting: Optional[int] = None + open_lines: Optional[List[str]] = None + + +# Open Lines: + +# "Line.padswitch(r:p9udt496-p9udt527)p9u_166790", +# "Line.padswitch(r:p9udt527-p9udt528)p9u_166794" is better for large SMART-DS. + +# "Line.goab_disswitch(r:p1udt1425-p1udt881)p1u_12301", # SMALL class FeederMapping(BaseModel): @@ -151,6 +160,7 @@ def __init__(self, config: FeederConfig): self.snapshot_run() assert self._state == OpenDSSState.SNAPSHOT_RUN, f"{self._state}" + self.open_lines = config.open_lines def forcast_pv(self, steps: int) -> list: """ @@ -351,6 +361,10 @@ def load_feeder(self): 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}") + + if self.open_lines is not None: + for l in self.open_lines: + self.open_line(l) self._state = OpenDSSState.LOADED def disable_elements(self): @@ -582,6 +596,11 @@ def get_PQs_gen(self, static=False): ) return pq_xr.sortby(pq_xr.ids) + def open_line(self, line_name: str): + """Open a line in the circuit.""" + dss.Circuit.SetActiveElement("Line." + line_name) + dss.CktElement.Open(2, 0) + def get_PQs_cap(self, static=False): """Get active and reactive power of Capacitors as xarray.""" self._ready_to_load_power(static) From cb05c163c775a7e6ab2250c246b2472b60c6b0ac Mon Sep 17 00:00:00 2001 From: Joseph McKinsey Date: Fri, 12 Jul 2024 10:21:34 -0600 Subject: [PATCH 3/9] Add updates to localfeeder --- LocalFeeder/FeederSimulator.py | 14 +++++++------- LocalFeeder/sender_cosim.py | 13 +++++++++---- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/LocalFeeder/FeederSimulator.py b/LocalFeeder/FeederSimulator.py index 7b5a363..fc6bd2e 100644 --- a/LocalFeeder/FeederSimulator.py +++ b/LocalFeeder/FeederSimulator.py @@ -153,6 +153,7 @@ def __init__(self, config: FeederConfig): else: self._feeder_file = config.existing_feeder_file + self.open_lines = config.open_lines self.load_feeder() if self._sensor_location is None: @@ -160,7 +161,6 @@ def __init__(self, config: FeederConfig): self.snapshot_run() assert self._state == OpenDSSState.SNAPSHOT_RUN, f"{self._state}" - self.open_lines = config.open_lines def forcast_pv(self, steps: int) -> list: """ @@ -203,7 +203,7 @@ def snapshot_run(self): assert self._state != OpenDSSState.UNLOADED, f"{self._state}" self.reenable() dss.Text.Command("CalcVoltageBases") - dss.Text.Command("solve mode=snapshot") + dss.Text.Command("solve mode=snapshot number=1") self._state = OpenDSSState.SNAPSHOT_RUN def reenable(self): @@ -387,7 +387,7 @@ def disabled_run(self): dss.Text.Command("CalcVoltageBases") dss.Text.Command("set maxiterations=20") # solve - dss.Text.Command("solve") + dss.Text.Command("solve number=1") self._state = OpenDSSState.DISABLED_RUN def get_y_matrix(self): @@ -412,7 +412,7 @@ def get_load_y_matrix(self): dss.Text.Command("CalcVoltageBases") dss.Text.Command("set maxiterations=20") # solve - dss.Text.Command("solve") + dss.Text.Command("solve number=1") Ysparse = csc_matrix(dss.YMatrix.getYsparse()) Ymatrix = Ysparse.tocoo() @@ -425,7 +425,7 @@ def get_load_y_matrix(self): dss.Text.Command("CalcVoltageBases") dss.Text.Command("set maxiterations=20") - dss.Text.Command("solve") + dss.Text.Command("solve number=1") self._state = OpenDSSState.SOLVE_AT_TIME return coo_matrix( @@ -460,7 +460,7 @@ def just_solve(self): self._state != OpenDSSState.UNLOADED and self._state != OpenDSSState.DISABLED_RUN ), f"{self._state}" - dss.Text.Command("solve") + dss.Text.Command("solve number=1") def solve(self, hour, second): """Solve at specified time. Must not be unloaded or disabled.""" @@ -473,7 +473,7 @@ def solve(self, hour, second): f"set mode=yearly loadmult=1 number=1 hour={hour} sec={second} " f"stepsize=0" ) - dss.Text.Command("solve") + dss.Text.Command("solve number=1") self._state = OpenDSSState.SOLVE_AT_TIME def _ready_to_load_power(self, static): diff --git a/LocalFeeder/sender_cosim.py b/LocalFeeder/sender_cosim.py index 9794b14..c2112fc 100644 --- a/LocalFeeder/sender_cosim.py +++ b/LocalFeeder/sender_cosim.py @@ -352,8 +352,9 @@ def go_cosim( h.helicsFederateEnterExecutingMode(vfed) initial_data = get_initial_data(sim, config) - topology_json = initial_data.topology.json() - topology_json["bus_coords"] = sim.get_bus_coords() + topology_dict = initial_data.topology.dict() + topology_dict["bus_coords"] = sim.get_bus_coords() + topology_json = json.dumps(topology_dict) logger.info("Sending topology and saving to topology.json") with open(config.topology_output, "w") as f: f.write(topology_json) @@ -368,6 +369,9 @@ def go_cosim( granted_time = -1 request_time = 0 + 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) @@ -396,14 +400,15 @@ 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 logger.info( - f"Solve at hour {floored_timestamp.hour} second " + f"Solve at hour {current_hour} second " f"{60*floored_timestamp.minute + floored_timestamp.second}" ) sim.snapshot_run() sim.solve( - floored_timestamp.hour, + current_hour, 60 * floored_timestamp.minute + floored_timestamp.second, ) From 6e4b9865c856b8f59446c73484190799dc8c6dd9 Mon Sep 17 00:00:00 2001 From: Joseph McKinsey Date: Tue, 6 Aug 2024 14:04:39 -0600 Subject: [PATCH 4/9] Remove Line. addition from open_line --- LocalFeeder/FeederSimulator.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/LocalFeeder/FeederSimulator.py b/LocalFeeder/FeederSimulator.py index fc6bd2e..242a4db 100644 --- a/LocalFeeder/FeederSimulator.py +++ b/LocalFeeder/FeederSimulator.py @@ -164,7 +164,7 @@ def __init__(self, config: FeederConfig): def forcast_pv(self, steps: int) -> list: """ - Forecasts day ahead PV generation for the OpenDSS feeder. The OpenDSS file is run and the + Forecasts day ahead PV generation for the OpenDSS feeder. The OpenDSS file is run and the average irradiance is computed over all PV systems for each time step. This average irradiance is used to compute the individual PV system power output """ @@ -173,7 +173,7 @@ def forcast_pv(self, steps: int) -> list: forecast = [] for k in range(steps): dss.Solution.Solve() - + # names of PV systems and forecasted power output pv_names = [] powers = [] @@ -191,7 +191,7 @@ def forcast_pv(self, steps: int) -> list: pv_names.append(f"PVSystem.{dss.PVsystems.Name()}") powers.append(dss.PVsystems.Pmpp() * avg_irradiance) flag = dss.PVsystems.Next() - + forecast.append(xr.DataArray(powers, coords={"ids": pv_names})) return forecast @@ -598,7 +598,7 @@ def get_PQs_gen(self, static=False): def open_line(self, line_name: str): """Open a line in the circuit.""" - dss.Circuit.SetActiveElement("Line." + line_name) + dss.Circuit.SetActiveElement(line_name) dss.CktElement.Open(2, 0) def get_PQs_cap(self, static=False): From e5ae445dc9d7c2ce7948b65a1fe1b42e8056b10b Mon Sep 17 00:00:00 2001 From: Joseph McKinsey Date: Tue, 6 Aug 2024 14:09:42 -0600 Subject: [PATCH 5/9] Add pause for file to be closed and flushed --- recorder/record_subscription.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/recorder/record_subscription.py b/recorder/record_subscription.py index 517b484..91537f1 100644 --- a/recorder/record_subscription.py +++ b/recorder/record_subscription.py @@ -2,6 +2,7 @@ import json import logging from datetime import datetime +import time import helics as h import numpy as np @@ -101,6 +102,7 @@ def run(self): if writer is not None: writer.close() streamwriter.close() + time.sleep(0.1) data = pd.read_feather(self.feather_filename) data.to_csv(self.csv_filename, header=True, index=False) self.destroy() From cefee663ec83b6c4e1666750d1142c877f3c58b5 Mon Sep 17 00:00:00 2001 From: Joseph McKinsey Date: Tue, 6 Aug 2024 14:17:58 -0600 Subject: [PATCH 6/9] Remove time.sleep and change bus coords --- LocalFeeder/FeederSimulator.py | 3 ++- recorder/record_subscription.py | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/LocalFeeder/FeederSimulator.py b/LocalFeeder/FeederSimulator.py index 242a4db..5a2f248 100644 --- a/LocalFeeder/FeederSimulator.py +++ b/LocalFeeder/FeederSimulator.py @@ -313,11 +313,12 @@ def get_node_names(self): """Get node names in order.""" return self._AllNodeNames - def get_bus_coords(self) -> Dict[str, Tuple[float, float]]: + def get_bus_coords(self) -> Dict[str, Tuple[float, float]] | None: """Load bus coordinates from OpenDSS.""" bus_path = os.path.join(os.path.dirname(self._feeder_file), "Buscoords.dss") if not os.path.exists(bus_path): self.bus_coords = None + return bus_coords with open(bus_path, "r") as f: bus_coord_csv = csv.reader(f, delimiter=" ") bus_coords = {} diff --git a/recorder/record_subscription.py b/recorder/record_subscription.py index 91537f1..abd5e38 100644 --- a/recorder/record_subscription.py +++ b/recorder/record_subscription.py @@ -102,7 +102,6 @@ def run(self): if writer is not None: writer.close() streamwriter.close() - time.sleep(0.1) data = pd.read_feather(self.feather_filename) data.to_csv(self.csv_filename, header=True, index=False) self.destroy() From 960b4a977575de6b7206e6a0b08cfb49ab84e41c Mon Sep 17 00:00:00 2001 From: Joseph McKinsey Date: Tue, 6 Aug 2024 14:22:03 -0600 Subject: [PATCH 7/9] Fix bus coords again --- LocalFeeder/FeederSimulator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LocalFeeder/FeederSimulator.py b/LocalFeeder/FeederSimulator.py index 5a2f248..19e5ace 100644 --- a/LocalFeeder/FeederSimulator.py +++ b/LocalFeeder/FeederSimulator.py @@ -318,7 +318,7 @@ def get_bus_coords(self) -> Dict[str, Tuple[float, float]] | None: bus_path = os.path.join(os.path.dirname(self._feeder_file), "Buscoords.dss") if not os.path.exists(bus_path): self.bus_coords = None - return bus_coords + return self.bus_coords with open(bus_path, "r") as f: bus_coord_csv = csv.reader(f, delimiter=" ") bus_coords = {} From 1c548416d006896a25cd36726379cef9aae12012 Mon Sep 17 00:00:00 2001 From: Joseph McKinsey Date: Tue, 6 Aug 2024 15:31:22 -0600 Subject: [PATCH 8/9] Add bus coords try except --- LocalFeeder/FeederSimulator.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/LocalFeeder/FeederSimulator.py b/LocalFeeder/FeederSimulator.py index 19e5ace..fed768e 100644 --- a/LocalFeeder/FeederSimulator.py +++ b/LocalFeeder/FeederSimulator.py @@ -319,13 +319,17 @@ def get_bus_coords(self) -> Dict[str, Tuple[float, float]] | None: if not os.path.exists(bus_path): self.bus_coords = None return self.bus_coords - with open(bus_path, "r") as f: - bus_coord_csv = csv.reader(f, delimiter=" ") - bus_coords = {} - for row in bus_coord_csv: - identifier, x, y = row - bus_coords[identifier] = (float(x), float(y)) - return bus_coords + try: + with open(bus_path, "r") as f: + bus_coord_csv = csv.reader(f, delimiter=" ") + bus_coords = {} + for row in bus_coord_csv: + identifier, x, y = row + bus_coords[identifier] = (float(x), float(y)) + return bus_coords + except Exception as e: + logging.warning(f"Unable to parse bus coords: {e}") + return None def load_feeder(self): """Load feeder once downloaded. Relies on legacy mode.""" From f1b79263e4c57e5e308445331e80076a61af8b1d Mon Sep 17 00:00:00 2001 From: Joseph McKinsey Date: Tue, 13 Aug 2024 12:20:43 -0600 Subject: [PATCH 9/9] Use better try/except --- LocalFeeder/FeederSimulator.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/LocalFeeder/FeederSimulator.py b/LocalFeeder/FeederSimulator.py index fed768e..99caa63 100644 --- a/LocalFeeder/FeederSimulator.py +++ b/LocalFeeder/FeederSimulator.py @@ -168,7 +168,7 @@ def forcast_pv(self, steps: int) -> list: average irradiance is computed over all PV systems for each time step. This average irradiance is used to compute the individual PV system power output """ - cmd = f'Set stepsize={self._simulation_time_step} Number=1' + cmd = f"Set stepsize={self._simulation_time_step} Number=1" dss.Text.Command(cmd) forecast = [] for k in range(steps): @@ -182,7 +182,7 @@ def forcast_pv(self, steps: int) -> list: flag = dss.PVsystems.First() avg_irradiance = dss.PVsystems.IrradianceNow() while flag: - avg_irradiance = (avg_irradiance + dss.PVsystems.IrradianceNow())/2 + avg_irradiance = (avg_irradiance + dss.PVsystems.IrradianceNow()) / 2 flag = dss.PVsystems.Next() # now compute the power output from the evaluated average irradiance @@ -319,17 +319,17 @@ def get_bus_coords(self) -> Dict[str, Tuple[float, float]] | None: if not os.path.exists(bus_path): self.bus_coords = None return self.bus_coords - try: - with open(bus_path, "r") as f: - bus_coord_csv = csv.reader(f, delimiter=" ") - bus_coords = {} - for row in bus_coord_csv: + with open(bus_path, "r") as f: + bus_coord_csv = csv.reader(f, delimiter=" ") + bus_coords = {} + for row in bus_coord_csv: + try: identifier, x, y = row bus_coords[identifier] = (float(x), float(y)) - return bus_coords - except Exception as e: - logging.warning(f"Unable to parse bus coords: {e}") - return None + except ValueError as e: + logging.warning(f"Unable to parse row in bus coords: {row}, {e}") + return None + return bus_coords def load_feeder(self): """Load feeder once downloaded. Relies on legacy mode."""