From b3aa6b24bd079e8652aceb310fe0a0c304cf50aa Mon Sep 17 00:00:00 2001 From: tarekelgindy Date: Wed, 26 Feb 2025 14:01:27 -0700 Subject: [PATCH 01/50] adding initial take at bus --- .../cyme/equipment/distribution_bus.py | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 src/ditto/readers/cyme/equipment/distribution_bus.py diff --git a/src/ditto/readers/cyme/equipment/distribution_bus.py b/src/ditto/readers/cyme/equipment/distribution_bus.py new file mode 100644 index 0000000..2e9ac2f --- /dev/null +++ b/src/ditto/readers/cyme/equipment/distribution_bus.py @@ -0,0 +1,71 @@ +from infrasys.location import Location +from gdm.distribution.components.distribution_bus import DistributionBus +from gdm import VoltageTypes, Phase, PositiveVoltage +from ditto.readers.cyme.cyme_mapping import CymeMapper + + +class DistributionBusMapper(CymeMapper): + def __init__(self, cyme_model): + super().__init__(cyme_model) + + cyme_file = 'Network' + cyme_section = 'NODE' + + def parse(self, row, from_node_sections, to_node_sections): + name = self.map_name(row) + coordinate = self.map_coordinate(row) + nominal_voltage = self.map_nominal_voltage(row) + phases = self.map_phases(row, from_node_sections, to_node_sections) + voltage_limits = self.map_voltagelimits(row) + voltage_type = self.map_voltage_type(row) + return DistributionBus(name=name, + coordinate=coordinate, + nominal_voltage=nominal_voltage, + phases=phases, + voltage_limits=voltage_limits, + voltage_type=voltage_type) + + def map_name(self, row): + name = row['NodeID'] + return name + + def map_coordinate(self, row): + return Location(row['X'], row['Y']) + + def map_nominal_voltage(self, row): + return PositiveVoltage(row['UserDefinedBaseVoltage'], "kilovolts") + + def map_phases(self, row, from_node_sections, to_node_sections): + node_id = row["NodeId"] + section = None + all_phases = set() + if node_id in from_node_sections: + for section in from_node_sections[node_id]: + phases = section["Phase"] + for phase in phases: + all_phases.add(phase) + if node_id in to_node_sections: + for section in to_node_sections[node_id]: + phases = section["Phase"] + for phase in phases: + all_phases.add(phase) + + all_phases = sorted(list(all_phases)) + phases = [] + if "A" in all_phases: + phases.append(Phase.A) + if "B" in all_phases: + phases.append(Phase.B) + if "C" in all_phases: + phases.append(Phase.C) + if "N" in all_phases: + phases.append(Phase.N) + + + def map_voltagelimits(self, row): + low_voltage = PositiveVoltage(row['LowVoltageLimit'], "kilovolts") + high_voltage = PositiveVoltage(row['HighVoltageLimit'], "kilovolts") + return [low_voltage, high_voltage] + + def map_voltage_type(self, row): + return row['VoltageType'] From 011f6d554444e0f0497bbb3ae07c0418eb956a1c Mon Sep 17 00:00:00 2001 From: Tengis Dashmunkh Date: Mon, 3 Mar 2025 16:24:08 -0700 Subject: [PATCH 02/50] add distribution capacitor equipment and component --- .../cyme/components/distribution_capacitor.py | 70 +++++++++++++++++++ src/ditto/readers/cyme/cyme_mapper.py | 7 ++ .../cyme/equipment/capacitor_equipment.py | 66 +++++++++++++++++ .../cyme/equipment/distribution_bus.py | 2 +- 4 files changed, 144 insertions(+), 1 deletion(-) create mode 100644 src/ditto/readers/cyme/components/distribution_capacitor.py create mode 100644 src/ditto/readers/cyme/cyme_mapper.py create mode 100644 src/ditto/readers/cyme/equipment/capacitor_equipment.py diff --git a/src/ditto/readers/cyme/components/distribution_capacitor.py b/src/ditto/readers/cyme/components/distribution_capacitor.py new file mode 100644 index 0000000..dd10f62 --- /dev/null +++ b/src/ditto/readers/cyme/components/distribution_capacitor.py @@ -0,0 +1,70 @@ +from ditto.readers.cyme.cyme_mapper import CymeMapper +from ditto.readers.cyme.equipment.capacitor_equipment import CapacitorEquipmentMapper +from gdm.distribution.components.distribution_bus import DistributionBus +from gdm.distribution.components.distribution_capacitor import DistributionCapacitor +from gdm import Phase +from loguru import logger + +class DistributionCapacitorMapper(CymeMapper): + def __init__(self, system): + super().__init__(system) + + cyme_file = 'Network' + cyme_section = 'SHUNT CAPACITOR' + + def parse(self, row, section_id_sections): + name = self.map_name(row) + bus = self.map_bus(row, section_id_sections) + phases = self.map_phases(row) + controllers = self.map_controllers(row) + equipment = self.map_equipment(row) + return DistributionCapacitor(name=name, + bus=bus, + phases=phases, + controllers=controllers, + equipment=equipment) + + def map_name(self, row): + return row["DeviceNumber"] + + def map_phases(self, row): + phases = [] + if row["FixedKVARA"]: + phases.append(Phase.A) + if row["FixedKVARB"]: + phases.append(Phase.B) + if row["FixedKVARC"]: + phases.append(Phase.C) + return phases + + def map_bus(self, row, section_id_sections): + section_id = row["SectionID"] + section = section_id_sections[section_id] + from_bus_name = section["FromNodeId"] + to_bus_name = section["ToNodeId"] + to_bus = None + from_bus = None + try: + from_bus = self.system.get_component(component_type=DistributionBus,name=from_bus_name) + except Exception as e: + pass + + try: + to_bus = self.system.get_component(component_type=DistributionBus,name=to_bus_name) + except: + pass + + if from_bus is None: + return to_bus + if from_bus is None: + logger.warning(f"Load {section_id} has no bus") + return from_bus + + + def map_controllers(self, row): + return [] + + def map_equipment(self, row): + mapper = CapacitorEquipmentMapper(self.system) + equipment = mapper.parse(row) + return equipment diff --git a/src/ditto/readers/cyme/cyme_mapper.py b/src/ditto/readers/cyme/cyme_mapper.py new file mode 100644 index 0000000..2c1cf0a --- /dev/null +++ b/src/ditto/readers/cyme/cyme_mapper.py @@ -0,0 +1,7 @@ +from abc import ABC, property + +class CymeMapper(ABC): + + + def __init__(self, system): + self.system = system diff --git a/src/ditto/readers/cyme/equipment/capacitor_equipment.py b/src/ditto/readers/cyme/equipment/capacitor_equipment.py new file mode 100644 index 0000000..22f1d6f --- /dev/null +++ b/src/ditto/readers/cyme/equipment/capacitor_equipment.py @@ -0,0 +1,66 @@ +from ditto.readers.cyme.cyme_mapper import CymeMapper +from gdm.quantities import PositiveReactivePower, PositiveResistance, PositiveReactance +from gdm.distribution.equipment.phase_capacitor_equipment import PhaseCapacitorEquipment +from gdm.distribution.equipment.capacitor_equipment import CapacitorEquipment +from gdm import ConnectionType +from gdm.quantities import PositiveVoltage + +class CapacitorEquipmentMapper(CymeMapper): + def __init__(self, system): + super().__init__(system) + + cyme_file = 'Equipment' + cyme_section = 'SHUNT CAPACITOR' + + def parse(self, row): + name = self.map_name(row) + nominal_voltage = self.map_nominal_voltage(row) + phase_capacitors = self.map_phase_capacitors(row) + return CapacitorEquipment(name=name, + phase_capacitors=phase_capacitors, + nominal_voltage=nominal_voltage) + + def map_name(self, row): + return row["ID"] + + def map_nominal_voltage(self, row): + return PositiveVoltage(row["KV"], "kilovolt") + + def map_phase_capacitors(self, row): + phase_capacitors = [] + number_of_phases = 3 if row["Type"] > 1 else 1 + + for phase in range(1, number_of_phases + 1): + mapper = PhaseCapacitorEquipmentMapper(self.system) + phase_capacitor = mapper.parse(row, phase) + phase_capacitor.num_banks_on = number_of_phases + phase_capacitors.append(phase_capacitor) + return phase_capacitors + + +class PhaseCapacitorEquipmentMapper(CymeMapper): + def __init__(self, system): + super().__init__(system) + + cyme_file = 'Equipment' + cyme_section = 'SHUNT CAPACITOR' + + def parse(self, row, phase): + name = self.map_name(row, phase) + rated_capacity = self.map_rated_capacity(row) + num_banks_on = self.map_num_banks_on(row, phase) + return PhaseCapacitorEquipment(name = name, + rated_capacity=rated_capacity, + num_banks_on=num_banks_on) + + def map_name(self, row, phase): + if phase == 1: + return row["ID"] + "_A" + if phase == 2: + return row["ID"] + "_B" + if phase == 3: + return row["ID"] + "_C" + + + def map_rated_capacity(self, row): + return PositiveReactivePower(row["KVAR"],'kilovar') diff --git a/src/ditto/readers/cyme/equipment/distribution_bus.py b/src/ditto/readers/cyme/equipment/distribution_bus.py index 2e9ac2f..3db21c2 100644 --- a/src/ditto/readers/cyme/equipment/distribution_bus.py +++ b/src/ditto/readers/cyme/equipment/distribution_bus.py @@ -1,7 +1,7 @@ from infrasys.location import Location from gdm.distribution.components.distribution_bus import DistributionBus from gdm import VoltageTypes, Phase, PositiveVoltage -from ditto.readers.cyme.cyme_mapping import CymeMapper +from ditto.readers.cyme.cyme_mapper import CymeMapper class DistributionBusMapper(CymeMapper): From abdc5ca077841397fa4b3faf48f6c67bd2494c41 Mon Sep 17 00:00:00 2001 From: tarekelgindy Date: Mon, 3 Mar 2025 16:51:35 -0700 Subject: [PATCH 03/50] adding testing with data not included in repo --- tests/test_cyme/test_cyme_reader.py | 43 +++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 tests/test_cyme/test_cyme_reader.py diff --git a/tests/test_cyme/test_cyme_reader.py b/tests/test_cyme/test_cyme_reader.py new file mode 100644 index 0000000..9f1cea5 --- /dev/null +++ b/tests/test_cyme/test_cyme_reader.py @@ -0,0 +1,43 @@ +""" Module for testing parsers.""" +from pathlib import Path +import os +import pytest +from ditto.readers.cyme.reader import Reader +from ditto.writers.opendss.write import Writer +import sys +from loguru import logger + +logger.add(sys.stderr, level="WARNING") + +base_path = Path(__file__).parent.parent +cyme_circuit_models = base_path / "data" / "cyme_test_cases" +assert cyme_circuit_models.exists, f"{cyme_circuit_models} does not exist" + +# Require all models to be called Model.mdb and Equipment.mdb for testing + +cyme_network_name = "Network.txt" +cyme_equipment_name = "Equipment.txt" + +target_files = set([cyme_network_name, cyme_equipment_name]) +matching_folders = [] +for folder in Path(cyme_circuit_models).rglob("*"): + if folder.is_dir(): + files_in_folder = set(f.name for f in folder.iterdir() if f.is_file()) + if target_files.issubset(files_in_folder): + matching_folders.append(folder) + +@pytest.mark.parametrize("cyme_folder", matching_folders) +def test_cyme_reader(cyme_folder: Path, tmp_path): + + export_path = base_path / "dump_from_tests" / "cyme" / cyme_folder.name + if not export_path.exists(): + export_path.mkdir(parents=True, exist_ok=True) + + reader = Reader(cyme_folder / cyme_network_name, cyme_folder / cyme_equipment_name) + writer = Writer(reader.get_system()) + writer.write(export_path / "opendss", separate_substations=False, separate_feeders=False) + system = reader.get_system() + json_path = (export_path / cyme_folder.stem.lower()).with_suffix(".json") + system.to_json(json_path, overwrite=True, indent=4) + + assert json_path.exists(), "Failed to export the json file" From bb66583d5ee2a1064b2fa5132bad1be0fc4306a6 Mon Sep 17 00:00:00 2001 From: tarekelgindy Date: Mon, 3 Mar 2025 16:52:56 -0700 Subject: [PATCH 04/50] adding general reading functionality for cyme --- src/ditto/readers/cyme/__init__.py | 1 + src/ditto/readers/cyme/cyme_mapper.py | 8 +++ src/ditto/readers/cyme/reader.py | 70 +++++++++++++++++++++++++++ src/ditto/readers/cyme/utils.py | 31 ++++++++++++ 4 files changed, 110 insertions(+) create mode 100644 src/ditto/readers/cyme/__init__.py create mode 100644 src/ditto/readers/cyme/cyme_mapper.py create mode 100644 src/ditto/readers/cyme/reader.py create mode 100644 src/ditto/readers/cyme/utils.py diff --git a/src/ditto/readers/cyme/__init__.py b/src/ditto/readers/cyme/__init__.py new file mode 100644 index 0000000..9809358 --- /dev/null +++ b/src/ditto/readers/cyme/__init__.py @@ -0,0 +1 @@ +from ditto.readers.cyme.components.distribution_bus import DistributionBusMapper diff --git a/src/ditto/readers/cyme/cyme_mapper.py b/src/ditto/readers/cyme/cyme_mapper.py new file mode 100644 index 0000000..b9cb003 --- /dev/null +++ b/src/ditto/readers/cyme/cyme_mapper.py @@ -0,0 +1,8 @@ +from abc import ABC, abstractproperty + +class CymeMapper(ABC): + + + def __init__(self, system): + self.system = system + diff --git a/src/ditto/readers/cyme/reader.py b/src/ditto/readers/cyme/reader.py new file mode 100644 index 0000000..a6d0ac9 --- /dev/null +++ b/src/ditto/readers/cyme/reader.py @@ -0,0 +1,70 @@ +from gdm.distribution.components.base.distribution_component_base import DistributionComponentBase + +from gdm import DistributionSystem +from ditto.readers.reader import AbstractReader +from ditto.readers.cyme.utils import read_cyme_data +import ditto.readers.cyme as cyme_mapper +from loguru import logger + + + +class Reader(AbstractReader): + + # Order of components is important + component_types = [ + "DistributionBus", + ] + + def __init__(self, network_file, equipment_file): + self.system = DistributionSystem(auto_add_composed_components=True) + self.read(network_file, equipment_file) + + def read(self, network_file, equipment_file): + + # Section data read separately as it links to other tables + section_id_sections = {} + from_node_sections = {} + to_node_sections = {} + + section_data = read_cyme_data(network_file,"SECTION") + for idx, row in section_data.iterrows(): + section_id = row["SectionID"] + section_id_sections[section_id] = row + + from_node = row["FromNodeID"] + to_node = row["ToNodeID"] + + if not from_node in from_node_sections: + from_node_sections[from_node] = [] + from_node_sections[from_node].append(row) + if not to_node in to_node_sections: + to_node_sections[to_node] = [] + to_node_sections[to_node].append(row) + + + for component_type in self.component_types: + mapper_name = component_type + "Mapper" + if not hasattr(cyme_mapper, mapper_name): + logger.warning(f"Mapper {mapper_name} not found. Skipping.") + mapper = getattr(cyme_mapper, mapper_name)(self.system) + cyme_file = mapper.cyme_file + cyme_section = mapper.cyme_section + + data = None + if cyme_file == "Network": + data = read_cyme_data(network_file, cyme_section) + elif cyme_file == "Equipment": + data = read_cyme_data(equipment_file, cyme_section) + else: + raise ValueError(f"Unknown CYME file {cyme_file}") + + components = [] + for idx, row in data.iterrows(): + mapper_name = component_type + "Mapper" + model_entry = mapper.parse(row, from_node_sections, to_node_sections) + if model_entry is not None: + components.append(model_entry) + self.system.add_component(model_entry) + + def get_system(self) -> DistributionSystem: + return self.system diff --git a/src/ditto/readers/cyme/utils.py b/src/ditto/readers/cyme/utils.py new file mode 100644 index 0000000..27a03a7 --- /dev/null +++ b/src/ditto/readers/cyme/utils.py @@ -0,0 +1,31 @@ +import pandas as pd + +def read_cyme_data(cyme_file, cyme_section): + all_data = [] + headers = None + with open(cyme_file) as f: + reading = False + for line in f: + if line.startswith(f"[{cyme_section}]"): + reading = True + continue + if reading: + if line.startswith(f"FORMAT_{cyme_section.replace(' ','')}"): + line_header = line.split("=")[1].strip() + headers = line_header.split(",") + continue + elif line.startswith("FORMAT") or line.startswith("FEEDER"): + # For SECTION Feeder headers + continue + elif line.strip() == "": + reading = False + break + else: + try: + line = line.strip() + all_data.append(line.split(",")) + except: + raise Exception(f"Failed to parse line: {line}") + + data = pd.DataFrame(all_data, columns=headers) + return data From 61ca60be10bf8608ca5a3f833696ca26f10dcddc Mon Sep 17 00:00:00 2001 From: tarekelgindy Date: Mon, 3 Mar 2025 16:53:42 -0700 Subject: [PATCH 05/50] moving distribution bus to components and fixing bugs --- .../distribution_bus.py | 51 +++++++++++-------- 1 file changed, 31 insertions(+), 20 deletions(-) rename src/ditto/readers/cyme/{equipment => components}/distribution_bus.py (51%) diff --git a/src/ditto/readers/cyme/equipment/distribution_bus.py b/src/ditto/readers/cyme/components/distribution_bus.py similarity index 51% rename from src/ditto/readers/cyme/equipment/distribution_bus.py rename to src/ditto/readers/cyme/components/distribution_bus.py index 2e9ac2f..e75f537 100644 --- a/src/ditto/readers/cyme/equipment/distribution_bus.py +++ b/src/ditto/readers/cyme/components/distribution_bus.py @@ -1,28 +1,28 @@ from infrasys.location import Location from gdm.distribution.components.distribution_bus import DistributionBus from gdm import VoltageTypes, Phase, PositiveVoltage -from ditto.readers.cyme.cyme_mapping import CymeMapper +from ditto.readers.cyme.cyme_mapper import CymeMapper class DistributionBusMapper(CymeMapper): def __init__(self, cyme_model): super().__init__(cyme_model) - cyme_file = 'Network' - cyme_section = 'NODE' + cyme_file = 'Network' + cyme_section = 'NODE' - def parse(self, row, from_node_sections, to_node_sections): - name = self.map_name(row) - coordinate = self.map_coordinate(row) - nominal_voltage = self.map_nominal_voltage(row) - phases = self.map_phases(row, from_node_sections, to_node_sections) - voltage_limits = self.map_voltagelimits(row) - voltage_type = self.map_voltage_type(row) - return DistributionBus(name=name, + def parse(self, row, from_node_sections, to_node_sections): + name = self.map_name(row) + coordinate = self.map_coordinate(row) + nominal_voltage = self.map_nominal_voltage(row) + phases = self.map_phases(row, from_node_sections, to_node_sections) + voltage_limits = self.map_voltagelimits(row) + voltage_type = self.map_voltage_type(row) + return DistributionBus(name=name, coordinate=coordinate, nominal_voltage=nominal_voltage, phases=phases, - voltage_limits=voltage_limits, + voltagelimits=voltage_limits, voltage_type=voltage_type) def map_name(self, row): @@ -30,13 +30,16 @@ def map_name(self, row): return name def map_coordinate(self, row): - return Location(row['X'], row['Y']) + X, Y = float(row["CoordX"]), float(row["CoordY"]) + crs = None + return Location(x=X, y=Y, crs=crs) def map_nominal_voltage(self, row): - return PositiveVoltage(row['UserDefinedBaseVoltage'], "kilovolts") + #return PositiveVoltage(float(row['UserDefinedBaseVoltage']), "kilovolts") + return PositiveVoltage(12.47, "kilovolts") - def map_phases(self, row, from_node_sections, to_node_sections): - node_id = row["NodeId"] + def map_phases(self, row, from_node_sections, to_node_sections): + node_id = row["NodeID"] section = None all_phases = set() if node_id in from_node_sections: @@ -60,12 +63,20 @@ def map_phases(self, row, from_node_sections, to_node_sections): phases.append(Phase.C) if "N" in all_phases: phases.append(Phase.N) + return phases def map_voltagelimits(self, row): - low_voltage = PositiveVoltage(row['LowVoltageLimit'], "kilovolts") - high_voltage = PositiveVoltage(row['HighVoltageLimit'], "kilovolts") - return [low_voltage, high_voltage] + low_voltage = None + high_voltage = None + if row['LowVoltageLimit'] != '': + low_voltage = PositiveVoltage(row['LowVoltageLimit'], "kilovolts") + if row['HighVoltageLimit'] != '': + high_voltage = PositiveVoltage(row['HighVoltageLimit'], "kilovolts") + if low_voltage is not None and high_voltage is not None: + return [low_voltage, high_voltage] + else: + return [] def map_voltage_type(self, row): - return row['VoltageType'] + return VoltageTypes.LINE_TO_LINE From 8e556b117f9f285a0e5875252e772a91e4b3a4c3 Mon Sep 17 00:00:00 2001 From: tarekelgindy Date: Wed, 5 Mar 2025 10:48:07 -0700 Subject: [PATCH 06/50] fixing indent problem --- src/ditto/readers/cyme/cyme_mapper.py | 3 +-- src/ditto/readers/cyme/reader.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/ditto/readers/cyme/cyme_mapper.py b/src/ditto/readers/cyme/cyme_mapper.py index 2c1cf0a..882efb3 100644 --- a/src/ditto/readers/cyme/cyme_mapper.py +++ b/src/ditto/readers/cyme/cyme_mapper.py @@ -1,7 +1,6 @@ -from abc import ABC, property +from abc import ABC class CymeMapper(ABC): - def __init__(self, system): self.system = system diff --git a/src/ditto/readers/cyme/reader.py b/src/ditto/readers/cyme/reader.py index a6d0ac9..ff03dce 100644 --- a/src/ditto/readers/cyme/reader.py +++ b/src/ditto/readers/cyme/reader.py @@ -64,7 +64,7 @@ def read(self, network_file, equipment_file): model_entry = mapper.parse(row, from_node_sections, to_node_sections) if model_entry is not None: components.append(model_entry) - self.system.add_component(model_entry) + self.system.add_component(model_entry) def get_system(self) -> DistributionSystem: return self.system From a86dccd3420b5af52e709317883780414052bc60 Mon Sep 17 00:00:00 2001 From: Tengis Dashmunkh Date: Fri, 7 Mar 2025 12:55:19 -0700 Subject: [PATCH 07/50] Create distribution capacitor equipment and component, add it to the cyme reader --- src/ditto/readers/cyme/__init__.py | 1 + .../cyme/components/distribution_capacitor.py | 30 ++++++++++++------- .../cyme/equipment/capacitor_equipment.py | 27 ++++++++++------- src/ditto/readers/cyme/reader.py | 9 +++--- .../components/distribution_capacitor.py | 3 ++ src/ditto/writers/opendss/opendss_mapper.py | 11 ++++--- tests/test_cyme/test_cyme_reader.py | 2 +- 7 files changed, 54 insertions(+), 29 deletions(-) diff --git a/src/ditto/readers/cyme/__init__.py b/src/ditto/readers/cyme/__init__.py index 9809358..8cf1bcf 100644 --- a/src/ditto/readers/cyme/__init__.py +++ b/src/ditto/readers/cyme/__init__.py @@ -1 +1,2 @@ from ditto.readers.cyme.components.distribution_bus import DistributionBusMapper +from ditto.readers.cyme.components.distribution_capacitor import DistributionCapacitorMapper diff --git a/src/ditto/readers/cyme/components/distribution_capacitor.py b/src/ditto/readers/cyme/components/distribution_capacitor.py index dd10f62..86515b8 100644 --- a/src/ditto/readers/cyme/components/distribution_capacitor.py +++ b/src/ditto/readers/cyme/components/distribution_capacitor.py @@ -1,3 +1,4 @@ +from ditto.readers.cyme.utils import read_cyme_data from ditto.readers.cyme.cyme_mapper import CymeMapper from ditto.readers.cyme.equipment.capacitor_equipment import CapacitorEquipmentMapper from gdm.distribution.components.distribution_bus import DistributionBus @@ -10,19 +11,21 @@ def __init__(self, system): super().__init__(system) cyme_file = 'Network' - cyme_section = 'SHUNT CAPACITOR' + cyme_section = 'SHUNT CAPACITOR SETTING' - def parse(self, row, section_id_sections): + def parse(self, row, section_id_sections, equipment_file): name = self.map_name(row) bus = self.map_bus(row, section_id_sections) phases = self.map_phases(row) controllers = self.map_controllers(row) - equipment = self.map_equipment(row) + equipment = self.map_equipment(row, equipment_file) + in_service = self.map_in_service(row) return DistributionCapacitor(name=name, bus=bus, phases=phases, controllers=controllers, - equipment=equipment) + equipment=equipment, + in_service=in_service) def map_name(self, row): return row["DeviceNumber"] @@ -40,8 +43,8 @@ def map_phases(self, row): def map_bus(self, row, section_id_sections): section_id = row["SectionID"] section = section_id_sections[section_id] - from_bus_name = section["FromNodeId"] - to_bus_name = section["ToNodeId"] + from_bus_name = section["FromNodeID"] + to_bus_name = section["ToNodeID"] to_bus = None from_bus = None try: @@ -51,7 +54,7 @@ def map_bus(self, row, section_id_sections): try: to_bus = self.system.get_component(component_type=DistributionBus,name=to_bus_name) - except: + except Exception as e: pass if from_bus is None: @@ -64,7 +67,14 @@ def map_bus(self, row, section_id_sections): def map_controllers(self, row): return [] - def map_equipment(self, row): + def map_equipment(self, row, equipment_file): mapper = CapacitorEquipmentMapper(self.system) - equipment = mapper.parse(row) - return equipment + equipment_data = read_cyme_data(equipment_file, mapper.cyme_section) + for idx, equipment_row in equipment_data.iterrows(): + if equipment_row['ID'] == row['ShuntCapacitorID']: + equipment = mapper.parse(equipment_row, connection=row['Connection']) + return equipment + return None + + def map_in_service(self, row): + return True if int(row['ConnectionStatus']) == 0 else False diff --git a/src/ditto/readers/cyme/equipment/capacitor_equipment.py b/src/ditto/readers/cyme/equipment/capacitor_equipment.py index 22f1d6f..2f84969 100644 --- a/src/ditto/readers/cyme/equipment/capacitor_equipment.py +++ b/src/ditto/readers/cyme/equipment/capacitor_equipment.py @@ -2,6 +2,7 @@ from gdm.quantities import PositiveReactivePower, PositiveResistance, PositiveReactance from gdm.distribution.equipment.phase_capacitor_equipment import PhaseCapacitorEquipment from gdm.distribution.equipment.capacitor_equipment import CapacitorEquipment +from gdm.distribution.distribution_enum import VoltageTypes from gdm import ConnectionType from gdm.quantities import PositiveVoltage @@ -12,35 +13,42 @@ def __init__(self, system): cyme_file = 'Equipment' cyme_section = 'SHUNT CAPACITOR' - def parse(self, row): + def parse(self, row, connection): name = self.map_name(row) nominal_voltage = self.map_nominal_voltage(row) phase_capacitors = self.map_phase_capacitors(row) + voltage_type = self.map_voltage_type(connection) return CapacitorEquipment(name=name, phase_capacitors=phase_capacitors, - nominal_voltage=nominal_voltage) + nominal_voltage=nominal_voltage, + voltage_type=voltage_type) def map_name(self, row): return row["ID"] def map_nominal_voltage(self, row): - return PositiveVoltage(row["KV"], "kilovolt") + return PositiveVoltage(float(row["KV"]), "kilovolt") def map_phase_capacitors(self, row): phase_capacitors = [] - number_of_phases = 3 if row["Type"] > 1 else 1 + number_of_phases = 3 if int(row["Type"]) > 1 else 1 for phase in range(1, number_of_phases + 1): - mapper = PhaseCapacitorEquipmentMapper(self.system) + mapper = PhaseCapacitorEquipmentMapper(self.system, num_banks_on=number_of_phases) phase_capacitor = mapper.parse(row, phase) - phase_capacitor.num_banks_on = number_of_phases phase_capacitors.append(phase_capacitor) return phase_capacitors + + def map_voltage_type(self, connection): + if connection in ("Y", "YNG"): + return VoltageTypes.LINE_TO_GROUND + return VoltageTypes.LINE_TO_LINE class PhaseCapacitorEquipmentMapper(CymeMapper): - def __init__(self, system): + def __init__(self, system, num_banks_on): super().__init__(system) + self.num_banks_on = num_banks_on cyme_file = 'Equipment' cyme_section = 'SHUNT CAPACITOR' @@ -48,10 +56,9 @@ def __init__(self, system): def parse(self, row, phase): name = self.map_name(row, phase) rated_capacity = self.map_rated_capacity(row) - num_banks_on = self.map_num_banks_on(row, phase) return PhaseCapacitorEquipment(name = name, rated_capacity=rated_capacity, - num_banks_on=num_banks_on) + num_banks_on=self.num_banks_on) def map_name(self, row, phase): if phase == 1: @@ -63,4 +70,4 @@ def map_name(self, row, phase): def map_rated_capacity(self, row): - return PositiveReactivePower(row["KVAR"],'kilovar') + return PositiveReactivePower(float(row["KVAR"]),'kilovar') diff --git a/src/ditto/readers/cyme/reader.py b/src/ditto/readers/cyme/reader.py index ff03dce..6d58e31 100644 --- a/src/ditto/readers/cyme/reader.py +++ b/src/ditto/readers/cyme/reader.py @@ -1,5 +1,4 @@ from gdm.distribution.components.base.distribution_component_base import DistributionComponentBase - from gdm import DistributionSystem from ditto.readers.reader import AbstractReader from ditto.readers.cyme.utils import read_cyme_data @@ -7,12 +6,11 @@ from loguru import logger - class Reader(AbstractReader): - # Order of components is important component_types = [ "DistributionBus", + "DistributionCapacitor" ] def __init__(self, network_file, equipment_file): @@ -61,7 +59,10 @@ def read(self, network_file, equipment_file): components = [] for idx, row in data.iterrows(): mapper_name = component_type + "Mapper" - model_entry = mapper.parse(row, from_node_sections, to_node_sections) + if mapper_name == "DistributionCapacitorMapper": + model_entry = mapper.parse(row, section_id_sections, equipment_file) + if mapper_name == "DistributionBusMapper": + model_entry = mapper.parse(row, from_node_sections, to_node_sections) if model_entry is not None: components.append(model_entry) self.system.add_component(model_entry) diff --git a/src/ditto/writers/opendss/components/distribution_capacitor.py b/src/ditto/writers/opendss/components/distribution_capacitor.py index d848463..4ea5f8a 100644 --- a/src/ditto/writers/opendss/components/distribution_capacitor.py +++ b/src/ditto/writers/opendss/components/distribution_capacitor.py @@ -62,3 +62,6 @@ def map_equipment(self): self.opendss_dict["kvar"] = [total_kvar_per_bank] * num_banks # TODO: We're not building equipment for the Capacitors. This means that there's no guarantee that we're addressing all of the attributes in the equipment in a structured way like we are for the component. + + def map_in_service(self): + self.opendss_dict["Enabled"] = "Yes" if self.model.in_serivce else "No" \ No newline at end of file diff --git a/src/ditto/writers/opendss/opendss_mapper.py b/src/ditto/writers/opendss/opendss_mapper.py index ceb3e35..ec00a38 100644 --- a/src/ditto/writers/opendss/opendss_mapper.py +++ b/src/ditto/writers/opendss/opendss_mapper.py @@ -45,7 +45,7 @@ def map_uuid(self): def map_system_uuid(self): return - + def map_substation(self): if hasattr(self.model, "substation") and self.model.substation is not None: self.substation = self.model.substation.name @@ -53,11 +53,14 @@ def map_substation(self): def map_feeder(self): if hasattr(self.model, "feeder") and self.model.feeder is not None: self.feeder = self.model.feeder.name - + def populate_opendss_dictionary(self): # Should not be populating an existing dictionary. Assert error if not empty assert len(self.opendss_dict) == 0 self.map_common() for field in self.model.model_fields: - mapping_function = getattr(self, "map_" + field) - mapping_function() + try: + mapping_function = getattr(self, "map_" + field) + mapping_function() + except Exception as e: + print(f"{self.model.label} - {field}") diff --git a/tests/test_cyme/test_cyme_reader.py b/tests/test_cyme/test_cyme_reader.py index 9f1cea5..8f9a6ba 100644 --- a/tests/test_cyme/test_cyme_reader.py +++ b/tests/test_cyme/test_cyme_reader.py @@ -39,5 +39,5 @@ def test_cyme_reader(cyme_folder: Path, tmp_path): system = reader.get_system() json_path = (export_path / cyme_folder.stem.lower()).with_suffix(".json") system.to_json(json_path, overwrite=True, indent=4) - + assert json_path.exists(), "Failed to export the json file" From 4cd65e32502399f247f6a34d09b297ab7ac60709 Mon Sep 17 00:00:00 2001 From: Tengis Dashmunkh Date: Sun, 9 Mar 2025 23:03:33 -0600 Subject: [PATCH 08/50] Implement distribution loads --- src/ditto/readers/cyme/__init__.py | 1 + .../cyme/components/distribution_load.py | 57 +++++++ .../readers/cyme/equipment/load_equipment.py | 148 ++++++++++++++++++ src/ditto/readers/cyme/reader.py | 7 +- 4 files changed, 211 insertions(+), 2 deletions(-) create mode 100644 src/ditto/readers/cyme/components/distribution_load.py create mode 100644 src/ditto/readers/cyme/equipment/load_equipment.py diff --git a/src/ditto/readers/cyme/__init__.py b/src/ditto/readers/cyme/__init__.py index 8cf1bcf..e1be047 100644 --- a/src/ditto/readers/cyme/__init__.py +++ b/src/ditto/readers/cyme/__init__.py @@ -1,2 +1,3 @@ from ditto.readers.cyme.components.distribution_bus import DistributionBusMapper from ditto.readers.cyme.components.distribution_capacitor import DistributionCapacitorMapper +from ditto.readers.cyme.components.distribution_load import DistributionLoadMapper diff --git a/src/ditto/readers/cyme/components/distribution_load.py b/src/ditto/readers/cyme/components/distribution_load.py new file mode 100644 index 0000000..6f3e202 --- /dev/null +++ b/src/ditto/readers/cyme/components/distribution_load.py @@ -0,0 +1,57 @@ +from ditto.readers.cyme.cyme_mapper import CymeMapper +from ditto.readers.cyme.equipment.load_equipment import LoadEquipmentMapper +from gdm.distribution.components.distribution_bus import DistributionBus +from gdm.distribution.components.distribution_load import DistributionLoad +from gdm import Phase +from loguru import logger + +class DistributionLoadMapper(CymeMapper): + def __init__(self, system): + super().__init__(system) + + cyme_file = 'Network' + cyme_section = "LOAD EQUIVALENT" + + + def parse(self, row): + name = self.map_name(row) + bus = self.map_bus(row) + phases = self.map_phases(row) + equipment = self.map_equipment(row) + if len(phases) == 0: + logger.warning(f"Load {name} has no kW values. Skipping...") + return None + return DistributionLoad(name=name, + bus=bus, + phases=phases, + equipment=equipment) + + def map_name(self, row): + return row["LoadModelName"] + + def map_bus(self, row): + bus_name = row["NodeID"] + bus = None + try: + bus = self.system.get_component(component_type=DistributionBus, name=bus_name) + except Exception as e: + pass + + if bus is None: + logger.warning(f"Load {row['NodeID']} has no bus") + return bus + + def map_phases(self, row): + phases = [] + if row['Value1A'] is not None: + phases.append(Phase.A) + if row['Value1B'] is not None: + phases.append(Phase.B) + if row['Value1C'] is not None: + phases.append(Phase.C) + return phases + + def map_equipment(self, row): + mapper = LoadEquipmentMapper(self.system) + equipment = mapper.parse(row) + return equipment diff --git a/src/ditto/readers/cyme/equipment/load_equipment.py b/src/ditto/readers/cyme/equipment/load_equipment.py new file mode 100644 index 0000000..bdd357d --- /dev/null +++ b/src/ditto/readers/cyme/equipment/load_equipment.py @@ -0,0 +1,148 @@ +from loguru import logger +from ditto.readers.cyme.cyme_mapper import CymeMapper +from gdm.distribution.equipment.load_equipment import LoadEquipment +from gdm.distribution.equipment.phase_load_equipment import PhaseLoadEquipment +from gdm.quantities import ActivePower, ReactivePower + +class LoadEquipmentMapper(CymeMapper): + def __init__(self, system): + super().__init__(system) + + cyme_file = 'Network' + cyme_section = 'LOAD EQUIVALENT' + + def parse(self, row): + name = self.map_name(row) + phase_loads = self.map_phase_loads(row) + return LoadEquipment(name=name, + phase_loads=phase_loads) + + def map_name(self, row): + return row['LoadModelName'] + + def map_phase_loads(self,row): + phase_loads = [] + for phase in range(1,4): + mapper = PhaseLoadEquipmentMapper(self.system) + phase_load = mapper.parse(row, phase) + phase_loads.append(phase_load) + return phase_loads + +class PhaseLoadEquipmentMapper(CymeMapper): + def __init__(self, system): + super().__init__(system) + + cyme_file = 'Network' + cyme_section = "LOAD EQUIVALENT" + + def parse(self, row, phase): + name = self.map_name(row, phase) + real_power, reactive_power = self.map_powers(row, phase) + z_real = self.map_z_real(row) + z_imag = self.map_z_imag(row) + i_real = self.map_i_real(row) + i_imag = self.map_i_imag(row) + p_real = self.map_p_real(row) + p_imag = self.map_p_imag(row) + return PhaseLoadEquipment(name=name, + real_power=real_power, + reactive_power=reactive_power, + z_real=z_real, + z_imag=z_imag, + i_real=i_real, + i_imag=i_imag, + p_real=p_real, + p_imag=p_imag) + + def map_name(self, row, phase): + if phase == 1: + return row['NodeID'] + "_A" + if phase == 2: + return row['NodeID'] + "_B" + if phase == 3: + return row['NodeID'] + "_C" + + def map_real_power(self, row, phase): + if phase == 1: + kW = float(row["Value1A"]) + elif phase == 2: + kW = float(row["Value1B"]) + elif phase == 3: + kW = float(row["Value1C"]) + return ActivePower(kW, 'kilowatt') + + def map_reactive_power(self, row, phase): + if phase == 1: + kvar = float(row["Value2A"]) + elif phase == 2: + kvar = float(row["Value2B"]) + elif phase == 3: + kvar = float(row["Value2C"]) + return ReactivePower(kvar, 'kilovar') + + def map_kva_pf(self, row, phase): + if phase == 1: + kw = float(row["Value1A"]) * float(row["Value2A"]) + kvar = float(row["Value1A"]) * (1 - float(row["Value2A"])**2) ** 0.5 + elif phase == 2: + kw = float(row["Value1B"]) * float(row["Value2B"]) + kvar = float(row["Value1B"]) * (1 - float(row["Value2B"])**2) ** 0.5 + elif phase == 3: + kw = float(row["Value1C"]) * float(row["Value2C"]) + kvar = float(row["Value1C"]) * (1 - float(row["Value2C"])**2) ** 0.5 + return ActivePower(kw, 'kilowatt'), ReactivePower(kvar, 'kilovar') + + def map_kw_pf(self, row, phase): + if phase == 1: + kw = float(row["Value1A"]) + + if float(row["Value2A"]) <= 0 or float(row["Value2A"]) > 1: + logger.warning("Power factor must be between 0 and 1 (exclusive of 0). Setting kVAR to 0.") + kvar = 0 + else: + kvar = float(row["Value1A"]) * ((1 / float(row["Value2A"])**2) - 1) ** 0.5 + elif phase == 2: + kw = float(row["Value1B"]) + + if float(row["Value2B"]) <= 0 or float(row["Value2B"]) > 1: + logger.warning("Power factor must be between 0 and 1 (exclusive of 0). Setting kVAR to 0.") + kvar = 0 + else: + kvar = float(row["Value1B"]) * ((1 / float(row["Value2B"])**2) - 1) ** 0.5 + elif phase == 3: + kw = float(row["Value1C"]) + + if float(row["Value2C"]) <= 0 or float(row["Value2C"]) > 1: + logger.warning("Power factor must be between 0 and 1 (exclusive of 0). Setting kVAR to 0.") + kvar = 0 + else: + kvar = float(row["Value1C"]) * ((1 / float(row["Value2C"])**2) - 1) ** 0.5 + return ActivePower(kw, 'kilowatt'), ReactivePower(kvar, 'kilovar') + + def map_powers(self, row, phase): + if row['Format'] == 'KW_KVAR': + return self.map_real_power(row, phase), self.map_reactive_power(row, phase) + elif row['Format'] == 'KVA_PF': + return self.map_kva_pf(row, phase) + elif row['Format'] == 'KW_PF': + return self.map_kw_pf(row, phase) + elif row['Format'] == 'AMP_PF': + pass #TODO + + def map_z_real(self, row): + return 1 + + def map_z_imag(self, row): + return 1 + + def map_i_real(self, row): + return 0 + + def map_i_imag(self, row): + return 0 + + def map_p_real(self, row): + return 0 + + def map_p_imag(self, row): + return 0 diff --git a/src/ditto/readers/cyme/reader.py b/src/ditto/readers/cyme/reader.py index 6d58e31..bb828a3 100644 --- a/src/ditto/readers/cyme/reader.py +++ b/src/ditto/readers/cyme/reader.py @@ -9,8 +9,9 @@ class Reader(AbstractReader): # Order of components is important component_types = [ - "DistributionBus", - "DistributionCapacitor" + "DistributionBus", + "DistributionCapacitor", + "DistributionLoad" ] def __init__(self, network_file, equipment_file): @@ -63,6 +64,8 @@ def read(self, network_file, equipment_file): model_entry = mapper.parse(row, section_id_sections, equipment_file) if mapper_name == "DistributionBusMapper": model_entry = mapper.parse(row, from_node_sections, to_node_sections) + if mapper_name == "DistributionLoadMapper": + model_entry = mapper.parse(row) if model_entry is not None: components.append(model_entry) self.system.add_component(model_entry) From a2f7351e394759fa97259c3777997a3e9da27ca5 Mon Sep 17 00:00:00 2001 From: tarekelgindy Date: Tue, 25 Mar 2025 16:47:15 -0600 Subject: [PATCH 09/50] untested changes to read from load file not network --- .../readers/cyme/equipment/load_equipment.py | 146 ++++++++---------- 1 file changed, 64 insertions(+), 82 deletions(-) diff --git a/src/ditto/readers/cyme/equipment/load_equipment.py b/src/ditto/readers/cyme/equipment/load_equipment.py index bdd357d..6348132 100644 --- a/src/ditto/readers/cyme/equipment/load_equipment.py +++ b/src/ditto/readers/cyme/equipment/load_equipment.py @@ -3,16 +3,18 @@ from gdm.distribution.equipment.load_equipment import LoadEquipment from gdm.distribution.equipment.phase_load_equipment import PhaseLoadEquipment from gdm.quantities import ActivePower, ReactivePower +from gdm.distribution.distribution_enum import ConnectionType class LoadEquipmentMapper(CymeMapper): def __init__(self, system): super().__init__(system) - cyme_file = 'Network' - cyme_section = 'LOAD EQUIVALENT' + cyme_file = 'Load' + cyme_section = 'LOADS' def parse(self, row): name = self.map_name(row) + connection_type = self.map_connection_type(row) phase_loads = self.map_phase_loads(row) return LoadEquipment(name=name, phase_loads=phase_loads) @@ -20,24 +22,42 @@ def parse(self, row): def map_name(self, row): return row['LoadModelName'] - def map_phase_loads(self,row): + def map_connection_type(self, row): + connection_number = int(row['Connection']) + connection_map = { + 0: ConnectionType.STAR, #Yg + 1: ConnectionType.STAR, #Y + 2: ConnectionType.DELTA, #Delta + 3: ConnectionType.OPEN_DELTA, #Open Delta + 4: ConnectinType.DELTA, #Closed Delta + 5: ConnectionType.ZIG_ZAG, # Zg + 6: ConnectionType.STAR, # CT + 7: ConnectionType.DELTA, # Dg - Not sure what this is? + } + return connection_map[connection_number] + + def map_phase_loads(self, row): + # Get the PhaseLoadEquipment with the same name as the Load + name = row['DeviceNumber'] phase_loads = [] - for phase in range(1,4): - mapper = PhaseLoadEquipmentMapper(self.system) - phase_load = mapper.parse(row, phase) - phase_loads.append(phase_load) + for phase in ['A', 'B', 'C']: + phase_load_name = name + '_' + phase + phase_load = self.system.get_component(component_type=PhaseLoadEquipment, name=phase_load_name) + if phase_load is not None: + phase_loads.append(phase_load) return phase_loads class PhaseLoadEquipmentMapper(CymeMapper): def __init__(self, system): super().__init__(system) - cyme_file = 'Network' - cyme_section = "LOAD EQUIVALENT" + cyme_file = 'Load' + cyme_section = "CUSTOMER LOADS" - def parse(self, row, phase): - name = self.map_name(row, phase) - real_power, reactive_power = self.map_powers(row, phase) + def parse(self, row): + name = self.map_name(row) + real_power = map_real_power(row), + reactive_power = map_reactive_power(row), z_real = self.map_z_real(row) z_imag = self.map_z_imag(row) i_real = self.map_i_real(row) @@ -54,81 +74,43 @@ def parse(self, row, phase): p_real=p_real, p_imag=p_imag) - def map_name(self, row, phase): - if phase == 1: - return row['NodeID'] + "_A" - if phase == 2: - return row['NodeID'] + "_B" - if phase == 3: - return row['NodeID'] + "_C" + def map_name(self, row): + phase = row['LoadPhase'] + return row['DeviceNumber']+"_"+str(phase) - def map_real_power(self, row, phase): - if phase == 1: - kW = float(row["Value1A"]) - elif phase == 2: - kW = float(row["Value1B"]) - elif phase == 3: - kW = float(row["Value1C"]) - return ActivePower(kW, 'kilowatt') - - def map_reactive_power(self, row, phase): - if phase == 1: - kvar = float(row["Value2A"]) - elif phase == 2: - kvar = float(row["Value2B"]) - elif phase == 3: - kvar = float(row["Value2C"]) - return ReactivePower(kvar, 'kilovar') - - def map_kva_pf(self, row, phase): - if phase == 1: - kw = float(row["Value1A"]) * float(row["Value2A"]) - kvar = float(row["Value1A"]) * (1 - float(row["Value2A"])**2) ** 0.5 - elif phase == 2: - kw = float(row["Value1B"]) * float(row["Value2B"]) - kvar = float(row["Value1B"]) * (1 - float(row["Value2B"])**2) ** 0.5 - elif phase == 3: - kw = float(row["Value1C"]) * float(row["Value2C"]) - kvar = float(row["Value1C"]) * (1 - float(row["Value2C"])**2) ** 0.5 + def compute_powers(self, row): + v1 = float(row['Value1']) + v2 = float(row['Value2']) + kw = None + kvar = None + value_type = int(row['ValueType']) + # kw and kvar + if value_type == 0: + kw = v1 + kvar = v2 + # kva and pf + elif value_type == 1: + kw = v1 * v2 + kvar = v1 * (1 - v2**2) ** 0.5 + # kw and pf + elif value_type == 2: + kw = v1 + kvar = v1 * (1/v2**2 - 1) ** 0.5 + # amp and pf + else: + pass return ActivePower(kw, 'kilowatt'), ReactivePower(kvar, 'kilovar') - def map_kw_pf(self, row, phase): - if phase == 1: - kw = float(row["Value1A"]) - - if float(row["Value2A"]) <= 0 or float(row["Value2A"]) > 1: - logger.warning("Power factor must be between 0 and 1 (exclusive of 0). Setting kVAR to 0.") - kvar = 0 - else: - kvar = float(row["Value1A"]) * ((1 / float(row["Value2A"])**2) - 1) ** 0.5 - elif phase == 2: - kw = float(row["Value1B"]) - - if float(row["Value2B"]) <= 0 or float(row["Value2B"]) > 1: - logger.warning("Power factor must be between 0 and 1 (exclusive of 0). Setting kVAR to 0.") - kvar = 0 - else: - kvar = float(row["Value1B"]) * ((1 / float(row["Value2B"])**2) - 1) ** 0.5 - elif phase == 3: - kw = float(row["Value1C"]) - - if float(row["Value2C"]) <= 0 or float(row["Value2C"]) > 1: - logger.warning("Power factor must be between 0 and 1 (exclusive of 0). Setting kVAR to 0.") - kvar = 0 - else: - kvar = float(row["Value1C"]) * ((1 / float(row["Value2C"])**2) - 1) ** 0.5 - return ActivePower(kw, 'kilowatt'), ReactivePower(kvar, 'kilovar') - def map_powers(self, row, phase): - if row['Format'] == 'KW_KVAR': - return self.map_real_power(row, phase), self.map_reactive_power(row, phase) - elif row['Format'] == 'KVA_PF': - return self.map_kva_pf(row, phase) - elif row['Format'] == 'KW_PF': - return self.map_kw_pf(row, phase) - elif row['Format'] == 'AMP_PF': - pass #TODO + def map_real_power(self, row): + kw, kvar = self.compute_powers(row) + return ActivePower(kw, 'kilowatt') + def map_reactive_power(self, row): + kw, kvar = self.compute_powers(row) + return ReactivePower(kvar, 'kilovar') + + # Is this included in CYME 9.* ? It was in customer class in previous cyme versions def map_z_real(self, row): return 1 From 7e257e475d185ea943744ce627f0bc0db9264095 Mon Sep 17 00:00:00 2001 From: tarekelgindy Date: Tue, 6 May 2025 15:35:12 -0600 Subject: [PATCH 10/50] fixing breaking changes from GDM updates in opendss writer --- src/ditto/writers/opendss/components/distribution_branch.py | 2 +- .../writers/opendss/components/distribution_capacitor.py | 4 ++-- src/ditto/writers/opendss/components/distribution_load.py | 2 +- src/ditto/writers/opendss/write.py | 3 ++- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/ditto/writers/opendss/components/distribution_branch.py b/src/ditto/writers/opendss/components/distribution_branch.py index da80012..ef4c041 100644 --- a/src/ditto/writers/opendss/components/distribution_branch.py +++ b/src/ditto/writers/opendss/components/distribution_branch.py @@ -1,4 +1,4 @@ -from gdm import Phase +from gdm.distribution.enums import Phase from ditto.writers.opendss.opendss_mapper import OpenDSSMapper from ditto.enumerations import OpenDSSFileTypes diff --git a/src/ditto/writers/opendss/components/distribution_capacitor.py b/src/ditto/writers/opendss/components/distribution_capacitor.py index 4ea5f8a..b9c885b 100644 --- a/src/ditto/writers/opendss/components/distribution_capacitor.py +++ b/src/ditto/writers/opendss/components/distribution_capacitor.py @@ -1,4 +1,4 @@ -from gdm import ConnectionType +from gdm.distribution.enums import ConnectionType from ditto.writers.opendss.opendss_mapper import OpenDSSMapper from ditto.enumerations import OpenDSSFileTypes @@ -64,4 +64,4 @@ def map_equipment(self): # TODO: We're not building equipment for the Capacitors. This means that there's no guarantee that we're addressing all of the attributes in the equipment in a structured way like we are for the component. def map_in_service(self): - self.opendss_dict["Enabled"] = "Yes" if self.model.in_serivce else "No" \ No newline at end of file + self.opendss_dict["Enabled"] = "Yes" if self.model.in_serivce else "No" diff --git a/src/ditto/writers/opendss/components/distribution_load.py b/src/ditto/writers/opendss/components/distribution_load.py index 465e3b0..f619297 100644 --- a/src/ditto/writers/opendss/components/distribution_load.py +++ b/src/ditto/writers/opendss/components/distribution_load.py @@ -1,4 +1,4 @@ -from gdm import ConnectionType +from gdm.distribution.enums import ConnectionType from gdm.quantities import ActivePower, ReactivePower diff --git a/src/ditto/writers/opendss/write.py b/src/ditto/writers/opendss/write.py index f9ac130..28afdf9 100644 --- a/src/ditto/writers/opendss/write.py +++ b/src/ditto/writers/opendss/write.py @@ -6,7 +6,8 @@ from gdm.distribution.components.base.distribution_component_base import DistributionComponentBase from gdm.distribution.equipment.concentric_cable_equipment import ConcentricCableEquipment from gdm.distribution.equipment.bare_conductor_equipment import BareConductorEquipment -from gdm import DistributionBus, MatrixImpedanceSwitch +from gdm.distribution.components.distribution_bus import DistributionBus +from gdm.distribution.components.matrix_impedance_switch import MatrixImpedanceSwitch from altdss_schema import altdss_models from loguru import logger From 83847ceface457db325621d47602b3eea6096a5f Mon Sep 17 00:00:00 2001 From: tarekelgindy Date: Tue, 6 May 2025 15:36:18 -0600 Subject: [PATCH 11/50] fixing breaking changes from GDM updates in cyme reader --- src/ditto/readers/cyme/components/distribution_bus.py | 3 ++- src/ditto/readers/cyme/components/distribution_capacitor.py | 2 +- src/ditto/readers/cyme/components/distribution_load.py | 6 +++--- src/ditto/readers/cyme/equipment/capacitor_equipment.py | 3 +-- src/ditto/readers/cyme/equipment/load_equipment.py | 2 +- src/ditto/readers/cyme/reader.py | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/ditto/readers/cyme/components/distribution_bus.py b/src/ditto/readers/cyme/components/distribution_bus.py index e75f537..a315c84 100644 --- a/src/ditto/readers/cyme/components/distribution_bus.py +++ b/src/ditto/readers/cyme/components/distribution_bus.py @@ -1,6 +1,7 @@ from infrasys.location import Location from gdm.distribution.components.distribution_bus import DistributionBus -from gdm import VoltageTypes, Phase, PositiveVoltage +from gdm.distribution.enums import VoltageTypes, Phase +from gdm.quantities import PositiveVoltage from ditto.readers.cyme.cyme_mapper import CymeMapper diff --git a/src/ditto/readers/cyme/components/distribution_capacitor.py b/src/ditto/readers/cyme/components/distribution_capacitor.py index 86515b8..84ae5e9 100644 --- a/src/ditto/readers/cyme/components/distribution_capacitor.py +++ b/src/ditto/readers/cyme/components/distribution_capacitor.py @@ -3,7 +3,7 @@ from ditto.readers.cyme.equipment.capacitor_equipment import CapacitorEquipmentMapper from gdm.distribution.components.distribution_bus import DistributionBus from gdm.distribution.components.distribution_capacitor import DistributionCapacitor -from gdm import Phase +from gdm.distribution.enums import Phase from loguru import logger class DistributionCapacitorMapper(CymeMapper): diff --git a/src/ditto/readers/cyme/components/distribution_load.py b/src/ditto/readers/cyme/components/distribution_load.py index 6f3e202..6a6d955 100644 --- a/src/ditto/readers/cyme/components/distribution_load.py +++ b/src/ditto/readers/cyme/components/distribution_load.py @@ -2,15 +2,15 @@ from ditto.readers.cyme.equipment.load_equipment import LoadEquipmentMapper from gdm.distribution.components.distribution_bus import DistributionBus from gdm.distribution.components.distribution_load import DistributionLoad -from gdm import Phase +from gdm.distribution.enums import Phase from loguru import logger class DistributionLoadMapper(CymeMapper): def __init__(self, system): super().__init__(system) - cyme_file = 'Network' - cyme_section = "LOAD EQUIVALENT" + cyme_file = 'Load' + cyme_section = 'LOADS' def parse(self, row): diff --git a/src/ditto/readers/cyme/equipment/capacitor_equipment.py b/src/ditto/readers/cyme/equipment/capacitor_equipment.py index 2f84969..a7dbc87 100644 --- a/src/ditto/readers/cyme/equipment/capacitor_equipment.py +++ b/src/ditto/readers/cyme/equipment/capacitor_equipment.py @@ -2,8 +2,7 @@ from gdm.quantities import PositiveReactivePower, PositiveResistance, PositiveReactance from gdm.distribution.equipment.phase_capacitor_equipment import PhaseCapacitorEquipment from gdm.distribution.equipment.capacitor_equipment import CapacitorEquipment -from gdm.distribution.distribution_enum import VoltageTypes -from gdm import ConnectionType +from gdm.distribution.enums import VoltageTypes, ConnectionType from gdm.quantities import PositiveVoltage class CapacitorEquipmentMapper(CymeMapper): diff --git a/src/ditto/readers/cyme/equipment/load_equipment.py b/src/ditto/readers/cyme/equipment/load_equipment.py index 6348132..65bac06 100644 --- a/src/ditto/readers/cyme/equipment/load_equipment.py +++ b/src/ditto/readers/cyme/equipment/load_equipment.py @@ -3,7 +3,7 @@ from gdm.distribution.equipment.load_equipment import LoadEquipment from gdm.distribution.equipment.phase_load_equipment import PhaseLoadEquipment from gdm.quantities import ActivePower, ReactivePower -from gdm.distribution.distribution_enum import ConnectionType +from gdm.distribution.enums import ConnectionType class LoadEquipmentMapper(CymeMapper): def __init__(self, system): diff --git a/src/ditto/readers/cyme/reader.py b/src/ditto/readers/cyme/reader.py index bb828a3..f3b273f 100644 --- a/src/ditto/readers/cyme/reader.py +++ b/src/ditto/readers/cyme/reader.py @@ -1,5 +1,5 @@ from gdm.distribution.components.base.distribution_component_base import DistributionComponentBase -from gdm import DistributionSystem +from gdm.distribution.distribution_system import DistributionSystem from ditto.readers.reader import AbstractReader from ditto.readers.cyme.utils import read_cyme_data import ditto.readers.cyme as cyme_mapper From 97a400dd73961d7f2ea72b23a65f18bba73cc7ea Mon Sep 17 00:00:00 2001 From: tarekelgindy Date: Wed, 2 Jul 2025 14:52:06 -0600 Subject: [PATCH 12/50] Current chagnes for cyme reader. Needs fixes --- src/ditto/readers/cyme/__init__.py | 7 + .../cyme/components/distribution_bus.py | 6 +- .../cyme/components/distribution_load.py | 60 +++-- .../components/distribution_transformer.py | 55 ++++ .../cyme/components/geometry_branch.py | 63 +++++ .../cyme/equipment/capacitor_equipment.py | 12 +- .../distribution_transformer_equipment.py | 254 ++++++++++++++++++ .../readers/cyme/equipment/geometry_branch.py | 23 ++ .../equipment/geometry_branch_equipment.py | 227 ++++++++++++++++ .../readers/cyme/equipment/load_equipment.py | 27 +- src/ditto/readers/cyme/reader.py | 51 +++- .../components/distribution_capacitor.py | 4 +- .../opendss/components/distribution_load.py | 2 +- 13 files changed, 744 insertions(+), 47 deletions(-) create mode 100644 src/ditto/readers/cyme/components/distribution_transformer.py create mode 100644 src/ditto/readers/cyme/components/geometry_branch.py create mode 100644 src/ditto/readers/cyme/equipment/distribution_transformer_equipment.py create mode 100644 src/ditto/readers/cyme/equipment/geometry_branch.py create mode 100644 src/ditto/readers/cyme/equipment/geometry_branch_equipment.py diff --git a/src/ditto/readers/cyme/__init__.py b/src/ditto/readers/cyme/__init__.py index e1be047..e65bd09 100644 --- a/src/ditto/readers/cyme/__init__.py +++ b/src/ditto/readers/cyme/__init__.py @@ -1,3 +1,10 @@ from ditto.readers.cyme.components.distribution_bus import DistributionBusMapper from ditto.readers.cyme.components.distribution_capacitor import DistributionCapacitorMapper from ditto.readers.cyme.components.distribution_load import DistributionLoadMapper +from ditto.readers.cyme.equipment.geometry_branch_equipment import BareConductorEquipmentMapper +from ditto.readers.cyme.equipment.geometry_branch_equipment import GeometryBranchEquipmentMapper +from ditto.readers.cyme.components.geometry_branch import GeometryBranchMapper +from ditto.readers.cyme.equipment.distribution_transformer_equipment import DistributionTransformerEquipmentMapper +from ditto.readers.cyme.equipment.distribution_transformer_equipment import WindingEquipmentMapper +from ditto.readers.cyme.components.distribution_transformer import DistributionTransformerMapper + diff --git a/src/ditto/readers/cyme/components/distribution_bus.py b/src/ditto/readers/cyme/components/distribution_bus.py index a315c84..1c82724 100644 --- a/src/ditto/readers/cyme/components/distribution_bus.py +++ b/src/ditto/readers/cyme/components/distribution_bus.py @@ -15,13 +15,13 @@ def __init__(self, cyme_model): def parse(self, row, from_node_sections, to_node_sections): name = self.map_name(row) coordinate = self.map_coordinate(row) - nominal_voltage = self.map_nominal_voltage(row) + rated_voltage = self.map_rated_voltage(row) phases = self.map_phases(row, from_node_sections, to_node_sections) voltage_limits = self.map_voltagelimits(row) voltage_type = self.map_voltage_type(row) return DistributionBus(name=name, coordinate=coordinate, - nominal_voltage=nominal_voltage, + rated_voltage=rated_voltage, phases=phases, voltagelimits=voltage_limits, voltage_type=voltage_type) @@ -35,7 +35,7 @@ def map_coordinate(self, row): crs = None return Location(x=X, y=Y, crs=crs) - def map_nominal_voltage(self, row): + def map_rated_voltage(self, row): #return PositiveVoltage(float(row['UserDefinedBaseVoltage']), "kilovolts") return PositiveVoltage(12.47, "kilovolts") diff --git a/src/ditto/readers/cyme/components/distribution_load.py b/src/ditto/readers/cyme/components/distribution_load.py index 6a6d955..594a6d4 100644 --- a/src/ditto/readers/cyme/components/distribution_load.py +++ b/src/ditto/readers/cyme/components/distribution_load.py @@ -1,3 +1,4 @@ +from ditto.readers.cyme.utils import read_cyme_data from ditto.readers.cyme.cyme_mapper import CymeMapper from ditto.readers.cyme.equipment.load_equipment import LoadEquipmentMapper from gdm.distribution.components.distribution_bus import DistributionBus @@ -10,14 +11,14 @@ def __init__(self, system): super().__init__(system) cyme_file = 'Load' - cyme_section = 'LOADS' + cyme_section = 'CUSTOMER LOADS' - def parse(self, row): + def parse(self, row, section_id_sections, load_file): name = self.map_name(row) - bus = self.map_bus(row) + bus = self.map_bus(row, section_id_sections) phases = self.map_phases(row) - equipment = self.map_equipment(row) + equipment = self.map_equipment(row, load_file) if len(phases) == 0: logger.warning(f"Load {name} has no kW values. Skipping...") return None @@ -27,9 +28,32 @@ def parse(self, row): equipment=equipment) def map_name(self, row): - return row["LoadModelName"] + load_phase = row["LoadPhase"] + return row["DeviceNumber"]+"_"+str(load_phase) + + def map_bus(self, row, section_id_sections): + section_id = row['SectionID'] + section = section_id_sections[section_id] + from_bus_name = section["FromNodeID"] + to_bus_name = section["ToNodeID"] + to_bus = None + from_bus = None + try: + from_bus = self.system.get_component(component_type=DistributionBus,name=from_bus_name) + except Exception as e: + pass + + try: + to_bus = self.system.get_component(component_type=DistributionBus,name=to_bus_name) + except Exception as e: + pass + + if from_bus is None: + return to_bus + if from_bus is None: + logger.warning(f"Load {section_id} has no bus") + return from_bus - def map_bus(self, row): bus_name = row["NodeID"] bus = None try: @@ -43,15 +67,21 @@ def map_bus(self, row): def map_phases(self, row): phases = [] - if row['Value1A'] is not None: - phases.append(Phase.A) - if row['Value1B'] is not None: - phases.append(Phase.B) - if row['Value1C'] is not None: - phases.append(Phase.C) + if row['LoadPhase'] is not None: + phase = row['LoadPhase'] + if phase == 'A': + phases.append(Phase.A) + elif phase == 'B': + phases.append(Phase.B) + elif phase == 'C': + phases.append(Phase.C) return phases - def map_equipment(self, row): + def map_equipment(self, row, load_file): mapper = LoadEquipmentMapper(self.system) - equipment = mapper.parse(row) - return equipment + equipment_data = read_cyme_data(load_file, mapper.cyme_section) + for idx, equipment_row in equipment_data.iterrows(): + if equipment_row['DeviceNumber'] == row['DeviceNumber']: + equipment = mapper.parse(equipment_row, row) + return equipment + return None diff --git a/src/ditto/readers/cyme/components/distribution_transformer.py b/src/ditto/readers/cyme/components/distribution_transformer.py new file mode 100644 index 0000000..e41a16c --- /dev/null +++ b/src/ditto/readers/cyme/components/distribution_transformer.py @@ -0,0 +1,55 @@ +from loguru import logger +from ditto.readers.cyme.cyme_mapper import CymeMapper +from gdm.distribution.components.distribution_transformer import DistributionTransformer +from gdm.distribution.equipment.distribution_transformer_equipment import DistributionTransformerEquipment +from gdm.quantities import ActivePower, ReactivePower +from gdm.distribution.enums import ConnectionType + +class DistributionTransformerMapper(CymeMapper): + def __init__(self, system): + super().__init__(system) + + cyme_file = 'Network' + cyme_section = 'TRANSFORMER SETTING' + + def parse(self, network_row, section_id_sections): + name = self.map_name(row) + buses = self.map_buses(row, section_id_sections) + winding_phases = self.map_winding_phases(row) + equipment = self.map_equipment(row, network_row) + return DistributionTransformerEquipment(name=name, + buses=buses, + winding_phases=winding_phases, + equipment=equipment) + + def map_name(self, row): + name = row['SectionID'] + return name + + def map_buses(self, row, section_id_sections): + section_id = str(row['SectionID']) + section = section_id_sections[section_id] + from_bus_name = section['FromNodeID'] + to_bus_name = section['ToNodeID'] + + from_bus = self.system.get_component(component_type=DistributionBus, name=from_bus_name) + to_bus = self.system.get_component(component_type=DistributionBus, name=to_bus_name) + return [from_bus, to_bus] + + def map_winding_phases(self, row): + section_id = str(row['SectionID']) + section = section_id_sections[section_id] + phase = section['Phase'] + phases = [] + if 'A' in phase: + phases.append(Phase.A) + if 'B' in phase: + phases.append(Phase.B) + if 'C' in phase: + phases.append(Phase.C) + return winding_phases + + def map_equipment(self, row, network_row): + transformer_id = row['EqID'] + equipment = self.system.get_component(component_type=DistributionTransformerEquipment, name=transformer_id) + return equipment diff --git a/src/ditto/readers/cyme/components/geometry_branch.py b/src/ditto/readers/cyme/components/geometry_branch.py new file mode 100644 index 0000000..763dc9b --- /dev/null +++ b/src/ditto/readers/cyme/components/geometry_branch.py @@ -0,0 +1,63 @@ +from gdm.quantities import Distance +from ditto.readers.cyme.cyme_mapper import CymeMapper +from ditto.readers.cyme.equipment.geometry_branch_equipment import GeometryBranchEquipment +from gdm.distribution.components.geometry_branch import GeometryBranch +from gdm.distribution.components.distribution_bus import DistributionBus +from gdm.distribution.enums import Phase + + + +class GeometryBranchMapper(CymeMapper): + def __init__(self, system): + super().__init__(system) + + cyme_file = 'Network' + cyme_section = 'OVERHEADLINE SETTING' + + def parse(self, row, section_id_sections): + name = self.map_name(row) + buses = self.map_buses(row,section_id_sections) + length = self.map_length(row) + phases = self.map_phases(row, section_id_sections) + equipment = self.map_equipment(row) + return GeometryBranch(name=name, + buses=buses, + length=length, + phases=phases, + equipment=equipment) + + def map_name(self, row): + name = row['SectionID'] + return name + + def map_buses(self, row, section_id_sections): + section_id = str(row['SectionID']) + section = section_id_sections[section_id] + from_bus_name = section['FromNodeID'] + to_bus_name = section['ToNodeID'] + + from_bus = self.system.get_component(component_type=DistributionBus, name=from_bus_name) + to_bus = self.system.get_component(component_type=DistributionBus, name=to_bus_name) + return [from_bus, to_bus] + + def map_length(self, row): + length = Distance(float(row['Length']),'mile').to('km') + return length + + def map_phases(self, row, section_id_sections): + section_id = str(row['SectionID']) + section = section_id_sections[section_id] + phase = section['Phase'] + phases = [] + if 'A' in phase: + phases.append(Phase.A) + if 'B' in phase: + phases.append(Phase.B) + if 'C' in phase: + phases.append(Phase.C) + return phases + + def map_equipment(self, row): + line_id = row['LineCableID'] + line = self.system.get_component(component_type=GeometryBranchEquipment, name=line_id) + return line diff --git a/src/ditto/readers/cyme/equipment/capacitor_equipment.py b/src/ditto/readers/cyme/equipment/capacitor_equipment.py index a7dbc87..2e0561e 100644 --- a/src/ditto/readers/cyme/equipment/capacitor_equipment.py +++ b/src/ditto/readers/cyme/equipment/capacitor_equipment.py @@ -14,18 +14,18 @@ def __init__(self, system): def parse(self, row, connection): name = self.map_name(row) - nominal_voltage = self.map_nominal_voltage(row) + rated_voltage = self.map_rated_voltage(row) phase_capacitors = self.map_phase_capacitors(row) voltage_type = self.map_voltage_type(connection) return CapacitorEquipment(name=name, phase_capacitors=phase_capacitors, - nominal_voltage=nominal_voltage, + rated_voltage=rated_voltage, voltage_type=voltage_type) def map_name(self, row): return row["ID"] - def map_nominal_voltage(self, row): + def map_rated_voltage(self, row): return PositiveVoltage(float(row["KV"]), "kilovolt") def map_phase_capacitors(self, row): @@ -54,9 +54,9 @@ def __init__(self, system, num_banks_on): def parse(self, row, phase): name = self.map_name(row, phase) - rated_capacity = self.map_rated_capacity(row) + rated_reactive_power = self.map_rated_reactive_power(row) return PhaseCapacitorEquipment(name = name, - rated_capacity=rated_capacity, + rated_reactive_power=rated_reactive_power, num_banks_on=self.num_banks_on) def map_name(self, row, phase): @@ -68,5 +68,5 @@ def map_name(self, row, phase): return row["ID"] + "_C" - def map_rated_capacity(self, row): + def map_rated_reactive_power(self, row): return PositiveReactivePower(float(row["KVAR"]),'kilovar') diff --git a/src/ditto/readers/cyme/equipment/distribution_transformer_equipment.py b/src/ditto/readers/cyme/equipment/distribution_transformer_equipment.py new file mode 100644 index 0000000..4190829 --- /dev/null +++ b/src/ditto/readers/cyme/equipment/distribution_transformer_equipment.py @@ -0,0 +1,254 @@ +from loguru import logger +from ditto.readers.cyme.cyme_mapper import CymeMapper +from gdm.distribution.equipment.distribution_transformer_equipment import DistributionTransformerEquipment +from gdm.distribution.equipment.distribution_transformer_equipment import WindingEquipment +from gdm.quantities import ActivePower, ReactivePower +from gdm.distribution.enums import ConnectionType + +class DistributionTransformerEquipmentMapper(CymeMapper): + def __init__(self, system): + super().__init__(system) + + cyme_file = 'Equipment' + cyme_section = 'TRANSFORMER' + + def parse(self, row, network_row): + name = self.map_name(row) + pct_no_load_loss = self.map_pct_no_load_loss(row) + pct_full_load_loss = self.map_pct_full_load_loss(row) + windings = self.map_windings(row, network_row) + winding_reactances = self.map_winding_reactances(row) + is_center_tapped = self.map_is_center_tapped(row) + return DistributionTransformerEquipment(name=name, + pct_no_load_loss=pct_no_load_loss, + pct_full_load_loss=pct_full_load_loss, + windings=windings, + winding_reactances=winding_reactances, + is_center_tapped=is_center_tapped) + + def map_name(self, row): + name = row['ID'] + return name + + def map_pct_no_load_loss(self, row): + no_load_loss = float(row['NoLoadLosses']) + kva = float(row['KVA']) + pct_no_load_loss = no_load_loss/kva*100 + return pct_no_load_loss + + def map_pct_full_load_loss(self, row): + # Need to compute rated current and rated resistance to compute full load loss + rated_current_sec = float(row['KVA'])/float(row['KVLLsec']) + resistance_pu = float(row['Z1'])/float((1+float(row['XR'])**2)**0.5) + resistance_sec = float(resistance_pu)*float(float(row['KVLLsec'])**2) / float(row['KVA']) + pct_full_load_loss = rated_current_sec*resistance_sec + return pct_full_load_loss + + def map_winding_reactances(self, row): + xr_ratio = row['XR'] + rx_ratio = 1/xr_ratio + reactance_pu = row['Z1']/((1+rx_ratio**2)**0.5) + transformer_type = row['Type'] + if transformer_type == '4': + winding_reactances = [reactance_pu, reactance_pu, reactance_pu] + else: + winding_reactances = [rectance_pu] + return winding_reactances + + def map_is_center_tapped(self, row): + transformer_type = row['Type'] + if transformer_type == '4': + return True + return False + + def map_windings(self, row, network_row): + windings = [] + is_center_tapped = False + if row['Type'] == '4': + is_center_tapped = True + + winding_mapper1 = WindingEquipmentMapper(self.system) + import pdb;pdb.set_trace() + winding_1 = winding_mapper1.parse(row, network_row, winding_number=1) + winding_mapper2 = WindingEquipmentMapper(self.system) + winding_2 = winding_mapper2.parse(row, network_row, winding_number=2) + windings.append(winding_1) + windings.append(winding_2) + if is_center_tapped: + winding_mapper3 = WindingEquipmentMapper(self.system) + winding_3 = winding_mapper3.parse(row, network_row, winding_number=3) + windings.append(winding_3) + return windings + +def WindingEquipmentMapper(CymeMapper): + def __init__(self, system): + super().__init__(system) + + cyme_file = 'Equipment' + cyme_section = 'TRANSFORMER' + connection_map = { + 0: 'Y_Y', + 1: 'D_Y', + 2: 'D_Y', + 3: 'YNG_YNG', + 4: 'D_D', + 5: 'DO_DO', + 6: 'YO_DO', + 7: 'D_YNG', + 8: 'YNG_D', + 9: 'Y_YNG', + 10: 'YNG_Y', + 11: 'Yg_Zg', + 12: 'D_Zg' + } + + + def parse(self, row, winding_number, network_row): + name = self.map_name(row) + resistance = self.map_resistance(row, winding_number) + is_grounded = self.map_is_grounded(row, winding_number) + rated_voltage = self.map_rated_voltage(row, winding_number) + voltage_type = self.map_voltage_type(row) + rated_power = self.map_rated_power(row) + num_phases = self.map_num_phases(row) + connection_type = self.map_connection_type(row, winding_number) + tap_positions = self.map_tap_positions(row, winding_number, network_row) + total_taps = self.map_total_taps(row) + min_tap_pu = self.map_min_tap_pu(row) + max_tap_pu = self.map_max_tap_pu(row) + return WindingEquipment(name=name, + resistance=resistance, + is_grounded=is_grounded, + rated_voltage=rated_voltage, + voltage_type=voltage_type, + rated_power=rated_power, + num_phases=num_phases, + connection_type=connection_type, + tap_positions=tap_positions, + total_taps=total_taps, + min_tap_pu=min_tap_pu, + max_tap_pu=max_tap_pu) + + def map_name(self, row): + name = row['ID'] + return name + + def map_resistance(self, row, winding_number): + + if winding_number == 1: + resistance = resistance_pu*row['KVLLprim']**2 / row['KVA'] + elif winding_number == 2: + resistance = resistance_pu*row['KVLLsec']**2 / row['KVA'] + elif winding_number == 3: + resistance = resistance_pu*row['KVLLsec']**2 / row['KVA'] + return resistance + + def map_is_grounded(self, row, winding_number): + + connection_type = row['Conn'] + if winding_number == 1: + winding = 0 + elif winding_number == 2: + winding = 1 + elif winding_number == 3: + winding = 1 + winding_type = self.connection_map[connection_type].split('_')[winding] + if 'YNG' in winding_type: + grounded = False + elif 'D' in winding_type: + grounded = False + elif 'YO' in winding_type: + grounded = False + else: + grounded = True + return is_grounded + + def map_rated_voltage(self, row, winding_number): + if winding_number == 1: + voltage = row['KVLLprim'] + elif winding_number == 2: + voltage = row['KVLLsec'] + elif winding_number == 3: + voltage = row['KVLLsec'] + + voltage = PositiveVoltage(float(voltage), "kilovolt") + return voltage + + def map_voltage_type(self, row): + return VoltageTypes.LINE_TO_LINE + + def map_rated_power(self, row): + power = row['KVA'] + power = ActivePower(float(power), "kilowatt") + return power + + def map_num_phases(self, row): + phase_type = row['Type'] + if phase_type == 1 or phase_type == 4: + num_phaes = 1 + else: + num_phases = 3 + return num_phases + + def map_connection_type(self, row, winding_number): + if winding_number == 1: + connection = row['Conn'] + elif winding_number == 2: + connection = row['Conn'] + elif winding_number == 3: + connection = row['Conn'] + + winding_type = self.connection_map[connection_type].split('_')[winding] + if winding_type == 'YO': + connection_type = 'OPEN_STAR' + elif winding_type == 'DO': + connection_type = 'OPEN_DELTA' + elif 'Z' in winding_type: + connection_type = 'ZIG_ZAG' + elif 'Y' in winding_type: + connection_type = 'STAR' + elif 'D' in winding_type: + connection_type = 'DELTA' + else: + raise ValueError("Unknown winding type: {}".format(winding_type)) + + return ConnectionType(connection_type) + + def map_tap_positions(self, row, winding_number, network_row): + phase_type = row['Type'] + if phase_type == 1 or phase_type == 4: + num_phaes = 1 + else: + num_phases = 3 + + tap_positions = [] + if winding_number == 1: + if network_row is None: + tap = 100.0 + else: + tap = network_row['PrimTap'] + elif winding_number == 2: + if network_row is None: + tap = 100.0 + else: + tap = network_row['SecondaryTap'] + elif winding_number == 3: + if network_row is None: + tap = 100.0 + else: + tap = network_row['SecondaryTap'] + for phase in range(1, num_phases + 1): + tap_positions.append(tap) + return tap_positions + + def map_total_taps(self, row): + total_taps = row['Tap'] + return total_taps + + def min_tap_pu(self, row): + min_tap_pu = row['LowerBandwidth'] + return min_tap_pu + + def max_tap_pu(self, row): + max_tap_pu = row['UpperBandwidth'] + return max_tap_pu diff --git a/src/ditto/readers/cyme/equipment/geometry_branch.py b/src/ditto/readers/cyme/equipment/geometry_branch.py new file mode 100644 index 0000000..f82b100 --- /dev/null +++ b/src/ditto/readers/cyme/equipment/geometry_branch.py @@ -0,0 +1,23 @@ +from ditto.readers.cyme.cyme_mapper import CymeMapper + +class BareConductorElement(CymeMapper): + def __init__(self, system): + super().__init__(system) + cyme_file = 'Equipment' + cyme_section = 'CABLE' + + def parse(self, row): + name = self.map_name(row) + + return + +class GeometryBranch(CymeMapper): + def __init__(self, system): + super().__init__(system) + cyme_file = 'Equipment' + cyme_section = 'CABLE' + + def parse(self, row): + name = self.map_name(row) + + diff --git a/src/ditto/readers/cyme/equipment/geometry_branch_equipment.py b/src/ditto/readers/cyme/equipment/geometry_branch_equipment.py new file mode 100644 index 0000000..ad86306 --- /dev/null +++ b/src/ditto/readers/cyme/equipment/geometry_branch_equipment.py @@ -0,0 +1,227 @@ +from gdm.quantities import PositiveCurrent, PositiveDistance, PositiveResistancePULength, Distance +from ditto.readers.cyme.utils import read_cyme_data +from ditto.readers.cyme.cyme_mapper import CymeMapper +from gdm.distribution.equipment.geometry_branch_equipment import GeometryBranchEquipment +from gdm.distribution.equipment.bare_conductor_equipment import BareConductorEquipment + + +class GeometryBranchEquipmentMapper(CymeMapper): + def __init__(self, system): + super().__init__(system) + + cyme_file = 'Equipment' + cyme_section = 'LINE' + + def parse(self, row, equipment_file): + name = self.map_name(row) + conductors = self.map_conductors(row, equipment_file) + horizontal_positions = self.map_horizontal_positions(row, equipment_file) + vertical_positions = self.map_vertical_positions(row, equipment_file) + + return GeometryBranchEquipment(name=name, + conductors=conductors, + horizontal_positions=horizontal_positions, + vertical_positions=vertical_positions) + + def map_name(self, row): + name = row['ID'] + return name + + def map_vertical_positions(self, row, equipment_file): + spacing_id = row['SpacingID'] + spacing_ids = read_cyme_data(equipment_file,"SPACING TABLE FOR LINE") + for idx, row in spacing_ids.iterrows(): + if row['ID'] == spacing_id: + vertical_positions = [] + spacing = row + cond1_y = spacing['PosOfCond1_Y'] + cond2_y = spacing['PosOfCond2_Y'] + cond3_y = spacing['PosOfCond3_Y'] + neutral_y = spacing['PosOfNeutralCond_Y'] + if cond1_y != "": + y1 = float(cond1_y) + vertical_positions.append(y1) + if cond2_y != "": + y2 = float(cond2_y) + vertical_positions.append(y2) + if cond3_y != "": + y3 = float(cond3_y) + vertical_positions.append(y3) + if neutral_y != "": + y_n = float(neutral_y) + vertical_positions.append(y_n) + return Distance(vertical_positions, 'feet').to('m') + return None + + def map_horizontal_positions(self, row, equipment_file): + spacing_id = row['SpacingID'] + spacing_ids = read_cyme_data(equipment_file,"SPACING TABLE FOR LINE") + for idx, row in spacing_ids.iterrows(): + if row['ID'] == spacing_id: + spacing = row + horizontal_positions = [] + cond1_x = spacing['PosOfCond1_X'] + cond2_x = spacing['PosOfCond2_X'] + cond3_x = spacing['PosOfCond3_X'] + neutral_x = spacing['PosOfNeutralCond_X'] + if cond1_x != "": + x1 = float(cond1_x) + horizontal_positions.append(x1) + if cond2_x != "": + x2 = float(cond2_x) + horizontal_positions.append(x2) + if cond3_x != "": + x3 = float(cond3_x) + horizontal_positions.append(x3) + if neutral_x != "": + x_n = float(neutral_x) + horizontal_positions.append(x_n) + return Distance(horizontal_positions, 'feet').to('m') + return None + + def map_conductors(self,row, equipment_file): + phase_conductor_name = row['PhaseCondID'] + neutral_conductor_name = row['NeutralCondID'] + try: + phase_conductor = self.system.get_component(component_type=BareConductorEquipment, name=phase_conductor_name) + except Exception as e: + phase_conductor = self.system.get_component(component_type=BareConductorEquipment, name="Default") + try: + neutral_conductor = self.system.get_component(component_type=BareConductorEquipment, name=neutral_conductor_name) + except: + neutral_conductor = self.system.get_component(component_type=BareConductorEquipment, name="Default") + + spacing_id = row['SpacingID'] + spacing_ids = read_cyme_data(equipment_file,"SPACING TABLE FOR LINE") + for idx, row in spacing_ids.iterrows(): + if row['ID'] == spacing_id: + spacing = row + conductors = [] + cond1 = spacing['PosOfCond1_X'] + cond2 = spacing['PosOfCond2_X'] + cond3 = spacing['PosOfCond3_X'] + neutral = spacing['PosOfNeutralCond_X'] + if cond1 != "": + conductors.append(phase_conductor) + if cond2 != "": + conductors.append(phase_conductor) + if cond3 != "": + conductors.append(phase_conductor) + if neutral != "": + conductors.append(neutral_conductor) + return conductors + return None + +class ConcentricCableEquipmentMapper(CymeMapper): + def __init__(self, system): + super().__init__(system) + + cyme_file = 'Equipment' + cyme_section = 'CABLE' + + def parse(self, row): + name = self.map_name(row) + strand_diameter = self.map_strand_diameter(row) + conductor_diameter = self.map_conductor_diameter(row) + cable_diameter = self.map_cable_diameter(row) + insulation_thickness = self.map_insulation_thickness(row) + insulation_diameter = self.map_insulation_diameter(row) + amapcity = self.map_ampacity(row) + conductor_gmr = self.map_conductor_gmr(row) + strand_gmr = self.map_strand_gmr(row) + phase_ac_resistance = self.map_phase_ac_resistance(row) + strand_ac_resistance = self.map_strand_ac_resistance(row) + num_neutral_strands = self.map_num_neutral_strands(row) + rated_voltage = self.map_rated_voltage(row) + return ConcentricCableEquipment(name=name, + strand_diameter=strand_diameter, + conductor_diameter=conductor_diameter, + cable_diameter=cable_diameter, + insulation_thickness=insulation_thickness, + insulation_diameter=insulation_diameter, + ampacity=amapcity, + conductor_gmr=conductor_gmr, + strand_gmr=strand_gmr, + phase_ac_resistance=phase_ac_resistance, + strand_ac_resistance=strand_ac_resistance, + num_neutral_strands=num_neutral_strands, + rated_voltage=rated_voltage) + + def map_name(self, row): + name = row['ID'] + return name + + def map_conductor_diameter(self, row): + cable_name = row['ID'] + conductor_info = read_cyme_data(self.system.equipment_file,"CABLE CONDUCTOR") + for idx, row in conductor_info.iterrows(): + if row['ID'] == cable_name: + conductor_diameter = float(row['Diameter']) + return PositiveDistance(conductor_diameter,'inch').to('mm') + return None + + + def map_strand_diameter(self, row): + cable_name = row['ID'] + strand_info = read_cyme_data(self.system.equipment_file,"CABLE CONCENTRIC NEUTRAL") + for idx, row in strand_info.iterrows(): + if row['ID'] == cable_name: + strand_diameter = float(row['Diameter']) + return PositiveDistance(strand_diameter,'inch').to('mm') + return None + + def map_ampacity(self, row): + ampacity = PositiveCurrent(float(row['Amps']),'amp') + return ampacity + +class BareConductorEquipmentMapper(CymeMapper): + def __init__(self, system): + super().__init__(system) + + cyme_file = 'Equipment' + cyme_section = 'CONDUCTOR' + + def parse(self, row): + name = self.map_name(row) + conductor_diameter = self.map_conductor_diameter(row) + conductor_gmr = self.map_conductor_gmr(row) + ampacity = self.map_ampacity(row) + emergency_ampacity = self.map_emergency_ampacity(row) + ac_resistance = self.map_ac_resistance(row) + dc_resistance = self.map_dc_resistance(row) + return BareConductorEquipment(name=name, + conductor_diameter=conductor_diameter, + conductor_gmr=conductor_gmr, + ampacity=ampacity, + emergency_ampacity=emergency_ampacity, + ac_resistance=ac_resistance, + dc_resistance=dc_resistance) + + def map_name(self, row): + name = row['ID'] + return name + + def map_conductor_diameter(self, row): + conductor_diameter = float(row['Diameter']) + return PositiveDistance(conductor_diameter,'inch').to('mm') + + def map_conductor_gmr(self, row): + conductor_gmr = PositiveDistance(float(row['GMR']),'inch').to('mm') + return conductor_gmr + + def map_ampacity(self, row): + ampacity = PositiveCurrent(float(row['Amps']),'amp') + return ampacity + + def map_emergency_ampacity(self, row): + emergency_ampacity = PositiveCurrent(float(row['Amps_4']),'amp') + return emergency_ampacity + + def map_ac_resistance(self, row): + ac_resistance = PositiveResistancePULength(float(row['R25']),'ohm/mile').to('ohm/km') + return ac_resistance + + def map_dc_resistance(self, row): + dc_resistance = PositiveResistancePULength(float(row['R25']),'ohm/mile').to('ohm/km') + return dc_resistance + diff --git a/src/ditto/readers/cyme/equipment/load_equipment.py b/src/ditto/readers/cyme/equipment/load_equipment.py index 65bac06..efed37f 100644 --- a/src/ditto/readers/cyme/equipment/load_equipment.py +++ b/src/ditto/readers/cyme/equipment/load_equipment.py @@ -12,15 +12,17 @@ def __init__(self, system): cyme_file = 'Load' cyme_section = 'LOADS' - def parse(self, row): + def parse(self, row, network_row): name = self.map_name(row) + # Connection is not included in LOOADS but in CONSUMER LOADS connection_type = self.map_connection_type(row) - phase_loads = self.map_phase_loads(row) + phase_loads = self.map_phase_loads(network_row) return LoadEquipment(name=name, - phase_loads=phase_loads) + phase_loads=phase_loads, + connection_type=connection_type) def map_name(self, row): - return row['LoadModelName'] + return row['DeviceNumber'] def map_connection_type(self, row): connection_number = int(row['Connection']) @@ -29,7 +31,7 @@ def map_connection_type(self, row): 1: ConnectionType.STAR, #Y 2: ConnectionType.DELTA, #Delta 3: ConnectionType.OPEN_DELTA, #Open Delta - 4: ConnectinType.DELTA, #Closed Delta + 4: ConnectionType.DELTA, #Closed Delta 5: ConnectionType.ZIG_ZAG, # Zg 6: ConnectionType.STAR, # CT 7: ConnectionType.DELTA, # Dg - Not sure what this is? @@ -39,12 +41,9 @@ def map_connection_type(self, row): def map_phase_loads(self, row): # Get the PhaseLoadEquipment with the same name as the Load name = row['DeviceNumber'] - phase_loads = [] - for phase in ['A', 'B', 'C']: - phase_load_name = name + '_' + phase - phase_load = self.system.get_component(component_type=PhaseLoadEquipment, name=phase_load_name) - if phase_load is not None: - phase_loads.append(phase_load) + phase_load_equipment_mapper = PhaseLoadEquipmentMapper(self.system) + phase_load_equipment = phase_load_equipment_mapper.parse(row) + phase_loads = [phase_load_equipment] return phase_loads class PhaseLoadEquipmentMapper(CymeMapper): @@ -56,8 +55,8 @@ def __init__(self, system): def parse(self, row): name = self.map_name(row) - real_power = map_real_power(row), - reactive_power = map_reactive_power(row), + real_power = self.map_real_power(row) + reactive_power = self.map_reactive_power(row) z_real = self.map_z_real(row) z_imag = self.map_z_imag(row) i_real = self.map_i_real(row) @@ -90,10 +89,12 @@ def compute_powers(self, row): kvar = v2 # kva and pf elif value_type == 1: + v2 = v2/100.0 kw = v1 * v2 kvar = v1 * (1 - v2**2) ** 0.5 # kw and pf elif value_type == 2: + v2 = v2/100.0 kw = v1 kvar = v1 * (1/v2**2 - 1) ** 0.5 # amp and pf diff --git a/src/ditto/readers/cyme/reader.py b/src/ditto/readers/cyme/reader.py index f3b273f..2a8dd21 100644 --- a/src/ditto/readers/cyme/reader.py +++ b/src/ditto/readers/cyme/reader.py @@ -1,3 +1,5 @@ +from gdm.quantities import Distance, PositiveCurrent, PositiveResistancePULength +from gdm.distribution.equipment.bare_conductor_equipment import BareConductorEquipment from gdm.distribution.components.base.distribution_component_base import DistributionComponentBase from gdm.distribution.distribution_system import DistributionSystem from ditto.readers.reader import AbstractReader @@ -11,19 +13,34 @@ class Reader(AbstractReader): component_types = [ "DistributionBus", "DistributionCapacitor", - "DistributionLoad" + "DistributionLoad", + "BareConductorEquipment", + "GeometryBranchEquipment", + "GeometryBranch", + "DistributionTransformerEquipment", + "DistributionTransformer" ] - def __init__(self, network_file, equipment_file): + def __init__(self, network_file, equipment_file, load_file): self.system = DistributionSystem(auto_add_composed_components=True) - self.read(network_file, equipment_file) + self.read(network_file, equipment_file, load_file) - def read(self, network_file, equipment_file): + def read(self, network_file, equipment_file, load_file): # Section data read separately as it links to other tables section_id_sections = {} from_node_sections = {} to_node_sections = {} + default_conductor = BareConductorEquipment( + name="Default", + conductor_diameter=Distance(0.368000,'inch').to('mm'), + conductor_gmr=Distance(0.133200,'inch').to('mm'), + ampacity=PositiveCurrent(600.0,'amp'), + emergency_ampacity=PositiveCurrent(600.0,'amp'), + ac_resistance=PositiveResistancePULength(0.555000,'ohm/mile').to('ohm/km'), + dc_resistance=PositiveResistancePULength(0.555000,'ohm/mile').to('ohm/km'), + ) + self.system.add_component(default_conductor) section_data = read_cyme_data(network_file,"SECTION") for idx, row in section_data.iterrows(): @@ -54,21 +71,41 @@ def read(self, network_file, equipment_file): data = read_cyme_data(network_file, cyme_section) elif cyme_file == "Equipment": data = read_cyme_data(equipment_file, cyme_section) + elif cyme_file == "Load": + data = read_cyme_data(load_file, cyme_section) else: raise ValueError(f"Unknown CYME file {cyme_file}") + transformer_network_data = read_cyme_data(network_file, 'TRANSFORMER SETTING') + transformer_map = {} + for idx, row in transformer_network_data.iterrows(): + transformer_type = row['EqID'] + transformer_map[transformer_type] = row + + components = [] for idx, row in data.iterrows(): mapper_name = component_type + "Mapper" if mapper_name == "DistributionCapacitorMapper": model_entry = mapper.parse(row, section_id_sections, equipment_file) - if mapper_name == "DistributionBusMapper": + elif mapper_name == "DistributionBusMapper": model_entry = mapper.parse(row, from_node_sections, to_node_sections) - if mapper_name == "DistributionLoadMapper": + elif mapper_name == "DistributionLoadMapper": + model_entry = mapper.parse(row, section_id_sections, load_file) + elif mapper_name == "GeometryBranchMapper": + model_entry = mapper.parse(row, section_id_sections) + elif mapper_name == "GeometryBranchEquipmentMapper": + model_entry = mapper.parse(row, equipment_file) + elif mapper_name == "DistributionTransformerEquipmentMapper": + network_row = None + if row['ID'] in transformer_map: + network_row = transformer_map[row['ID']] + model_entry = mapper.parse(row, network_row) + else: model_entry = mapper.parse(row) if model_entry is not None: components.append(model_entry) - self.system.add_component(model_entry) + self.system.add_components(*components) def get_system(self) -> DistributionSystem: return self.system diff --git a/src/ditto/writers/opendss/components/distribution_capacitor.py b/src/ditto/writers/opendss/components/distribution_capacitor.py index b9c885b..0cd2c0f 100644 --- a/src/ditto/writers/opendss/components/distribution_capacitor.py +++ b/src/ditto/writers/opendss/components/distribution_capacitor.py @@ -21,7 +21,7 @@ def map_bus(self): for phase in self.model.phases: self.opendss_dict["Bus1"] += self.phase_map[phase] # TODO: Should we include the phases its connected to here? - nom_voltage = self.model.bus.nominal_voltage.to("kV").magnitude + nom_voltage = self.model.bus.rated_voltage.to("kV").magnitude self.opendss_dict["kV"] = nom_voltage if num_phases == 1 else nom_voltage * 1.732 def map_phases(self): @@ -53,7 +53,7 @@ def map_equipment(self): total_resistance.append(phase_capacitor.resistance.to("ohm").magnitude) total_reactance.append(phase_capacitor.reactance.to("ohm").magnitude) total_rated_capacity.append( - phase_capacitor.rated_capacity.to("kvar").magnitude + phase_capacitor.rated_reactive_power.to("kvar").magnitude ) # from general capacitor equipment self.opendss_dict["States"] = [1] * num_banks self.opendss_dict["R"] = [sum(total_resistance) / num_banks] * num_banks diff --git a/src/ditto/writers/opendss/components/distribution_load.py b/src/ditto/writers/opendss/components/distribution_load.py index f619297..40de9cb 100644 --- a/src/ditto/writers/opendss/components/distribution_load.py +++ b/src/ditto/writers/opendss/components/distribution_load.py @@ -24,7 +24,7 @@ def map_bus(self): for phase in self.model.phases: self.opendss_dict["Bus1"] += self.phase_map[phase] # TODO: Should we include the phases its connected to here? - nom_voltage = self.model.bus.nominal_voltage.to("kV").magnitude + nom_voltage = self.model.bus.rated_voltage.to("kV").magnitude self.opendss_dict["kV"] = nom_voltage if num_phases == 1 else nom_voltage * 1.732 def map_phases(self): From 5c931b51e8510328bded23b7c980f40dce27ae94 Mon Sep 17 00:00:00 2001 From: "Zink, Zephyr" Date: Thu, 11 Sep 2025 13:11:15 -0600 Subject: [PATCH 13/50] Removing Positive Quantities --- .../cyme/components/distribution_bus.py | 8 ++--- .../cyme/equipment/capacitor_equipment.py | 8 ++--- .../readers/opendss/components/branches.py | 15 ++++---- src/ditto/readers/opendss/components/buses.py | 8 ++--- .../readers/opendss/components/cables.py | 34 +++++++++---------- .../readers/opendss/components/capacitors.py | 8 ++--- .../readers/opendss/components/conductors.py | 14 ++++---- .../readers/opendss/components/pv_systems.py | 6 ++-- .../opendss/components/transformers.py | 6 ++-- 9 files changed, 53 insertions(+), 54 deletions(-) diff --git a/src/ditto/readers/cyme/components/distribution_bus.py b/src/ditto/readers/cyme/components/distribution_bus.py index 1c82724..fd3e81d 100644 --- a/src/ditto/readers/cyme/components/distribution_bus.py +++ b/src/ditto/readers/cyme/components/distribution_bus.py @@ -1,7 +1,7 @@ from infrasys.location import Location from gdm.distribution.components.distribution_bus import DistributionBus from gdm.distribution.enums import VoltageTypes, Phase -from gdm.quantities import PositiveVoltage +from gdm.quantities import Voltage from ditto.readers.cyme.cyme_mapper import CymeMapper @@ -37,7 +37,7 @@ def map_coordinate(self, row): def map_rated_voltage(self, row): #return PositiveVoltage(float(row['UserDefinedBaseVoltage']), "kilovolts") - return PositiveVoltage(12.47, "kilovolts") + return Voltage(12.47, "kilovolts") def map_phases(self, row, from_node_sections, to_node_sections): node_id = row["NodeID"] @@ -71,9 +71,9 @@ def map_voltagelimits(self, row): low_voltage = None high_voltage = None if row['LowVoltageLimit'] != '': - low_voltage = PositiveVoltage(row['LowVoltageLimit'], "kilovolts") + low_voltage = Voltage(row['LowVoltageLimit'], "kilovolts") if row['HighVoltageLimit'] != '': - high_voltage = PositiveVoltage(row['HighVoltageLimit'], "kilovolts") + high_voltage = Voltage(row['HighVoltageLimit'], "kilovolts") if low_voltage is not None and high_voltage is not None: return [low_voltage, high_voltage] else: diff --git a/src/ditto/readers/cyme/equipment/capacitor_equipment.py b/src/ditto/readers/cyme/equipment/capacitor_equipment.py index 2e0561e..c66ed35 100644 --- a/src/ditto/readers/cyme/equipment/capacitor_equipment.py +++ b/src/ditto/readers/cyme/equipment/capacitor_equipment.py @@ -1,9 +1,9 @@ from ditto.readers.cyme.cyme_mapper import CymeMapper -from gdm.quantities import PositiveReactivePower, PositiveResistance, PositiveReactance +from gdm.quantities import ReactivePower, Resistance, Reactance from gdm.distribution.equipment.phase_capacitor_equipment import PhaseCapacitorEquipment from gdm.distribution.equipment.capacitor_equipment import CapacitorEquipment from gdm.distribution.enums import VoltageTypes, ConnectionType -from gdm.quantities import PositiveVoltage +from gdm.quantities import Voltage class CapacitorEquipmentMapper(CymeMapper): def __init__(self, system): @@ -26,7 +26,7 @@ def map_name(self, row): return row["ID"] def map_rated_voltage(self, row): - return PositiveVoltage(float(row["KV"]), "kilovolt") + return Voltage(float(row["KV"]), "kilovolt") def map_phase_capacitors(self, row): phase_capacitors = [] @@ -69,4 +69,4 @@ def map_name(self, row, phase): def map_rated_reactive_power(self, row): - return PositiveReactivePower(float(row["KVAR"]),'kilovar') + return ReactivePower(float(row["KVAR"]),'kilovar') diff --git a/src/ditto/readers/opendss/components/branches.py b/src/ditto/readers/opendss/components/branches.py index 212a5ac..6723aa8 100644 --- a/src/ditto/readers/opendss/components/branches.py +++ b/src/ditto/readers/opendss/components/branches.py @@ -5,11 +5,10 @@ from infrasys.quantities import Time from gdm.quantities import ( - PositiveResistancePULength, + ResistancePULength, CapacitancePULength, ReactancePULength, - PositiveDistance, - PositiveCurrent, + Distance, Current, ) @@ -148,7 +147,7 @@ def _build_matrix_branch( num_phase = module.Phases() thermal_limits = ThermalLimitSet( limit_type="max", - value=PositiveCurrent(module.EmergAmps(), "ampere"), + value=Current(module.EmergAmps(), "ampere"), ) thermal_limits = get_equipment_from_catalog(thermal_limits, thermal_limit_catalog) @@ -172,7 +171,7 @@ def _build_matrix_branch( ) matrix_branch_dict = { "name": equipment_uuid, - "r_matrix": PositiveResistancePULength( + "r_matrix": ResistancePULength( np.reshape(np.array(r_matrix), (num_phase, num_phase)), f"ohm/{length_units}", ), @@ -184,7 +183,7 @@ def _build_matrix_branch( np.reshape(np.array(c_matrix), (num_phase, num_phase)), f"nanofarad/{length_units}", ), - "ampacity": PositiveCurrent(module.NormAmps(), "ampere"), + "ampacity": Current(module.NormAmps(), "ampere"), "loading_limit": thermal_limits, } if model_class == MatrixImpedanceSwitchEquipment: @@ -321,7 +320,7 @@ def get_branches( system.get_component(DistributionBus, bus1), system.get_component(DistributionBus, bus2), ], - length=PositiveDistance( + length=Distance( odd.Lines.Length(), UNIT_MAPPER[odd.Lines.Units()] ), phases=[PHASE_MAPPER[node] for node in nodes], @@ -361,7 +360,7 @@ def get_branches( system.get_component(DistributionBus, bus1), system.get_component(DistributionBus, bus2), ], - "length": PositiveDistance( + "length": Distance( odd.Lines.Length(), UNIT_MAPPER[odd.Lines.Units()] ), "phases": [PHASE_MAPPER[node] for node in nodes], diff --git a/src/ditto/readers/opendss/components/buses.py b/src/ditto/readers/opendss/components/buses.py index 62b19a3..0112ae3 100644 --- a/src/ditto/readers/opendss/components/buses.py +++ b/src/ditto/readers/opendss/components/buses.py @@ -1,5 +1,5 @@ from gdm import DistributionBus, VoltageLimitSet, VoltageTypes -from gdm.quantities import PositiveVoltage +from gdm.quantities import Voltage from infrasys.location import Location import opendssdirect as odd from loguru import logger @@ -31,14 +31,14 @@ def get_buses(crs: str = None) -> list[DistributionBus]: voltage_lower_bound = VoltageLimitSet( limit_type="min", - value=PositiveVoltage(nominal_voltage * 0.95, "kilovolt"), + value=Voltage(nominal_voltage * 0.95, "kilovolt"), ) voltage_lower_bound = get_equipment_from_catalog( voltage_lower_bound, voltage_limit_set_catalog ) voltage_upper_bound = VoltageLimitSet( limit_type="max", - value=PositiveVoltage(nominal_voltage * 1.05, "kilovolt"), + value=Voltage(nominal_voltage * 1.05, "kilovolt"), ) voltage_upper_bound = get_equipment_from_catalog( voltage_upper_bound, voltage_limit_set_catalog @@ -49,7 +49,7 @@ def get_buses(crs: str = None) -> list[DistributionBus]: DistributionBus( voltage_type=VoltageTypes.LINE_TO_GROUND.value, name=bus, - nominal_voltage=PositiveVoltage(nominal_voltage, "kilovolt"), + nominal_voltage=Voltage(nominal_voltage, "kilovolt"), phases=[PHASE_MAPPER[str(node)] for node in odd.Bus.Nodes()], coordinate=loc, voltagelimits=limitsets, diff --git a/src/ditto/readers/opendss/components/cables.py b/src/ditto/readers/opendss/components/cables.py index d77248a..651723b 100644 --- a/src/ditto/readers/opendss/components/cables.py +++ b/src/ditto/readers/opendss/components/cables.py @@ -1,8 +1,8 @@ from gdm.quantities import ( - PositiveCurrent, - PositiveDistance, - PositiveResistancePULength, - PositiveVoltage, + Current, + Distance, + ResistancePULength, + Voltage, ) from gdm import ConcentricCableEquipment from pydantic import PositiveInt @@ -37,42 +37,42 @@ def get_cables_equipment() -> list[ConcentricCableEquipment]: diam_strand = query_model_data(model_type, model_name, "diam", float) cable = ConcentricCableEquipment( - strand_ac_resistance=PositiveResistancePULength( + strand_ac_resistance=ResistancePULength( query_model_data(model_type, model_name, "rstrand", float), f"ohms/{length_units}" ), - dc_resistance=PositiveResistancePULength( + dc_resistance=ResistancePULength( query_model_data(model_type, model_name, "rdc", float), f"ohms/{length_units}" ), - phase_ac_resistance=PositiveResistancePULength( + phase_ac_resistance=ResistancePULength( query_model_data(model_type, model_name, "rac", float), f"ohms/{length_units}" ), - strand_gmr=PositiveDistance( + strand_gmr=Distance( gmr_strand if gmr_strand else diam_strand * 0.7788, f"{radius_units}" ), - strand_diameter=PositiveDistance( + strand_diameter=Distance( diam_strand if diam_strand else gmr_strand / 0.7788, f"{radius_units}" ), - ampacity=PositiveCurrent( + ampacity=Current( query_model_data(model_type, model_name, "normamps", float), "ampere" ), - emergency_ampacity=PositiveCurrent( + emergency_ampacity=Current( query_model_data(model_type, model_name, "emergamps", float), "ampere" ), - insulation_thickness=PositiveDistance( + insulation_thickness=Distance( query_model_data(model_type, model_name, "InsLayer", float), "ampere" ), - cable_diameter=PositiveDistance( + cable_diameter=Distance( query_model_data(model_type, model_name, "DiaCable", float), "ampere" ), - insulation_diameter=PositiveDistance( + insulation_diameter=Distance( query_model_data(model_type, model_name, "diains", float), "ampere" ), num_neutral_strands=PositiveInt( query_model_data(model_type, model_name, "k", int), "ampere" ), - conductor_diameter=PositiveDistance(diam if diam else gmr / 0.7788, f"{radius_units}"), - conductor_gmr=PositiveDistance(gmr if gmr else diam * 0.7788, f"{gmr_units}"), - rated_voltage=PositiveVoltage(12.47, "volts"), + conductor_diameter=Distance(diam if diam else gmr / 0.7788, f"{radius_units}"), + conductor_gmr=Distance(gmr if gmr else diam * 0.7788, f"{gmr_units}"), + rated_voltage=Voltage(12.47, "volts"), loading_limit=None, name=model_type, ) diff --git a/src/ditto/readers/opendss/components/capacitors.py b/src/ditto/readers/opendss/components/capacitors.py index a31d9e8..904a324 100644 --- a/src/ditto/readers/opendss/components/capacitors.py +++ b/src/ditto/readers/opendss/components/capacitors.py @@ -7,7 +7,7 @@ PhaseCapacitorEquipment, ConnectionType, ) -from gdm.quantities import PositiveReactivePower, PositiveResistance, PositiveReactance +from gdm.quantities import ReactivePower, Resistance, Reactance from infrasys.system import System import opendssdirect as odd from loguru import logger @@ -41,11 +41,11 @@ def _build_capacitor_source_equipment( for el in nodes: phase_capacitor = PhaseCapacitorEquipment( name=f"{equipment_uuid}_{el}", - rated_capacity=PositiveReactivePower(kvar_ / len(nodes), "kilovar"), + rated_capacity=ReactivePower(kvar_ / len(nodes), "kilovar"), num_banks=odd.Capacitors.NumSteps(), num_banks_on=sum(odd.Capacitors.States()), - resistance=PositiveResistance(0, "ohm"), - reactance=PositiveReactance(0, "ohm"), + resistance=Resistance(0, "ohm"), + reactance=Reactance(0, "ohm"), ) phase_capacitor = get_equipment_from_catalog( phase_capacitor, phase_capacitor_equipment_catalog diff --git a/src/ditto/readers/opendss/components/conductors.py b/src/ditto/readers/opendss/components/conductors.py index 08d5fba..521fb36 100644 --- a/src/ditto/readers/opendss/components/conductors.py +++ b/src/ditto/readers/opendss/components/conductors.py @@ -1,4 +1,4 @@ -from gdm.quantities import PositiveCurrent, PositiveDistance, PositiveResistancePULength +from gdm.quantities import Current, Distance, ResistancePULength from gdm import BareConductorEquipment import opendssdirect as odd from loguru import logger @@ -27,18 +27,18 @@ def get_conductors_equipment() -> list[BareConductorEquipment]: gmr = query_model_data(model_type, model_name, "gmr", float) diam = query_model_data(model_type, model_name, "diam", float) conductor = BareConductorEquipment( - emergency_ampacity=PositiveCurrent( + emergency_ampacity=Current( query_model_data(model_type, model_name, "emergamps", float), "ampere" ), - conductor_diameter=PositiveDistance(diam if diam else gmr / 0.7788, f"{radius_units}"), - conductor_gmr=PositiveDistance(gmr if gmr else diam * 0.7788, f"{gmr_units}"), - ac_resistance=PositiveResistancePULength( + conductor_diameter=Distance(diam if diam else gmr / 0.7788, f"{radius_units}"), + conductor_gmr=Distance(gmr if gmr else diam * 0.7788, f"{gmr_units}"), + ac_resistance=ResistancePULength( query_model_data(model_type, model_name, "rac", float), f"ohms/{length_units}" ), - dc_resistance=PositiveResistancePULength( + dc_resistance=ResistancePULength( query_model_data(model_type, model_name, "rdc", float), f"ohms/{length_units}" ), - ampacity=PositiveCurrent( + ampacity=Current( query_model_data(model_type, model_name, "normamps", float), "ampere" ), loading_limit=None, diff --git a/src/ditto/readers/opendss/components/pv_systems.py b/src/ditto/readers/opendss/components/pv_systems.py index 7a3045d..02dee6a 100644 --- a/src/ditto/readers/opendss/components/pv_systems.py +++ b/src/ditto/readers/opendss/components/pv_systems.py @@ -5,7 +5,7 @@ DistributionBus, SolarEquipment, ) -from gdm.quantities import PositiveActivePower +from gdm.quantities import ActivePower from infrasys.system import System import opendssdirect as odd from loguru import logger @@ -43,8 +43,8 @@ def query(ppty): solar_equipment = SolarEquipment( name=str(equipment_uuid), - rated_capacity=PositiveActivePower(kva_ac, "kilova"), - solar_power=PositiveActivePower(kw_dc, "kilova"), + rated_capacity=ActivePower(kva_ac, "kilova"), + solar_power=ActivePower(kw_dc, "kilova"), resistance=float(query(r"%r")), reactance=float(query(r"%x")), cutout_percent=float(query(r"%cutout")), diff --git a/src/ditto/readers/opendss/components/transformers.py b/src/ditto/readers/opendss/components/transformers.py index 9a47520..e1e470f 100644 --- a/src/ditto/readers/opendss/components/transformers.py +++ b/src/ditto/readers/opendss/components/transformers.py @@ -4,7 +4,7 @@ from uuid import uuid4 from enum import Enum -from gdm.quantities import PositiveApparentPower, PositiveVoltage +from gdm.quantities import ApparentPower, Voltage from gdm import ( DistributionTransformerEquipment, DistributionTransformer, @@ -98,12 +98,12 @@ def set_ppty(property: str, value: Any): taps = query("taps", list) tap = [taps[wdg_index]] * num_phase winding = WindingEquipment( - rated_power=PositiveApparentPower(query("kva", float), "kilova"), + rated_power=ApparentPower(query("kva", float), "kilova"), num_phases=num_phase, connection_type=ConnectionType.DELTA if query("conn", str).lower() == "delta" else ConnectionType.STAR, - nominal_voltage=PositiveVoltage(nominal_voltage, "kilovolt"), + nominal_voltage=Voltage(nominal_voltage, "kilovolt"), resistance=query("%r", float), is_grounded=False, # TODO: Should be moved to the transformer model. Only known once the transformer is installed voltage_type=VoltageTypes.LINE_TO_GROUND, From 9ac42a6eaec2d2df2a723b94f255974311c897e5 Mon Sep 17 00:00:00 2001 From: "Zink, Zephyr" Date: Thu, 11 Sep 2025 13:46:04 -0600 Subject: [PATCH 14/50] complexity, byphase line comprehension, feeder, random fixes --- src/ditto/readers/cyme/__init__.py | 2 + .../cyme/components/distribution_bus.py | 12 +- .../cyme/components/distribution_capacitor.py | 15 +- .../cyme/components/distribution_load.py | 16 +- .../components/distribution_transformer.py | 50 ++++-- .../cyme/components/geometry_branch.py | 76 ++++++++- .../distribution_transformer_equipment.py | 133 +++++++++++----- .../equipment/geometry_branch_equipment.py | 144 ++++++++++++++++-- src/ditto/readers/cyme/reader.py | 120 +++++++++------ src/ditto/readers/cyme/utils.py | 21 ++- 10 files changed, 449 insertions(+), 140 deletions(-) diff --git a/src/ditto/readers/cyme/__init__.py b/src/ditto/readers/cyme/__init__.py index e65bd09..4fbd272 100644 --- a/src/ditto/readers/cyme/__init__.py +++ b/src/ditto/readers/cyme/__init__.py @@ -3,7 +3,9 @@ from ditto.readers.cyme.components.distribution_load import DistributionLoadMapper from ditto.readers.cyme.equipment.geometry_branch_equipment import BareConductorEquipmentMapper from ditto.readers.cyme.equipment.geometry_branch_equipment import GeometryBranchEquipmentMapper +from ditto.readers.cyme.equipment.geometry_branch_equipment import GeometryBranchByPhaseEquipmentMapper from ditto.readers.cyme.components.geometry_branch import GeometryBranchMapper +from ditto.readers.cyme.components.geometry_branch import GeometryBranchByPhaseMapper from ditto.readers.cyme.equipment.distribution_transformer_equipment import DistributionTransformerEquipmentMapper from ditto.readers.cyme.equipment.distribution_transformer_equipment import WindingEquipmentMapper from ditto.readers.cyme.components.distribution_transformer import DistributionTransformerMapper diff --git a/src/ditto/readers/cyme/components/distribution_bus.py b/src/ditto/readers/cyme/components/distribution_bus.py index fd3e81d..18781fb 100644 --- a/src/ditto/readers/cyme/components/distribution_bus.py +++ b/src/ditto/readers/cyme/components/distribution_bus.py @@ -12,18 +12,20 @@ def __init__(self, cyme_model): cyme_file = 'Network' cyme_section = 'NODE' - def parse(self, row, from_node_sections, to_node_sections): + def parse(self, row, from_node_sections, to_node_sections, node_feeder_map): name = self.map_name(row) + feeder = node_feeder_map.get(name, None) coordinate = self.map_coordinate(row) rated_voltage = self.map_rated_voltage(row) phases = self.map_phases(row, from_node_sections, to_node_sections) voltage_limits = self.map_voltagelimits(row) voltage_type = self.map_voltage_type(row) return DistributionBus(name=name, - coordinate=coordinate, - rated_voltage=rated_voltage, - phases=phases, - voltagelimits=voltage_limits, + coordinate=coordinate, + rated_voltage=rated_voltage, + feeder=feeder, + phases=phases, + voltagelimits=voltage_limits, voltage_type=voltage_type) def map_name(self, row): diff --git a/src/ditto/readers/cyme/components/distribution_capacitor.py b/src/ditto/readers/cyme/components/distribution_capacitor.py index 84ae5e9..a4df40c 100644 --- a/src/ditto/readers/cyme/components/distribution_capacitor.py +++ b/src/ditto/readers/cyme/components/distribution_capacitor.py @@ -13,12 +13,12 @@ def __init__(self, system): cyme_file = 'Network' cyme_section = 'SHUNT CAPACITOR SETTING' - def parse(self, row, section_id_sections, equipment_file): + def parse(self, row, section_id_sections, equipment_data): name = self.map_name(row) bus = self.map_bus(row, section_id_sections) phases = self.map_phases(row) controllers = self.map_controllers(row) - equipment = self.map_equipment(row, equipment_file) + equipment = self.map_equipment(row, equipment_data) in_service = self.map_in_service(row) return DistributionCapacitor(name=name, bus=bus, @@ -67,13 +67,12 @@ def map_bus(self, row, section_id_sections): def map_controllers(self, row): return [] - def map_equipment(self, row, equipment_file): + def map_equipment(self, row, equipment_data): mapper = CapacitorEquipmentMapper(self.system) - equipment_data = read_cyme_data(equipment_file, mapper.cyme_section) - for idx, equipment_row in equipment_data.iterrows(): - if equipment_row['ID'] == row['ShuntCapacitorID']: - equipment = mapper.parse(equipment_row, connection=row['Connection']) - return equipment + equipment_row = equipment_data.loc[row['ShuntCapacitorID']] + if not equipment_row.empty: + equipment = mapper.parse(equipment_row, connection=row['Connection']) + return equipment return None def map_in_service(self, row): diff --git a/src/ditto/readers/cyme/components/distribution_load.py b/src/ditto/readers/cyme/components/distribution_load.py index 594a6d4..ae1466b 100644 --- a/src/ditto/readers/cyme/components/distribution_load.py +++ b/src/ditto/readers/cyme/components/distribution_load.py @@ -14,11 +14,11 @@ def __init__(self, system): cyme_section = 'CUSTOMER LOADS' - def parse(self, row, section_id_sections, load_file): + def parse(self, row, section_id_sections, equipment_file): name = self.map_name(row) bus = self.map_bus(row, section_id_sections) phases = self.map_phases(row) - equipment = self.map_equipment(row, load_file) + equipment = self.map_equipment(row, equipment_file) if len(phases) == 0: logger.warning(f"Load {name} has no kW values. Skipping...") return None @@ -77,11 +77,11 @@ def map_phases(self, row): phases.append(Phase.C) return phases - def map_equipment(self, row, load_file): + def map_equipment(self, row, equipment_file): mapper = LoadEquipmentMapper(self.system) - equipment_data = read_cyme_data(load_file, mapper.cyme_section) - for idx, equipment_row in equipment_data.iterrows(): - if equipment_row['DeviceNumber'] == row['DeviceNumber']: - equipment = mapper.parse(equipment_row, row) - return equipment + equipment_row = equipment_file.loc[row['DeviceNumber']] + if equipment_row is not None: + equipment = mapper.parse(equipment_row, row) + return equipment + return None diff --git a/src/ditto/readers/cyme/components/distribution_transformer.py b/src/ditto/readers/cyme/components/distribution_transformer.py index e41a16c..1db0098 100644 --- a/src/ditto/readers/cyme/components/distribution_transformer.py +++ b/src/ditto/readers/cyme/components/distribution_transformer.py @@ -1,9 +1,10 @@ from loguru import logger from ditto.readers.cyme.cyme_mapper import CymeMapper +from gdm.distribution.components.distribution_bus import DistributionBus from gdm.distribution.components.distribution_transformer import DistributionTransformer from gdm.distribution.equipment.distribution_transformer_equipment import DistributionTransformerEquipment from gdm.quantities import ActivePower, ReactivePower -from gdm.distribution.enums import ConnectionType +from gdm.distribution.enums import ConnectionType, Phase class DistributionTransformerMapper(CymeMapper): def __init__(self, system): @@ -12,15 +13,21 @@ def __init__(self, system): cyme_file = 'Network' cyme_section = 'TRANSFORMER SETTING' - def parse(self, network_row, section_id_sections): + def parse(self, row, section_id_sections, transformer_map): + equipment_row = transformer_map.get(row['EqID'], None) name = self.map_name(row) buses = self.map_buses(row, section_id_sections) - winding_phases = self.map_winding_phases(row) - equipment = self.map_equipment(row, network_row) - return DistributionTransformerEquipment(name=name, + winding_phases = self.map_winding_phases(row, section_id_sections, equipment_row) + equipment = self.map_equipment(row) + try: + return DistributionTransformer(name=name, buses=buses, winding_phases=winding_phases, equipment=equipment) + except Exception as e: + print(f"Error creating DistributionTransformer {name}: {e}") + print(buses) + return None def map_name(self, row): name = row['SectionID'] @@ -36,20 +43,31 @@ def map_buses(self, row, section_id_sections): to_bus = self.system.get_component(component_type=DistributionBus, name=to_bus_name) return [from_bus, to_bus] - def map_winding_phases(self, row): + def map_winding_phases(self, row, section_id_sections, equipment_row): section_id = str(row['SectionID']) section = section_id_sections[section_id] phase = section['Phase'] - phases = [] - if 'A' in phase: - phases.append(Phase.A) - if 'B' in phase: - phases.append(Phase.B) - if 'C' in phase: - phases.append(Phase.C) - return winding_phases - - def map_equipment(self, row, network_row): + if equipment_row is None: + print(f"Equipment row not found for transformer {row['EqID']}. Assuming 2 windings. {section}") + equipment_row = {'Type': 2} + windings_list = [] + if equipment_row['Type'] == 4: + num_windings = 3 + else: + num_windings = 2 + + for i in range(num_windings): + winding_phases = [] + if 'A' in phase: + winding_phases.append(Phase.A) + if 'B' in phase: + winding_phases.append(Phase.B) + if 'C' in phase: + winding_phases.append(Phase.C) + windings_list.append(winding_phases) + return windings_list + + def map_equipment(self, row): transformer_id = row['EqID'] equipment = self.system.get_component(component_type=DistributionTransformerEquipment, name=transformer_id) return equipment diff --git a/src/ditto/readers/cyme/components/geometry_branch.py b/src/ditto/readers/cyme/components/geometry_branch.py index 763dc9b..8de80ed 100644 --- a/src/ditto/readers/cyme/components/geometry_branch.py +++ b/src/ditto/readers/cyme/components/geometry_branch.py @@ -20,11 +20,16 @@ def parse(self, row, section_id_sections): length = self.map_length(row) phases = self.map_phases(row, section_id_sections) equipment = self.map_equipment(row) - return GeometryBranch(name=name, - buses=buses, - length=length, - phases=phases, - equipment=equipment) + try: + return GeometryBranch(name=name, + buses=buses, + length=length, + phases=phases, + equipment=equipment) + except Exception as e: + print(f"Error creating GeometryBranch {name}: {e}") + print(buses) + return None def map_name(self, row): name = row['SectionID'] @@ -61,3 +66,64 @@ def map_equipment(self, row): line_id = row['LineCableID'] line = self.system.get_component(component_type=GeometryBranchEquipment, name=line_id) return line + +class GeometryBranchByPhaseMapper(CymeMapper): + def __init__(self, system): + super().__init__(system) + + cyme_file = 'Network' + cyme_section = 'OVERHEAD BYPHASE SETTING' + + def parse(self, row, section_id_sections): + name = self.map_name(row) + buses = self.map_buses(row,section_id_sections) + length = self.map_length(row) + phases = self.map_phases(row, section_id_sections) + equipment = self.map_equipment(row) + try: + return GeometryBranch(name=name, + buses=buses, + length=length, + phases=phases, + equipment=equipment) + except Exception as e: + print(f"Error creating GeometryBranch {name}: {e}") + print(buses) + return None + + def map_name(self, row): + name = row['SectionID'] + return name + + def map_buses(self, row, section_id_sections): + section_id = str(row['SectionID']) + section = section_id_sections[section_id] + from_bus_name = section['FromNodeID'] + to_bus_name = section['ToNodeID'] + + from_bus = self.system.get_component(component_type=DistributionBus, name=from_bus_name) + to_bus = self.system.get_component(component_type=DistributionBus, name=to_bus_name) + return [from_bus, to_bus] + + def map_length(self, row): + length = Distance(float(row['Length']),'mile').to('km') + return length + + def map_phases(self, row, section_id_sections): + section_id = str(row['SectionID']) + section = section_id_sections[section_id] + phase = section['Phase'] + phases = [] + if 'A' in phase: + phases.append(Phase.A) + if 'B' in phase: + phases.append(Phase.B) + if 'C' in phase: + phases.append(Phase.C) + return phases + + def map_equipment(self, row): + line_id = row['SectionID'] + line = self.system.get_component(component_type=GeometryBranchEquipment, name=line_id) + return line + diff --git a/src/ditto/readers/cyme/equipment/distribution_transformer_equipment.py b/src/ditto/readers/cyme/equipment/distribution_transformer_equipment.py index 4190829..d2954ed 100644 --- a/src/ditto/readers/cyme/equipment/distribution_transformer_equipment.py +++ b/src/ditto/readers/cyme/equipment/distribution_transformer_equipment.py @@ -2,8 +2,9 @@ from ditto.readers.cyme.cyme_mapper import CymeMapper from gdm.distribution.equipment.distribution_transformer_equipment import DistributionTransformerEquipment from gdm.distribution.equipment.distribution_transformer_equipment import WindingEquipment -from gdm.quantities import ActivePower, ReactivePower -from gdm.distribution.enums import ConnectionType +from gdm.quantities import ActivePower, ReactivePower, Voltage +from gdm.distribution.common.sequence_pair import SequencePair +from gdm.distribution.enums import ConnectionType, VoltageTypes class DistributionTransformerEquipmentMapper(CymeMapper): def __init__(self, system): @@ -12,19 +13,29 @@ def __init__(self, system): cyme_file = 'Equipment' cyme_section = 'TRANSFORMER' - def parse(self, row, network_row): + def parse(self, row, transformer_map): + network_row = None + if row['ID'] in transformer_map: + network_row = transformer_map[row['ID']] + + if network_row is None: + return None + name = self.map_name(row) pct_no_load_loss = self.map_pct_no_load_loss(row) pct_full_load_loss = self.map_pct_full_load_loss(row) windings = self.map_windings(row, network_row) winding_reactances = self.map_winding_reactances(row) is_center_tapped = self.map_is_center_tapped(row) + coupling_sequences = self.map_coupling(row) + return DistributionTransformerEquipment(name=name, pct_no_load_loss=pct_no_load_loss, pct_full_load_loss=pct_full_load_loss, windings=windings, winding_reactances=winding_reactances, - is_center_tapped=is_center_tapped) + is_center_tapped=is_center_tapped, + coupling_sequences=coupling_sequences) def map_name(self, row): name = row['ID'] @@ -41,18 +52,20 @@ def map_pct_full_load_loss(self, row): rated_current_sec = float(row['KVA'])/float(row['KVLLsec']) resistance_pu = float(row['Z1'])/float((1+float(row['XR'])**2)**0.5) resistance_sec = float(resistance_pu)*float(float(row['KVLLsec'])**2) / float(row['KVA']) - pct_full_load_loss = rated_current_sec*resistance_sec + pct_full_load_loss = rated_current_sec*resistance_sec/100 return pct_full_load_loss def map_winding_reactances(self, row): - xr_ratio = row['XR'] + xr_ratio = float(row['XR']) + if xr_ratio == 0: + xr_ratio = 0.01 rx_ratio = 1/xr_ratio - reactance_pu = row['Z1']/((1+rx_ratio**2)**0.5) + reactance_pu = float(row['Z1'])/((1+rx_ratio**2)**0.5) transformer_type = row['Type'] if transformer_type == '4': winding_reactances = [reactance_pu, reactance_pu, reactance_pu] else: - winding_reactances = [rectance_pu] + winding_reactances = [reactance_pu] return winding_reactances def map_is_center_tapped(self, row): @@ -68,7 +81,6 @@ def map_windings(self, row, network_row): is_center_tapped = True winding_mapper1 = WindingEquipmentMapper(self.system) - import pdb;pdb.set_trace() winding_1 = winding_mapper1.parse(row, network_row, winding_number=1) winding_mapper2 = WindingEquipmentMapper(self.system) winding_2 = winding_mapper2.parse(row, network_row, winding_number=2) @@ -79,8 +91,18 @@ def map_windings(self, row, network_row): winding_3 = winding_mapper3.parse(row, network_row, winding_number=3) windings.append(winding_3) return windings + + def map_coupling(self, row): + transformer_type = row['Type'] + if transformer_type == '4': + coupling = [SequencePair(0,1), + SequencePair(0,2), + SequencePair(1,2)] + else: + coupling = [SequencePair(0,1)] + return coupling -def WindingEquipmentMapper(CymeMapper): +class WindingEquipmentMapper(CymeMapper): def __init__(self, system): super().__init__(system) @@ -99,11 +121,12 @@ def __init__(self, system): 9: 'Y_YNG', 10: 'YNG_Y', 11: 'Yg_Zg', - 12: 'D_Zg' + 12: 'D_Zg', + 15: 'YG_CT', + 16: 'D_DCT', } - - def parse(self, row, winding_number, network_row): + def parse(self, row, network_row, winding_number): name = self.map_name(row) resistance = self.map_resistance(row, winding_number) is_grounded = self.map_is_grounded(row, winding_number) @@ -114,8 +137,8 @@ def parse(self, row, winding_number, network_row): connection_type = self.map_connection_type(row, winding_number) tap_positions = self.map_tap_positions(row, winding_number, network_row) total_taps = self.map_total_taps(row) - min_tap_pu = self.map_min_tap_pu(row) - max_tap_pu = self.map_max_tap_pu(row) + min_tap_pu = self.min_tap_pu(row) + max_tap_pu = self.max_tap_pu(row) return WindingEquipment(name=name, resistance=resistance, is_grounded=is_grounded, @@ -134,14 +157,15 @@ def map_name(self, row): return name def map_resistance(self, row, winding_number): - + xr_ratio = float(row['XR']) + resistance_pu = float(row['Z1'])/((1+xr_ratio**2)**0.5) if winding_number == 1: - resistance = resistance_pu*row['KVLLprim']**2 / row['KVA'] + resistance = resistance_pu*float(row['KVLLprim'])**2 / float(row['KVA']) elif winding_number == 2: - resistance = resistance_pu*row['KVLLsec']**2 / row['KVA'] + resistance = resistance_pu*float(row['KVLLsec'])**2 / float(row['KVA']) elif winding_number == 3: - resistance = resistance_pu*row['KVLLsec']**2 / row['KVA'] - return resistance + resistance = resistance_pu*float(row['KVLLsec'])**2 / float(row['KVA']) + return resistance/100 def map_is_grounded(self, row, winding_number): @@ -152,7 +176,7 @@ def map_is_grounded(self, row, winding_number): winding = 1 elif winding_number == 3: winding = 1 - winding_type = self.connection_map[connection_type].split('_')[winding] + winding_type = self.connection_map.get(int(connection_type), 'Y_Y').split('_')[winding] if 'YNG' in winding_type: grounded = False elif 'D' in winding_type: @@ -161,7 +185,7 @@ def map_is_grounded(self, row, winding_number): grounded = False else: grounded = True - return is_grounded + return grounded def map_rated_voltage(self, row, winding_number): if winding_number == 1: @@ -171,7 +195,7 @@ def map_rated_voltage(self, row, winding_number): elif winding_number == 3: voltage = row['KVLLsec'] - voltage = PositiveVoltage(float(voltage), "kilovolt") + voltage = Voltage(float(voltage), "kilovolt") return voltage def map_voltage_type(self, row): @@ -185,20 +209,21 @@ def map_rated_power(self, row): def map_num_phases(self, row): phase_type = row['Type'] if phase_type == 1 or phase_type == 4: - num_phaes = 1 + num_phases = 1 else: num_phases = 3 return num_phases def map_connection_type(self, row, winding_number): + + connection_type = row['Conn'] if winding_number == 1: - connection = row['Conn'] + winding = 0 elif winding_number == 2: - connection = row['Conn'] + winding = 1 elif winding_number == 3: - connection = row['Conn'] - - winding_type = self.connection_map[connection_type].split('_')[winding] + winding = 1 + winding_type = self.connection_map.get(int(connection_type), 'Y_Y').split('_')[winding] if winding_type == 'YO': connection_type = 'OPEN_STAR' elif winding_type == 'DO': @@ -209,6 +234,10 @@ def map_connection_type(self, row, winding_number): connection_type = 'STAR' elif 'D' in winding_type: connection_type = 'DELTA' + elif 'DCT' == winding_type: + connection_type = 'DELTA' + elif 'CT' == winding_type: + connection_type = 'STAR' else: raise ValueError("Unknown winding type: {}".format(winding_type)) @@ -217,38 +246,58 @@ def map_connection_type(self, row, winding_number): def map_tap_positions(self, row, winding_number, network_row): phase_type = row['Type'] if phase_type == 1 or phase_type == 4: - num_phaes = 1 + num_phases = 1 else: num_phases = 3 tap_positions = [] if winding_number == 1: if network_row is None: - tap = 100.0 + tap = 0.0 else: - tap = network_row['PrimTap'] + tap = network_row.get('PrimTap', None) + if tap is None: + tap = network_row.get('PrimaryTapSettingA', 100) + tap = float(tap) - 100 + elif winding_number == 2: if network_row is None: - tap = 100.0 + tap = 0.0 else: - tap = network_row['SecondaryTap'] + tap = network_row.get('SecTap', None) + if tap is None: + tap = network_row.get('SecondaryTapSettingA', 100) + tap = float(tap) - 100 elif winding_number == 3: if network_row is None: - tap = 100.0 + tap = 0.0 else: - tap = network_row['SecondaryTap'] + tap = network_row.get('SecTap', None) + if tap is None: + tap = network_row.get('SecondaryTapSettingA', 100) + tap = float(tap) - 100 for phase in range(1, num_phases + 1): - tap_positions.append(tap) + tap_positions.append(0.0) return tap_positions def map_total_taps(self, row): - total_taps = row['Tap'] + taps = row['Taps'] + if taps == '' or taps is None: + taps = 0 + total_taps = int(taps) return total_taps def min_tap_pu(self, row): - min_tap_pu = row['LowerBandwidth'] - return min_tap_pu + min_tap_pu = row['MinReg_Range'] + if min_tap_pu == '' or min_tap_pu is None: + return 0.9 + min_tap_pu = 1 - float(min_tap_pu)/100 + return float(min_tap_pu) def max_tap_pu(self, row): - max_tap_pu = row['UpperBandwidth'] - return max_tap_pu + max_tap_pu = row['MaxReg_Range'] + if max_tap_pu == '' or max_tap_pu is None: + return 1.1 + max_tap_pu = 1 + float(max_tap_pu)/100 + return float(max_tap_pu) + diff --git a/src/ditto/readers/cyme/equipment/geometry_branch_equipment.py b/src/ditto/readers/cyme/equipment/geometry_branch_equipment.py index ad86306..cee1ce7 100644 --- a/src/ditto/readers/cyme/equipment/geometry_branch_equipment.py +++ b/src/ditto/readers/cyme/equipment/geometry_branch_equipment.py @@ -1,8 +1,9 @@ -from gdm.quantities import PositiveCurrent, PositiveDistance, PositiveResistancePULength, Distance +from gdm.quantities import Current, Distance, ResistancePULength, Distance from ditto.readers.cyme.utils import read_cyme_data from ditto.readers.cyme.cyme_mapper import CymeMapper from gdm.distribution.equipment.geometry_branch_equipment import GeometryBranchEquipment from gdm.distribution.equipment.bare_conductor_equipment import BareConductorEquipment +from gdm.distribution.equipment.concentric_cable_equipment import ConcentricCableEquipment class GeometryBranchEquipmentMapper(CymeMapper): @@ -157,7 +158,7 @@ def map_conductor_diameter(self, row): for idx, row in conductor_info.iterrows(): if row['ID'] == cable_name: conductor_diameter = float(row['Diameter']) - return PositiveDistance(conductor_diameter,'inch').to('mm') + return Distance(conductor_diameter,'inch').to('mm') return None @@ -167,11 +168,11 @@ def map_strand_diameter(self, row): for idx, row in strand_info.iterrows(): if row['ID'] == cable_name: strand_diameter = float(row['Diameter']) - return PositiveDistance(strand_diameter,'inch').to('mm') + return Distance(strand_diameter,'inch').to('mm') return None def map_ampacity(self, row): - ampacity = PositiveCurrent(float(row['Amps']),'amp') + ampacity = Current(float(row['Amps']),'amp') return ampacity class BareConductorEquipmentMapper(CymeMapper): @@ -203,25 +204,148 @@ def map_name(self, row): def map_conductor_diameter(self, row): conductor_diameter = float(row['Diameter']) - return PositiveDistance(conductor_diameter,'inch').to('mm') + return Distance(conductor_diameter,'inch').to('mm') def map_conductor_gmr(self, row): - conductor_gmr = PositiveDistance(float(row['GMR']),'inch').to('mm') + conductor_gmr = Distance(float(row['GMR']),'inch').to('mm') return conductor_gmr def map_ampacity(self, row): - ampacity = PositiveCurrent(float(row['Amps']),'amp') + ampacity = Current(float(row['Amps']),'amp') + if ampacity == 0.0: + ampacity = Current(600.0,'amp') return ampacity def map_emergency_ampacity(self, row): - emergency_ampacity = PositiveCurrent(float(row['Amps_4']),'amp') + emergency_ampacity = Current(float(row['Amps_4']),'amp') + if emergency_ampacity == 0.0: + emergency_ampacity = Current(600.0,'amp') return emergency_ampacity def map_ac_resistance(self, row): - ac_resistance = PositiveResistancePULength(float(row['R25']),'ohm/mile').to('ohm/km') + ac_resistance = ResistancePULength(float(row['R25']),'ohm/mile').to('ohm/km') + if ac_resistance == 0.0: + ac_resistance = ResistancePULength(0.555000,'ohm/mile').to('ohm/km') return ac_resistance def map_dc_resistance(self, row): - dc_resistance = PositiveResistancePULength(float(row['R25']),'ohm/mile').to('ohm/km') + dc_resistance = ResistancePULength(float(row['R25']),'ohm/mile').to('ohm/km') + if dc_resistance == 0.0: + dc_resistance = ResistancePULength(0.555000,'ohm/mile').to('ohm/km') return dc_resistance +class GeometryBranchByPhaseEquipmentMapper(CymeMapper): + def __init__(self, system): + super().__init__(system) + + cyme_file = 'Network' + cyme_section = 'OVERHEAD BYPHASE SETTING' + + def parse(self, row, spacing_ids): + name = self.map_name(row) + conductors = self.map_conductors(row, spacing_ids) + horizontal_positions = self.map_horizontal_positions(row, spacing_ids) + vertical_positions = self.map_vertical_positions(row, spacing_ids) + + return GeometryBranchEquipment(name=name, + conductors=conductors, + horizontal_positions=horizontal_positions, + vertical_positions=vertical_positions) + + def map_name(self, row): + name = row['SectionID'] + return name + + def map_vertical_positions(self, row, spacing_ids): + spacing_id = row['SpacingID'] + row = spacing_ids.loc[spacing_id] + + if not row.empty: + vertical_positions = [] + spacing = row + cond1_y = spacing['PosOfCond1_Y'] + cond2_y = spacing['PosOfCond2_Y'] + cond3_y = spacing['PosOfCond3_Y'] + neutral_y = spacing['PosOfNeutralCond_Y'] + if cond1_y != "": + y1 = float(cond1_y) + vertical_positions.append(y1) + if cond2_y != "": + y2 = float(cond2_y) + vertical_positions.append(y2) + if cond3_y != "": + y3 = float(cond3_y) + vertical_positions.append(y3) + if neutral_y != "": + y_n = float(neutral_y) + vertical_positions.append(y_n) + return Distance(vertical_positions, 'feet').to('m') + return None + + def map_horizontal_positions(self, row, spacing_ids): + spacing_id = row['SpacingID'] + row = spacing_ids.loc[spacing_id] + if not row.empty: + spacing = row + horizontal_positions = [] + cond1_x = spacing['PosOfCond1_X'] + cond2_x = spacing['PosOfCond2_X'] + cond3_x = spacing['PosOfCond3_X'] + neutral_x = spacing['PosOfNeutralCond_X'] + if cond1_x != "": + x1 = float(cond1_x) + horizontal_positions.append(x1) + if cond2_x != "": + x2 = float(cond2_x) + horizontal_positions.append(x2) + if cond3_x != "": + x3 = float(cond3_x) + horizontal_positions.append(x3) + if neutral_x != "": + x_n = float(neutral_x) + horizontal_positions.append(x_n) + return Distance(horizontal_positions, 'feet').to('m') + return None + + def map_conductors(self,row, spacing_ids): + phase_A_conductor_name = row['CondID_A'] + phase_B_conductor_name = row['CondID_B'] + phase_C_conductor_name = row['CondID_C'] + neutral_conductor_name = row['CondID_N1'] + try: + phase_A_conductor = self.system.get_component(component_type=BareConductorEquipment, name=phase_A_conductor_name) + except Exception as e: + phase_A_conductor = self.system.get_component(component_type=BareConductorEquipment, name="Default") + try: + phase_B_conductor = self.system.get_component(component_type=BareConductorEquipment, name=phase_B_conductor_name) + except Exception as e: + phase_B_conductor = self.system.get_component(component_type=BareConductorEquipment, name="Default") + try: + phase_C_conductor = self.system.get_component(component_type=BareConductorEquipment, name=phase_C_conductor_name) + except Exception as e: + phase_C_conductor = self.system.get_component(component_type=BareConductorEquipment, name="Default") + try: + neutral_conductor = self.system.get_component(component_type=BareConductorEquipment, name=neutral_conductor_name) + except Exception as e: + neutral_conductor = self.system.get_component(component_type=BareConductorEquipment, name="Default") + + spacing_id = row['SpacingID'] + + row = spacing_ids.loc[spacing_id] + if not row.empty: + spacing = row + conductors = [] + cond1 = spacing['PosOfCond1_X'] + cond2 = spacing['PosOfCond2_X'] + cond3 = spacing['PosOfCond3_X'] + neutral = spacing['PosOfNeutralCond_X'] + if cond1 != "" and phase_A_conductor != "NONE": + conductors.append(phase_A_conductor) + if cond2 != "" and phase_B_conductor != "NONE": + conductors.append(phase_B_conductor) + if cond3 != "" and phase_C_conductor != "NONE": + conductors.append(phase_C_conductor) + if neutral != "" and neutral_conductor != "NONE": + conductors.append(neutral_conductor) + return conductors + return None \ No newline at end of file diff --git a/src/ditto/readers/cyme/reader.py b/src/ditto/readers/cyme/reader.py index 2a8dd21..7a1204f 100644 --- a/src/ditto/readers/cyme/reader.py +++ b/src/ditto/readers/cyme/reader.py @@ -1,4 +1,4 @@ -from gdm.quantities import Distance, PositiveCurrent, PositiveResistancePULength +from gdm.quantities import Distance, Current, ResistancePULength from gdm.distribution.equipment.bare_conductor_equipment import BareConductorEquipment from gdm.distribution.components.base.distribution_component_base import DistributionComponentBase from gdm.distribution.distribution_system import DistributionSystem @@ -16,7 +16,9 @@ class Reader(AbstractReader): "DistributionLoad", "BareConductorEquipment", "GeometryBranchEquipment", + "GeometryBranchByPhaseEquipment", "GeometryBranch", + "GeometryBranchByPhase", "DistributionTransformerEquipment", "DistributionTransformer" ] @@ -35,30 +37,25 @@ def read(self, network_file, equipment_file, load_file): name="Default", conductor_diameter=Distance(0.368000,'inch').to('mm'), conductor_gmr=Distance(0.133200,'inch').to('mm'), - ampacity=PositiveCurrent(600.0,'amp'), - emergency_ampacity=PositiveCurrent(600.0,'amp'), - ac_resistance=PositiveResistancePULength(0.555000,'ohm/mile').to('ohm/km'), - dc_resistance=PositiveResistancePULength(0.555000,'ohm/mile').to('ohm/km'), + ampacity=Current(600.0,'amp'), + emergency_ampacity=Current(600.0,'amp'), + ac_resistance=ResistancePULength(0.555000,'ohm/mile').to('ohm/km'), + dc_resistance=ResistancePULength(0.555000,'ohm/mile').to('ohm/km'), ) self.system.add_component(default_conductor) - section_data = read_cyme_data(network_file,"SECTION") - for idx, row in section_data.iterrows(): - section_id = row["SectionID"] - section_id_sections[section_id] = row + node_feeder_map = {} - from_node = row["FromNodeID"] - to_node = row["ToNodeID"] + section_data = read_cyme_data(network_file,"SECTION", node_feeder_map, parse_feeders=True) + + section_id_sections = section_data.set_index("SectionID").to_dict(orient="index") + from_node_sections = section_data.groupby("FromNodeID").apply(lambda df: df.to_dict(orient="records")).to_dict() + to_node_sections = section_data.groupby("ToNodeID").apply(lambda df: df.to_dict(orient="records")).to_dict() - if not from_node in from_node_sections: - from_node_sections[from_node] = [] - from_node_sections[from_node].append(row) - if not to_node in to_node_sections: - to_node_sections[to_node] = [] - to_node_sections[to_node].append(row) for component_type in self.component_types: + print(component_type) mapper_name = component_type + "Mapper" if not hasattr(cyme_mapper, mapper_name): logger.warning(f"Mapper {mapper_name} not found. Skipping.") @@ -76,35 +73,70 @@ def read(self, network_file, equipment_file, load_file): else: raise ValueError(f"Unknown CYME file {cyme_file}") - transformer_network_data = read_cyme_data(network_file, 'TRANSFORMER SETTING') - transformer_map = {} - for idx, row in transformer_network_data.iterrows(): - transformer_type = row['EqID'] - transformer_map[transformer_type] = row + + args = [] + mapper_name = component_type + "Mapper" + if mapper_name == "DistributionCapacitorMapper": + + equipment_data = read_cyme_data(equipment_file, "SHUNT CAPACITOR") + equipment_data.index = equipment_data['ID'] + args = [section_id_sections, equipment_data] + + elif mapper_name == "DistributionBusMapper": + args = [from_node_sections, to_node_sections, node_feeder_map] + + elif mapper_name == "DistributionLoadMapper": + + equipment_data = read_cyme_data(load_file, "LOADS") + equipment_data.index = equipment_data['DeviceNumber'] + args = [section_id_sections, equipment_data] + + elif mapper_name == "GeometryBranchMapper": + args = [section_id_sections] + elif mapper_name == "GeometryBranchByPhaseMapper": + args = [section_id_sections] + + elif mapper_name == "BareConductorEquipmentMapper": + args = [] + + elif mapper_name == "GeometryBranchEquipmentMapper": + args = [equipment_file] + + elif mapper_name == "GeometryBranchByPhaseEquipmentMapper": + spacing_ids = read_cyme_data(equipment_file,"SPACING TABLE FOR LINE") + spacing_ids.index = spacing_ids['ID'] + args = [spacing_ids] + + elif mapper_name == "DistributionTransformerEquipmentMapper": + transformer_network_data = read_cyme_data(network_file, 'TRANSFORMER SETTING') + transformer_map = {} + for idx, row in transformer_network_data.iterrows(): + transformer_type = row['EqID'] + transformer_map[transformer_type] = row + + byphase_transformer_network_data = read_cyme_data(equipment_file, "TRANSFORMER BYPHASE SETTING") + for idx, row in byphase_transformer_network_data.iterrows(): + for phase in ['1', '2', '3']: + transformer_type = row['PhaseTransformerID' + phase] + if transformer_type is not None and transformer_type != '': + transformer_map[transformer_type] = row + args = [transformer_map] + + elif mapper_name == "DistributionTransformerMapper": + transformer_equipment_data = read_cyme_data(equipment_file, "TRANSFORMER") + transformer_map = {} + for idx, row in transformer_equipment_data.iterrows(): + transformer_type = row['ID'] + transformer_map[transformer_type] = row + args = [section_id_sections, transformer_map] + + def parse_row(row): + model_entry = mapper.parse(row, *args) + return model_entry - components = [] - for idx, row in data.iterrows(): - mapper_name = component_type + "Mapper" - if mapper_name == "DistributionCapacitorMapper": - model_entry = mapper.parse(row, section_id_sections, equipment_file) - elif mapper_name == "DistributionBusMapper": - model_entry = mapper.parse(row, from_node_sections, to_node_sections) - elif mapper_name == "DistributionLoadMapper": - model_entry = mapper.parse(row, section_id_sections, load_file) - elif mapper_name == "GeometryBranchMapper": - model_entry = mapper.parse(row, section_id_sections) - elif mapper_name == "GeometryBranchEquipmentMapper": - model_entry = mapper.parse(row, equipment_file) - elif mapper_name == "DistributionTransformerEquipmentMapper": - network_row = None - if row['ID'] in transformer_map: - network_row = transformer_map[row['ID']] - model_entry = mapper.parse(row, network_row) - else: - model_entry = mapper.parse(row) - if model_entry is not None: - components.append(model_entry) + components = data.apply(parse_row, axis=1) + components = [c for c in components if c is not None] self.system.add_components(*components) def get_system(self) -> DistributionSystem: diff --git a/src/ditto/readers/cyme/utils.py b/src/ditto/readers/cyme/utils.py index 27a03a7..9ca7e32 100644 --- a/src/ditto/readers/cyme/utils.py +++ b/src/ditto/readers/cyme/utils.py @@ -1,10 +1,13 @@ import pandas as pd +from gdm.distribution.components.distribution_feeder import DistributionFeeder -def read_cyme_data(cyme_file, cyme_section): +def read_cyme_data(cyme_file, cyme_section, node_feeder_map = None, parse_feeders=False): all_data = [] headers = None with open(cyme_file) as f: reading = False + feeder_id = None + feeder_object_map = {} for line in f: if line.startswith(f"[{cyme_section}]"): reading = True @@ -14,7 +17,12 @@ def read_cyme_data(cyme_file, cyme_section): line_header = line.split("=")[1].strip() headers = line_header.split(",") continue - elif line.startswith("FORMAT") or line.startswith("FEEDER"): + elif line.startswith("FORMAT") or line.startswith("FEEDER") or line.startswith("SUBSTATION"): + if parse_feeders: + if line.startswith("FEEDER"): + feeder_id = line.split(",")[0].split("=")[1].strip() + else: + feeder_id = None # For SECTION Feeder headers continue elif line.strip() == "": @@ -23,6 +31,15 @@ def read_cyme_data(cyme_file, cyme_section): else: try: line = line.strip() + line_data = line.split(",") + if parse_feeders and (feeder_id is not None): + if cyme_section == 'SECTION': + node1 = line_data[1].strip() + node2 = line_data[3].strip() + feeder = feeder_object_map.get(feeder_id, DistributionFeeder(name = feeder_id)) + node_feeder_map[node1] = feeder + node_feeder_map[node2] = feeder + feeder_object_map[feeder_id] = feeder all_data.append(line.split(",")) except: raise Exception(f"Failed to parse line: {line}") From 7d45ab2d0fcae125dc064d5673db6ec392127c05 Mon Sep 17 00:00:00 2001 From: "Zink, Zephyr" Date: Tue, 16 Sep 2025 10:04:14 -0600 Subject: [PATCH 15/50] transformer byphase, feeder mapping, switches --- src/ditto/readers/cyme/__init__.py | 4 +- .../cyme/components/distribution_bus.py | 11 ++- .../components/distribution_transformer.py | 82 +++++++++++++++++- .../components/matrix_impedance_switch.py | 86 +++++++++++++++++++ .../matrix_impedance_switch_equipment.py | 71 +++++++++++++++ src/ditto/readers/cyme/reader.py | 20 +++-- src/ditto/readers/cyme/utils.py | 3 +- 7 files changed, 262 insertions(+), 15 deletions(-) create mode 100644 src/ditto/readers/cyme/components/matrix_impedance_switch.py create mode 100644 src/ditto/readers/cyme/equipment/matrix_impedance_switch_equipment.py diff --git a/src/ditto/readers/cyme/__init__.py b/src/ditto/readers/cyme/__init__.py index 4fbd272..362e3a0 100644 --- a/src/ditto/readers/cyme/__init__.py +++ b/src/ditto/readers/cyme/__init__.py @@ -8,5 +8,7 @@ from ditto.readers.cyme.components.geometry_branch import GeometryBranchByPhaseMapper from ditto.readers.cyme.equipment.distribution_transformer_equipment import DistributionTransformerEquipmentMapper from ditto.readers.cyme.equipment.distribution_transformer_equipment import WindingEquipmentMapper -from ditto.readers.cyme.components.distribution_transformer import DistributionTransformerMapper +from ditto.readers.cyme.components.distribution_transformer import DistributionTransformerByPhaseMapper, DistributionTransformerMapper +from ditto.readers.cyme.components.matrix_impedance_switch import MatrixImpedanceSwitchMapper +from ditto.readers.cyme.equipment.matrix_impedance_switch_equipment import MatrixImpedanceSwitchEquipmentMapper diff --git a/src/ditto/readers/cyme/components/distribution_bus.py b/src/ditto/readers/cyme/components/distribution_bus.py index 18781fb..47b4c78 100644 --- a/src/ditto/readers/cyme/components/distribution_bus.py +++ b/src/ditto/readers/cyme/components/distribution_bus.py @@ -12,12 +12,15 @@ def __init__(self, cyme_model): cyme_file = 'Network' cyme_section = 'NODE' - def parse(self, row, from_node_sections, to_node_sections, node_feeder_map): + def parse(self, row, from_node_sections, to_node_sections, node_feeder_map, feeder_voltage_map): name = self.map_name(row) feeder = node_feeder_map.get(name, None) + feeder_name = None + if feeder is not None: + feeder_name = feeder.name coordinate = self.map_coordinate(row) - rated_voltage = self.map_rated_voltage(row) phases = self.map_phases(row, from_node_sections, to_node_sections) + rated_voltage = self.map_rated_voltage(row, phases, feeder_voltage_map.get(feeder_name)) voltage_limits = self.map_voltagelimits(row) voltage_type = self.map_voltage_type(row) return DistributionBus(name=name, @@ -37,9 +40,9 @@ def map_coordinate(self, row): crs = None return Location(x=X, y=Y, crs=crs) - def map_rated_voltage(self, row): + def map_rated_voltage(self, row, phases, feeder_voltage): #return PositiveVoltage(float(row['UserDefinedBaseVoltage']), "kilovolts") - return Voltage(12.47, "kilovolts") + return Voltage(float(12.47), "kilovolts") def map_phases(self, row, from_node_sections, to_node_sections): node_id = row["NodeID"] diff --git a/src/ditto/readers/cyme/components/distribution_transformer.py b/src/ditto/readers/cyme/components/distribution_transformer.py index 1db0098..ce453be 100644 --- a/src/ditto/readers/cyme/components/distribution_transformer.py +++ b/src/ditto/readers/cyme/components/distribution_transformer.py @@ -25,8 +25,7 @@ def parse(self, row, section_id_sections, transformer_map): winding_phases=winding_phases, equipment=equipment) except Exception as e: - print(f"Error creating DistributionTransformer {name}: {e}") - print(buses) + logger.warning(f"Failed to create DistributionTransformer {name}: {e}") return None def map_name(self, row): @@ -49,9 +48,9 @@ def map_winding_phases(self, row, section_id_sections, equipment_row): phase = section['Phase'] if equipment_row is None: print(f"Equipment row not found for transformer {row['EqID']}. Assuming 2 windings. {section}") - equipment_row = {'Type': 2} + equipment_row = {'Type': "2"} windings_list = [] - if equipment_row['Type'] == 4: + if equipment_row['Type'] == "4": num_windings = 3 else: num_windings = 2 @@ -71,3 +70,78 @@ def map_equipment(self, row): transformer_id = row['EqID'] equipment = self.system.get_component(component_type=DistributionTransformerEquipment, name=transformer_id) return equipment + + +class DistributionTransformerByPhaseMapper(CymeMapper): + def __init__(self, system): + super().__init__(system) + + cyme_file = 'Network' + cyme_section = 'TRANSFORMER BYPHASE SETTING' + + def parse(self, row, section_id_sections, transformer_map): + additional_transformers = [] + for phase in ['1', '2', '3']: + if row['PhaseTransformerID' + phase] is None or row['PhaseTransformerID' + phase] == '': + continue + equipment_row = transformer_map.get(row['PhaseTransformerID' + phase], None) + name = self.map_name(row, phase) + equipment = self.map_equipment(row, phase) + buses = self.map_buses(row, section_id_sections, equipment.is_center_tapped) + winding_phases = self.map_winding_phases(row, phase, equipment_row) + + + try: + additional_transformers.append(DistributionTransformer(name=name, + buses=buses, + winding_phases=winding_phases, + equipment=equipment)) + except Exception as e: + logger.warning(f"Failed to add additional transformer {name} for phase {phase} on {row['SectionID']}: {e}") + continue + return additional_transformers + + + def map_name(self, row, phase): + name = row['SectionID'] + f"_{phase}" + return name + + def map_buses(self, row, section_id_sections, is_center_tapped=False): + section_id = str(row['SectionID']) + section = section_id_sections[section_id] + from_bus_name = section['FromNodeID'] + to_bus_name = section['ToNodeID'] + + from_bus = self.system.get_component(component_type=DistributionBus, name=from_bus_name) + to_bus = self.system.get_component(component_type=DistributionBus, name=to_bus_name) + if is_center_tapped: + return [from_bus, to_bus, to_bus] + return [from_bus, to_bus] + + def map_winding_phases(self, row, phase, equipment_row): + if equipment_row is None: + print(f"Equipment row not found for transformer {row['PhaseTransformerID' + phase]}. Assuming 2 windings.") + equipment_row = {'Type': 2} + windings_list = [] + num_windings = 3 + if equipment_row['Type'] == "4": + num_windings = 3 + else: + num_windings = 2 + + for i in range(num_windings): + winding_phases = [] + if '1' == phase: + winding_phases.append(Phase.A) + if '2' == phase: + winding_phases.append(Phase.B) + if '3' == phase: + winding_phases.append(Phase.C) + windings_list.append(winding_phases) + assert len(windings_list) == num_windings + return windings_list + + def map_equipment(self, row, phase): + transformer_id = row['PhaseTransformerID' + phase] + equipment = self.system.get_component(component_type=DistributionTransformerEquipment, name=transformer_id) + return equipment \ No newline at end of file diff --git a/src/ditto/readers/cyme/components/matrix_impedance_switch.py b/src/ditto/readers/cyme/components/matrix_impedance_switch.py new file mode 100644 index 0000000..cb9f47c --- /dev/null +++ b/src/ditto/readers/cyme/components/matrix_impedance_switch.py @@ -0,0 +1,86 @@ +from ditto.readers.cyme.cyme_mapper import CymeMapper +from ditto.readers.cyme.equipment.matrix_impedance_switch_equipment import MatrixImpedanceSwitchEquipmentMapper +from gdm.distribution.components.matrix_impedance_switch import MatrixImpedanceSwitch +from gdm.distribution.components.distribution_bus import DistributionBus +from gdm.quantities import Distance +from gdm.distribution.enums import Phase + +class MatrixImpedanceSwitchMapper(CymeMapper): + def __init__(self, system): + super().__init__(system) + + cyme_file = 'Network' + cyme_section = 'SWITCH SETTING' + + def parse(self, row, section_id_sections, equipment_data): + + name = self.map_name(row) + buses = self.map_buses(row, section_id_sections) + length = self.map_length(row) + phases = self.map_phases(row, section_id_sections) + is_closed = self.map_is_closed(row, phases) + equipment = self.map_equipment(row, phases, equipment_data) + + return MatrixImpedanceSwitch( + name=name, + buses=buses, + length=length, + phases=phases, + is_closed=is_closed, + equipment=equipment + ) + + def map_name(self, row): + name = row['SectionID'] + return name + + def map_buses(self, row, section_id_sections): + section_id = str(row['SectionID']) + section = section_id_sections[section_id] + from_bus_name = section['FromNodeID'] + to_bus_name = section['ToNodeID'] + + from_bus = self.system.get_component(component_type=DistributionBus, name=from_bus_name) + to_bus = self.system.get_component(component_type=DistributionBus, name=to_bus_name) + return [from_bus, to_bus] + + def map_length(self, row): + length = Distance(0.001,'km') + return length + + def map_phases(self, row, section_id_sections): + section_id = str(row['SectionID']) + section = section_id_sections[section_id] + phase = section['Phase'] + phases = [] + if 'A' in phase: + phases.append(Phase.A) + if 'B' in phase: + phases.append(Phase.B) + if 'C' in phase: + phases.append(Phase.C) + return phases + + def map_is_closed(self, row, phases): + is_closed = [] + for phase in phases: + if row['ConnectionStatus'] == '0': + is_closed.append(True) + else: + is_closed.append(False) + return is_closed + + + def map_equipment(self, row, phases,equipment_data): + switch_id = row['EqID'] + mapper = MatrixImpedanceSwitchEquipmentMapper(self.system) + equipment_row = equipment_data.loc[switch_id] + if equipment_row is not None: + equipment = mapper.parse(equipment_row, phases) + if equipment is not None: + return equipment + return None + + + + diff --git a/src/ditto/readers/cyme/equipment/matrix_impedance_switch_equipment.py b/src/ditto/readers/cyme/equipment/matrix_impedance_switch_equipment.py new file mode 100644 index 0000000..efdc746 --- /dev/null +++ b/src/ditto/readers/cyme/equipment/matrix_impedance_switch_equipment.py @@ -0,0 +1,71 @@ +from ditto.readers.cyme.cyme_mapper import CymeMapper +from gdm.quantities import Distance, Current, ResistancePULength, ReactancePULength, CapacitancePULength +from gdm.distribution.equipment.matrix_impedance_switch_equipment import MatrixImpedanceSwitchEquipment + + +class MatrixImpedanceSwitchEquipmentMapper(CymeMapper): + def __init__(self, system): + super().__init__(system) + + cyme_file = 'Equipment' + cyme_section = 'SWITCH' + + def parse(self, row, phases): + name = self.map_name(row) + r_matrix = self.map_r_matrix(phases) + x_matrix = self.map_x_matrix(phases) + c_matrix = self.map_c_matrix(phases) + ampacity = self.map_ampacity(row) + + return MatrixImpedanceSwitchEquipment( + name=name, + r_matrix=r_matrix, + x_matrix=x_matrix, + c_matrix=c_matrix, + ampacity=ampacity + ) + + def map_name(self, row): + return row['ID'] + + def map_r_matrix(self, phases): + default_matrix = [ + [0.08820, 0.0312137, 0.0306264], + [0.0312137, 0.0901946, 0.0316143], + [0.0306264, 0.0316143, 0.0889665], + ] + matrix = [row[:len(phases)] for row in default_matrix[:len(phases)]] + return ResistancePULength( + matrix, + "ohm/mi", + ) + + def map_x_matrix(self, phases): + default_matrix = [ + [0.20744, 0.0935314, 0.0760312], + [0.0935314, 0.200783, 0.0855879], + [0.0760312, 0.0855879, 0.204877], + ] + matrix = [row[:len(phases)] for row in default_matrix[:len(phases)]] + return ReactancePULength( + matrix, + "ohm/mi", + ) + + def map_c_matrix(self, phases): + default_matrix = [ + [2.90301, -0.679335, -0.22313], + [-0.679335, 3.15896, -0.481416], + [-0.22313, -0.481416, 2.8965], + ] + matrix = [row[:len(phases)] for row in default_matrix[:len(phases)]] + + return CapacitancePULength( + matrix, + "nanofarad/mi", + ) + + def map_ampacity(self, row): + return Current(float(row['Amps']), "ampere") + + diff --git a/src/ditto/readers/cyme/reader.py b/src/ditto/readers/cyme/reader.py index 7a1204f..ca2d784 100644 --- a/src/ditto/readers/cyme/reader.py +++ b/src/ditto/readers/cyme/reader.py @@ -12,6 +12,7 @@ class Reader(AbstractReader): # Order of components is important component_types = [ "DistributionBus", + "MatrixImpedanceSwitch", "DistributionCapacitor", "DistributionLoad", "BareConductorEquipment", @@ -20,6 +21,7 @@ class Reader(AbstractReader): "GeometryBranch", "GeometryBranchByPhase", "DistributionTransformerEquipment", + "DistributionTransformerByPhase", "DistributionTransformer" ] @@ -45,8 +47,9 @@ def read(self, network_file, equipment_file, load_file): self.system.add_component(default_conductor) node_feeder_map = {} + feeder_voltage_map = {} - section_data = read_cyme_data(network_file,"SECTION", node_feeder_map, parse_feeders=True) + section_data = read_cyme_data(network_file,"SECTION", node_feeder_map, feeder_voltage_map, parse_feeders=True) section_id_sections = section_data.set_index("SectionID").to_dict(orient="index") from_node_sections = section_data.groupby("FromNodeID").apply(lambda df: df.to_dict(orient="records")).to_dict() @@ -55,11 +58,12 @@ def read(self, network_file, equipment_file, load_file): for component_type in self.component_types: - print(component_type) + logger.info(f"Parsing Type: {component_type}") mapper_name = component_type + "Mapper" if not hasattr(cyme_mapper, mapper_name): logger.warning(f"Mapper {mapper_name} not found. Skipping.") mapper = getattr(cyme_mapper, mapper_name)(self.system) + cyme_file = mapper.cyme_file cyme_section = mapper.cyme_section @@ -83,7 +87,7 @@ def read(self, network_file, equipment_file, load_file): args = [section_id_sections, equipment_data] elif mapper_name == "DistributionBusMapper": - args = [from_node_sections, to_node_sections, node_feeder_map] + args = [from_node_sections, to_node_sections, node_feeder_map, feeder_voltage_map] elif mapper_name == "DistributionLoadMapper": @@ -103,6 +107,11 @@ def read(self, network_file, equipment_file, load_file): elif mapper_name == "GeometryBranchEquipmentMapper": args = [equipment_file] + elif mapper_name == "MatrixImpedanceSwitchMapper": + equipment_data = read_cyme_data(equipment_file, "SWITCH") + equipment_data.index = equipment_data['ID'] + args = [section_id_sections, equipment_data] + elif mapper_name == "GeometryBranchByPhaseEquipmentMapper": spacing_ids = read_cyme_data(equipment_file,"SPACING TABLE FOR LINE") spacing_ids.index = spacing_ids['ID'] @@ -115,7 +124,7 @@ def read(self, network_file, equipment_file, load_file): transformer_type = row['EqID'] transformer_map[transformer_type] = row - byphase_transformer_network_data = read_cyme_data(equipment_file, "TRANSFORMER BYPHASE SETTING") + byphase_transformer_network_data = read_cyme_data(network_file, "TRANSFORMER BYPHASE SETTING") for idx, row in byphase_transformer_network_data.iterrows(): for phase in ['1', '2', '3']: transformer_type = row['PhaseTransformerID' + phase] @@ -123,7 +132,7 @@ def read(self, network_file, equipment_file, load_file): transformer_map[transformer_type] = row args = [transformer_map] - elif mapper_name == "DistributionTransformerMapper": + elif mapper_name == "DistributionTransformerMapper" or mapper_name == "DistributionTransformerByPhaseMapper": transformer_equipment_data = read_cyme_data(equipment_file, "TRANSFORMER") transformer_map = {} for idx, row in transformer_equipment_data.iterrows(): @@ -137,6 +146,7 @@ def parse_row(row): components = data.apply(parse_row, axis=1) components = [c for c in components if c is not None] + components = [item for c in components for item in (c if isinstance(c, list) else [c])] self.system.add_components(*components) def get_system(self) -> DistributionSystem: diff --git a/src/ditto/readers/cyme/utils.py b/src/ditto/readers/cyme/utils.py index 9ca7e32..5d474b9 100644 --- a/src/ditto/readers/cyme/utils.py +++ b/src/ditto/readers/cyme/utils.py @@ -1,7 +1,7 @@ import pandas as pd from gdm.distribution.components.distribution_feeder import DistributionFeeder -def read_cyme_data(cyme_file, cyme_section, node_feeder_map = None, parse_feeders=False): +def read_cyme_data(cyme_file, cyme_section, node_feeder_map = None, feeder_voltage_map = None, parse_feeders=False): all_data = [] headers = None with open(cyme_file) as f: @@ -21,6 +21,7 @@ def read_cyme_data(cyme_file, cyme_section, node_feeder_map = None, parse_feeder if parse_feeders: if line.startswith("FEEDER"): feeder_id = line.split(",")[0].split("=")[1].strip() + feeder_voltage_map[feeder_id] = line.split(",")[9].strip() else: feeder_id = None # For SECTION Feeder headers From e04f9005791dfd3674244c01014268f7a9fbb234 Mon Sep 17 00:00:00 2001 From: "Zink, Zephyr" Date: Tue, 16 Sep 2025 11:19:02 -0600 Subject: [PATCH 16/50] fuse and recloser additions --- src/ditto/readers/cyme/__init__.py | 4 + .../cyme/components/matrix_impedance_fuse.py | 86 +++++++++++++++++ .../components/matrix_impedance_recloser.py | 92 +++++++++++++++++++ .../matrix_impedance_fuse_equipment.py | 82 +++++++++++++++++ .../matrix_impedance_recloser_equipment.py | 71 ++++++++++++++ src/ditto/readers/cyme/reader.py | 12 +++ 6 files changed, 347 insertions(+) create mode 100644 src/ditto/readers/cyme/components/matrix_impedance_fuse.py create mode 100644 src/ditto/readers/cyme/components/matrix_impedance_recloser.py create mode 100644 src/ditto/readers/cyme/equipment/matrix_impedance_fuse_equipment.py create mode 100644 src/ditto/readers/cyme/equipment/matrix_impedance_recloser_equipment.py diff --git a/src/ditto/readers/cyme/__init__.py b/src/ditto/readers/cyme/__init__.py index 362e3a0..06bb65b 100644 --- a/src/ditto/readers/cyme/__init__.py +++ b/src/ditto/readers/cyme/__init__.py @@ -11,4 +11,8 @@ from ditto.readers.cyme.components.distribution_transformer import DistributionTransformerByPhaseMapper, DistributionTransformerMapper from ditto.readers.cyme.components.matrix_impedance_switch import MatrixImpedanceSwitchMapper from ditto.readers.cyme.equipment.matrix_impedance_switch_equipment import MatrixImpedanceSwitchEquipmentMapper +from ditto.readers.cyme.components.matrix_impedance_fuse import MatrixImpedanceFuseMapper +from ditto.readers.cyme.equipment.matrix_impedance_fuse_equipment import MatrixImpedanceFuseEquipmentMapper +from ditto.readers.cyme.components.matrix_impedance_recloser import MatrixImpedanceRecloserMapper +from ditto.readers.cyme.equipment.matrix_impedance_recloser_equipment import MatrixImpedanceRecloserEquipmentMapper diff --git a/src/ditto/readers/cyme/components/matrix_impedance_fuse.py b/src/ditto/readers/cyme/components/matrix_impedance_fuse.py new file mode 100644 index 0000000..2575184 --- /dev/null +++ b/src/ditto/readers/cyme/components/matrix_impedance_fuse.py @@ -0,0 +1,86 @@ +from ditto.readers.cyme.cyme_mapper import CymeMapper +from ditto.readers.cyme.equipment.matrix_impedance_fuse_equipment import MatrixImpedanceFuseEquipmentMapper +from gdm.distribution.components.matrix_impedance_fuse import MatrixImpedanceFuse +from gdm.distribution.components.distribution_bus import DistributionBus +from gdm.quantities import Distance +from gdm.distribution.enums import Phase + +class MatrixImpedanceFuseMapper(CymeMapper): + def __init__(self, system): + super().__init__(system) + + cyme_file = 'Network' + cyme_section = 'FUSE SETTING' + + def parse(self, row, section_id_sections, equipment_data): + + name = self.map_name(row) + buses = self.map_buses(row, section_id_sections) + length = self.map_length(row) + phases = self.map_phases(row, section_id_sections) + is_closed = self.map_is_closed(row, phases) + equipment = self.map_equipment(row, phases, equipment_data) + + return MatrixImpedanceFuse( + name=name, + buses=buses, + length=length, + phases=phases, + is_closed=is_closed, + equipment=equipment + ) + + def map_name(self, row): + name = row['SectionID'] + return name + + def map_buses(self, row, section_id_sections): + section_id = str(row['SectionID']) + section = section_id_sections[section_id] + from_bus_name = section['FromNodeID'] + to_bus_name = section['ToNodeID'] + + from_bus = self.system.get_component(component_type=DistributionBus, name=from_bus_name) + to_bus = self.system.get_component(component_type=DistributionBus, name=to_bus_name) + return [from_bus, to_bus] + + def map_length(self, row): + length = Distance(0.001,'km') + return length + + def map_phases(self, row, section_id_sections): + section_id = str(row['SectionID']) + section = section_id_sections[section_id] + phase = section['Phase'] + phases = [] + if 'A' in phase: + phases.append(Phase.A) + if 'B' in phase: + phases.append(Phase.B) + if 'C' in phase: + phases.append(Phase.C) + return phases + + def map_is_closed(self, row, phases): + is_closed = [] + for phase in phases: + if row['ConnectionStatus'] == '0': + is_closed.append(True) + else: + is_closed.append(False) + return is_closed + + + def map_equipment(self, row, phases,equipment_data): + fuse_id = row['EqID'] + mapper = MatrixImpedanceFuseEquipmentMapper(self.system) + equipment_row = equipment_data.loc[fuse_id] + if equipment_row is not None: + equipment = mapper.parse(equipment_row, phases) + if equipment is not None: + return equipment + return None + + + + diff --git a/src/ditto/readers/cyme/components/matrix_impedance_recloser.py b/src/ditto/readers/cyme/components/matrix_impedance_recloser.py new file mode 100644 index 0000000..4978053 --- /dev/null +++ b/src/ditto/readers/cyme/components/matrix_impedance_recloser.py @@ -0,0 +1,92 @@ +from ditto.readers.cyme.cyme_mapper import CymeMapper +from ditto.readers.cyme.equipment.matrix_impedance_recloser_equipment import MatrixImpedanceRecloserEquipmentMapper +from gdm.distribution.components.matrix_impedance_recloser import MatrixImpedanceRecloser +from gdm.distribution.components.distribution_bus import DistributionBus +from gdm.distribution.controllers.distribution_recloser_controller import DistributionRecloserController +from gdm.quantities import Distance +from gdm.distribution.enums import Phase + +class MatrixImpedanceRecloserMapper(CymeMapper): + def __init__(self, system): + super().__init__(system) + + cyme_file = 'Network' + cyme_section = 'RECLOSER SETTING' + + def parse(self, row, section_id_sections, equipment_data): + + name = self.map_name(row) + buses = self.map_buses(row, section_id_sections) + length = self.map_length(row) + phases = self.map_phases(row, section_id_sections) + is_closed = self.map_is_closed(row, phases) + controller = self.map_controller(row) + equipment = self.map_equipment(row, phases, equipment_data) + + return MatrixImpedanceRecloser( + name=name, + buses=buses, + length=length, + phases=phases, + is_closed=is_closed, + controller=controller, + equipment=equipment + ) + + def map_name(self, row): + name = row['SectionID'] + return name + + def map_buses(self, row, section_id_sections): + section_id = str(row['SectionID']) + section = section_id_sections[section_id] + from_bus_name = section['FromNodeID'] + to_bus_name = section['ToNodeID'] + + from_bus = self.system.get_component(component_type=DistributionBus, name=from_bus_name) + to_bus = self.system.get_component(component_type=DistributionBus, name=to_bus_name) + return [from_bus, to_bus] + + def map_length(self, row): + length = Distance(0.001,'km') + return length + + def map_phases(self, row, section_id_sections): + section_id = str(row['SectionID']) + section = section_id_sections[section_id] + phase = section['Phase'] + phases = [] + if 'A' in phase: + phases.append(Phase.A) + if 'B' in phase: + phases.append(Phase.B) + if 'C' in phase: + phases.append(Phase.C) + return phases + + def map_is_closed(self, row, phases): + is_closed = [] + for phase in phases: + if row['ConnectionStatus'] == '0': + is_closed.append(True) + else: + is_closed.append(False) + return is_closed + + def map_controller(self, row): + return DistributionRecloserController.example() + + + def map_equipment(self, row, phases,equipment_data): + recloser_id = row['EqID'] + mapper = MatrixImpedanceRecloserEquipmentMapper(self.system) + equipment_row = equipment_data.loc[recloser_id] + if equipment_row is not None: + equipment = mapper.parse(equipment_row, phases) + if equipment is not None: + return equipment + return None + + + + diff --git a/src/ditto/readers/cyme/equipment/matrix_impedance_fuse_equipment.py b/src/ditto/readers/cyme/equipment/matrix_impedance_fuse_equipment.py new file mode 100644 index 0000000..cdc1a59 --- /dev/null +++ b/src/ditto/readers/cyme/equipment/matrix_impedance_fuse_equipment.py @@ -0,0 +1,82 @@ +from ditto.readers.cyme.cyme_mapper import CymeMapper +from gdm.quantities import Distance, Current, ResistancePULength, ReactancePULength, CapacitancePULength +from gdm.distribution.equipment.matrix_impedance_fuse_equipment import MatrixImpedanceFuseEquipment +from gdm.distribution.common.curve import TimeCurrentCurve +from infrasys.quantities import Time + + +class MatrixImpedanceFuseEquipmentMapper(CymeMapper): + def __init__(self, system): + super().__init__(system) + + cyme_file = 'Equipment' + cyme_section = 'FUSE' + + def parse(self, row, phases): + name = self.map_name(row) + delay = self.map_delay(row) + tcc_curve = self.map_tcc_curve(row) + r_matrix = self.map_r_matrix(phases) + x_matrix = self.map_x_matrix(phases) + c_matrix = self.map_c_matrix(phases) + ampacity = self.map_ampacity(row) + + return MatrixImpedanceFuseEquipment( + name=name, + delay=delay, + tcc_curve=tcc_curve, + r_matrix=r_matrix, + x_matrix=x_matrix, + c_matrix=c_matrix, + ampacity=ampacity + ) + + def map_name(self, row): + return row['ID'] + + def map_r_matrix(self, phases): + default_matrix = [ + [0.08820, 0.0312137, 0.0306264], + [0.0312137, 0.0901946, 0.0316143], + [0.0306264, 0.0316143, 0.0889665], + ] + matrix = [row[:len(phases)] for row in default_matrix[:len(phases)]] + return ResistancePULength( + matrix, + "ohm/mi", + ) + + def map_x_matrix(self, phases): + default_matrix = [ + [0.20744, 0.0935314, 0.0760312], + [0.0935314, 0.200783, 0.0855879], + [0.0760312, 0.0855879, 0.204877], + ] + matrix = [row[:len(phases)] for row in default_matrix[:len(phases)]] + return ReactancePULength( + matrix, + "ohm/mi", + ) + + def map_c_matrix(self, phases): + default_matrix = [ + [2.90301, -0.679335, -0.22313], + [-0.679335, 3.15896, -0.481416], + [-0.22313, -0.481416, 2.8965], + ] + matrix = [row[:len(phases)] for row in default_matrix[:len(phases)]] + + return CapacitancePULength( + matrix, + "nanofarad/mi", + ) + + def map_ampacity(self, row): + return Current(float(row['Amps']), "ampere") + + def map_delay(self, row): + return Time(0, "minutes") + + def map_tcc_curve(self, row): + return TimeCurrentCurve.example() + diff --git a/src/ditto/readers/cyme/equipment/matrix_impedance_recloser_equipment.py b/src/ditto/readers/cyme/equipment/matrix_impedance_recloser_equipment.py new file mode 100644 index 0000000..14555c4 --- /dev/null +++ b/src/ditto/readers/cyme/equipment/matrix_impedance_recloser_equipment.py @@ -0,0 +1,71 @@ +from ditto.readers.cyme.cyme_mapper import CymeMapper +from gdm.quantities import Distance, Current, ResistancePULength, ReactancePULength, CapacitancePULength +from gdm.distribution.equipment.matrix_impedance_recloser_equipment import MatrixImpedanceRecloserEquipment + + +class MatrixImpedanceRecloserEquipmentMapper(CymeMapper): + def __init__(self, system): + super().__init__(system) + + cyme_file = 'Equipment' + cyme_section = 'RECLOSER' + + def parse(self, row, phases): + name = self.map_name(row) + r_matrix = self.map_r_matrix(phases) + x_matrix = self.map_x_matrix(phases) + c_matrix = self.map_c_matrix(phases) + ampacity = self.map_ampacity(row) + + return MatrixImpedanceRecloserEquipment( + name=name, + r_matrix=r_matrix, + x_matrix=x_matrix, + c_matrix=c_matrix, + ampacity=ampacity + ) + + def map_name(self, row): + return row['ID'] + + def map_r_matrix(self, phases): + default_matrix = [ + [0.08820, 0.0312137, 0.0306264], + [0.0312137, 0.0901946, 0.0316143], + [0.0306264, 0.0316143, 0.0889665], + ] + matrix = [row[:len(phases)] for row in default_matrix[:len(phases)]] + return ResistancePULength( + matrix, + "ohm/mi", + ) + + def map_x_matrix(self, phases): + default_matrix = [ + [0.20744, 0.0935314, 0.0760312], + [0.0935314, 0.200783, 0.0855879], + [0.0760312, 0.0855879, 0.204877], + ] + matrix = [row[:len(phases)] for row in default_matrix[:len(phases)]] + return ReactancePULength( + matrix, + "ohm/mi", + ) + + def map_c_matrix(self, phases): + default_matrix = [ + [2.90301, -0.679335, -0.22313], + [-0.679335, 3.15896, -0.481416], + [-0.22313, -0.481416, 2.8965], + ] + matrix = [row[:len(phases)] for row in default_matrix[:len(phases)]] + + return CapacitancePULength( + matrix, + "nanofarad/mi", + ) + + def map_ampacity(self, row): + return Current(float(row['Amps']), "ampere") + + diff --git a/src/ditto/readers/cyme/reader.py b/src/ditto/readers/cyme/reader.py index ca2d784..7fb3497 100644 --- a/src/ditto/readers/cyme/reader.py +++ b/src/ditto/readers/cyme/reader.py @@ -12,7 +12,9 @@ class Reader(AbstractReader): # Order of components is important component_types = [ "DistributionBus", + "MatrixImpedanceRecloser", "MatrixImpedanceSwitch", + "MatrixImpedanceFuse", "DistributionCapacitor", "DistributionLoad", "BareConductorEquipment", @@ -112,6 +114,16 @@ def read(self, network_file, equipment_file, load_file): equipment_data.index = equipment_data['ID'] args = [section_id_sections, equipment_data] + elif mapper_name == "MatrixImpedanceFuseMapper": + equipment_data = read_cyme_data(equipment_file, "FUSE") + equipment_data.index = equipment_data['ID'] + args = [section_id_sections, equipment_data] + + elif mapper_name == "MatrixImpedanceRecloserMapper": + equipment_data = read_cyme_data(equipment_file, "RECLOSER") + equipment_data.index = equipment_data['ID'] + args = [section_id_sections, equipment_data] + elif mapper_name == "GeometryBranchByPhaseEquipmentMapper": spacing_ids = read_cyme_data(equipment_file,"SPACING TABLE FOR LINE") spacing_ids.index = spacing_ids['ID'] From a74227e100fe84727988933f60a29332f8358d82 Mon Sep 17 00:00:00 2001 From: "Zink, Zephyr" Date: Tue, 30 Sep 2025 15:49:22 -0600 Subject: [PATCH 17/50] feeder construction, other stuff --- src/ditto/readers/cyme/__init__.py | 3 + .../cyme/components/distribution_bus.py | 2 +- .../cyme/components/distribution_capacitor.py | 2 +- .../cyme/components/distribution_load.py | 8 +- .../components/distribution_transformer.py | 46 +-- .../components/distribution_voltage_source.py | 58 ++++ .../cyme/components/geometry_branch.py | 10 +- .../components/matrix_impedance_branch.py | 97 +++++++ .../cyme/components/matrix_impedance_fuse.py | 5 +- .../components/matrix_impedance_recloser.py | 4 +- .../components/matrix_impedance_switch.py | 4 +- .../distribution_transformer_equipment.py | 95 +++++-- .../matrix_impedance_fuse_equipment.py | 22 +- .../matrix_impedance_recloser_equipment.py | 25 +- .../matrix_impedance_switch_equipment.py | 21 +- .../phase_voltagesource_equipment.py | 26 ++ src/ditto/readers/cyme/reader.py | 266 +++++++++++++----- src/ditto/readers/opendss/common.py | 3 +- 18 files changed, 525 insertions(+), 172 deletions(-) create mode 100644 src/ditto/readers/cyme/components/distribution_voltage_source.py create mode 100644 src/ditto/readers/cyme/components/matrix_impedance_branch.py create mode 100644 src/ditto/readers/cyme/equipment/phase_voltagesource_equipment.py diff --git a/src/ditto/readers/cyme/__init__.py b/src/ditto/readers/cyme/__init__.py index 06bb65b..4ab920e 100644 --- a/src/ditto/readers/cyme/__init__.py +++ b/src/ditto/readers/cyme/__init__.py @@ -15,4 +15,7 @@ from ditto.readers.cyme.equipment.matrix_impedance_fuse_equipment import MatrixImpedanceFuseEquipmentMapper from ditto.readers.cyme.components.matrix_impedance_recloser import MatrixImpedanceRecloserMapper from ditto.readers.cyme.equipment.matrix_impedance_recloser_equipment import MatrixImpedanceRecloserEquipmentMapper +from ditto.readers.cyme.components.matrix_impedance_branch import MatrixImpedanceBranchMapper +from ditto.readers.cyme.components.distribution_voltage_source import DistributionVoltageSourceMapper +from ditto.readers.cyme.equipment.phase_voltagesource_equipment import PhaseVoltageSourceEquipmentMapper diff --git a/src/ditto/readers/cyme/components/distribution_bus.py b/src/ditto/readers/cyme/components/distribution_bus.py index 47b4c78..3209c72 100644 --- a/src/ditto/readers/cyme/components/distribution_bus.py +++ b/src/ditto/readers/cyme/components/distribution_bus.py @@ -23,7 +23,7 @@ def parse(self, row, from_node_sections, to_node_sections, node_feeder_map, feed rated_voltage = self.map_rated_voltage(row, phases, feeder_voltage_map.get(feeder_name)) voltage_limits = self.map_voltagelimits(row) voltage_type = self.map_voltage_type(row) - return DistributionBus(name=name, + return DistributionBus.model_construct(name=name, coordinate=coordinate, rated_voltage=rated_voltage, feeder=feeder, diff --git a/src/ditto/readers/cyme/components/distribution_capacitor.py b/src/ditto/readers/cyme/components/distribution_capacitor.py index a4df40c..e5e0dd9 100644 --- a/src/ditto/readers/cyme/components/distribution_capacitor.py +++ b/src/ditto/readers/cyme/components/distribution_capacitor.py @@ -20,7 +20,7 @@ def parse(self, row, section_id_sections, equipment_data): controllers = self.map_controllers(row) equipment = self.map_equipment(row, equipment_data) in_service = self.map_in_service(row) - return DistributionCapacitor(name=name, + return DistributionCapacitor.model_construct(name=name, bus=bus, phases=phases, controllers=controllers, diff --git a/src/ditto/readers/cyme/components/distribution_load.py b/src/ditto/readers/cyme/components/distribution_load.py index ae1466b..c506ff2 100644 --- a/src/ditto/readers/cyme/components/distribution_load.py +++ b/src/ditto/readers/cyme/components/distribution_load.py @@ -19,10 +19,16 @@ def parse(self, row, section_id_sections, equipment_file): bus = self.map_bus(row, section_id_sections) phases = self.map_phases(row) equipment = self.map_equipment(row, equipment_file) + if len(list(self.system.list_components_by_name(DistributionLoad, name))) > 0: + existing_load = self.system.get_component(component_type=DistributionLoad, name=name) + existing_load.equipment.phase_loads.real_power += equipment.phase_loads.real_power + existing_load.equipment.phase_loads.reactive_power += equipment.phase_loads.reactive_power + return None + if len(phases) == 0: logger.warning(f"Load {name} has no kW values. Skipping...") return None - return DistributionLoad(name=name, + return DistributionLoad.model_construct(name=name, bus=bus, phases=phases, equipment=equipment) diff --git a/src/ditto/readers/cyme/components/distribution_transformer.py b/src/ditto/readers/cyme/components/distribution_transformer.py index ce453be..2e57b37 100644 --- a/src/ditto/readers/cyme/components/distribution_transformer.py +++ b/src/ditto/readers/cyme/components/distribution_transformer.py @@ -1,10 +1,10 @@ from loguru import logger from ditto.readers.cyme.cyme_mapper import CymeMapper +from ditto.readers.cyme.equipment.distribution_transformer_equipment import DistributionTransformerEquipmentMapper from gdm.distribution.components.distribution_bus import DistributionBus from gdm.distribution.components.distribution_transformer import DistributionTransformer from gdm.distribution.equipment.distribution_transformer_equipment import DistributionTransformerEquipment -from gdm.quantities import ActivePower, ReactivePower -from gdm.distribution.enums import ConnectionType, Phase +from gdm.distribution.enums import Phase class DistributionTransformerMapper(CymeMapper): def __init__(self, system): @@ -13,14 +13,15 @@ def __init__(self, system): cyme_file = 'Network' cyme_section = 'TRANSFORMER SETTING' - def parse(self, row, section_id_sections, transformer_map): - equipment_row = transformer_map.get(row['EqID'], None) + def parse(self, row, used_sections, section_id_sections, equipment_data): + equipment_row = equipment_data.get(row['EqID'], None) name = self.map_name(row) buses = self.map_buses(row, section_id_sections) winding_phases = self.map_winding_phases(row, section_id_sections, equipment_row) - equipment = self.map_equipment(row) + equipment = self.map_equipment(row, equipment_row) try: - return DistributionTransformer(name=name, + used_sections.add(name) + return DistributionTransformer.model_construct(name=name, buses=buses, winding_phases=winding_phases, equipment=equipment) @@ -66,10 +67,13 @@ def map_winding_phases(self, row, section_id_sections, equipment_row): windings_list.append(winding_phases) return windings_list - def map_equipment(self, row): - transformer_id = row['EqID'] - equipment = self.system.get_component(component_type=DistributionTransformerEquipment, name=transformer_id) - return equipment + def map_equipment(self, row, equipment_row): + mapper = DistributionTransformerEquipmentMapper(self.system) + if equipment_row is not None: + equipment = mapper.parse(equipment_row, row) + if equipment is not None: + return equipment + return None class DistributionTransformerByPhaseMapper(CymeMapper): @@ -79,20 +83,23 @@ def __init__(self, system): cyme_file = 'Network' cyme_section = 'TRANSFORMER BYPHASE SETTING' - def parse(self, row, section_id_sections, transformer_map): + def parse(self, row, used_sections, section_id_sections, equipment_data): additional_transformers = [] + for phase in ['1', '2', '3']: if row['PhaseTransformerID' + phase] is None or row['PhaseTransformerID' + phase] == '': continue - equipment_row = transformer_map.get(row['PhaseTransformerID' + phase], None) + equipment_row = equipment_data.get(row['PhaseTransformerID' + phase], None) + name = self.map_name(row, phase) - equipment = self.map_equipment(row, phase) + equipment = self.map_equipment(row, phase, equipment_row) buses = self.map_buses(row, section_id_sections, equipment.is_center_tapped) winding_phases = self.map_winding_phases(row, phase, equipment_row) try: - additional_transformers.append(DistributionTransformer(name=name, + used_sections.add(row['SectionID']) + additional_transformers.append(DistributionTransformer.model_construct(name=name, buses=buses, winding_phases=winding_phases, equipment=equipment)) @@ -141,7 +148,10 @@ def map_winding_phases(self, row, phase, equipment_row): assert len(windings_list) == num_windings return windings_list - def map_equipment(self, row, phase): - transformer_id = row['PhaseTransformerID' + phase] - equipment = self.system.get_component(component_type=DistributionTransformerEquipment, name=transformer_id) - return equipment \ No newline at end of file + def map_equipment(self, row, phase, equipment_row): + mapper = DistributionTransformerEquipmentMapper(self.system) + if equipment_row is not None: + equipment = mapper.parse(equipment_row, row) + if equipment is not None: + return equipment + return None \ No newline at end of file diff --git a/src/ditto/readers/cyme/components/distribution_voltage_source.py b/src/ditto/readers/cyme/components/distribution_voltage_source.py new file mode 100644 index 0000000..4edad23 --- /dev/null +++ b/src/ditto/readers/cyme/components/distribution_voltage_source.py @@ -0,0 +1,58 @@ +from ditto.readers.cyme.cyme_mapper import CymeMapper +from ditto.readers.cyme.equipment.phase_voltagesource_equipment import PhaseVoltageSourceEquipmentMapper +from gdm.distribution.components.distribution_bus import DistributionBus +from gdm.distribution.components.distribution_vsource import DistributionVoltageSource +from gdm.distribution.equipment.voltagesource_equipment import VoltageSourceEquipment + + + +class DistributionVoltageSourceMapper(CymeMapper): + def __init__(self, cyme_model): + super().__init__(cyme_model) + + cyme_file = 'Network' + cyme_section = 'HEADNODES' + + def parse(self, row, feeder_voltage_map): + name = self.map_name(row) + bus = self.map_bus(row) + feeder = bus.feeder + if feeder is None: + return None + feeder_id = feeder.name + + feeder_voltage = feeder_voltage_map.get(feeder_id) + + if feeder_voltage is None or feeder_voltage == '': + return None + + phases = bus.phases + equipment = self.map_equipment(bus, feeder_id, feeder_voltage) + + return DistributionVoltageSource.model_construct(name=name, + feeder=feeder, + bus=bus, + phases=phases, + equipment=equipment) + + def map_name(self, row): + name = row['NodeID'] + return name + + def map_feeder(self, row): + feeder = row['NetworkID'] + return feeder + + def map_bus(self, row): + bus_name = row['NodeID'] + bus = self.system.get_component(DistributionBus, bus_name) + return bus + + def map_equipment(self, bus, feeder, feeder_voltage): + mapper = PhaseVoltageSourceEquipmentMapper(self.system) + sources = mapper.parse(bus, feeder_voltage) + return VoltageSourceEquipment.model_construct( + name=feeder+bus.name+"-source", + sources=sources + ) + \ No newline at end of file diff --git a/src/ditto/readers/cyme/components/geometry_branch.py b/src/ditto/readers/cyme/components/geometry_branch.py index 8de80ed..52956c0 100644 --- a/src/ditto/readers/cyme/components/geometry_branch.py +++ b/src/ditto/readers/cyme/components/geometry_branch.py @@ -14,14 +14,15 @@ def __init__(self, system): cyme_file = 'Network' cyme_section = 'OVERHEADLINE SETTING' - def parse(self, row, section_id_sections): + def parse(self, row, used_sections, section_id_sections): name = self.map_name(row) buses = self.map_buses(row,section_id_sections) length = self.map_length(row) phases = self.map_phases(row, section_id_sections) equipment = self.map_equipment(row) try: - return GeometryBranch(name=name, + used_sections.add(name) + return GeometryBranch.model_construct(name=name, buses=buses, length=length, phases=phases, @@ -74,14 +75,15 @@ def __init__(self, system): cyme_file = 'Network' cyme_section = 'OVERHEAD BYPHASE SETTING' - def parse(self, row, section_id_sections): + def parse(self, row, used_sections, section_id_sections): name = self.map_name(row) buses = self.map_buses(row,section_id_sections) length = self.map_length(row) phases = self.map_phases(row, section_id_sections) equipment = self.map_equipment(row) try: - return GeometryBranch(name=name, + used_sections.add(name) + return GeometryBranch.model_construct(name=name, buses=buses, length=length, phases=phases, diff --git a/src/ditto/readers/cyme/components/matrix_impedance_branch.py b/src/ditto/readers/cyme/components/matrix_impedance_branch.py new file mode 100644 index 0000000..ac77128 --- /dev/null +++ b/src/ditto/readers/cyme/components/matrix_impedance_branch.py @@ -0,0 +1,97 @@ + +from gdm.quantities import Distance, Current, ResistancePULength, ReactancePULength, CapacitancePULength +from ditto.readers.cyme.cyme_mapper import CymeMapper +from gdm.distribution.equipment.matrix_impedance_branch_equipment import MatrixImpedanceBranchEquipment +from gdm.distribution.components.matrix_impedance_branch import MatrixImpedanceBranch +from gdm.distribution.components.distribution_bus import DistributionBus +from gdm.distribution.enums import Phase + + + + +class MatrixImpedanceBranchMapper(CymeMapper): + def __init__(self, system): + super().__init__(system) + + cyme_file = 'Network' + cyme_section = 'SECTION' + + def parse(self, row, used_sections, section_id_sections): + + name = self.map_name(row) + if name in used_sections: + return None + buses = self.map_buses(row,section_id_sections) + length = self.map_length() + phases = self.map_phases(row, section_id_sections) + equipment = self.map_equipment(row, phases) + + try: + return MatrixImpedanceBranch.model_construct(name=name, + buses=buses, + length=length, + phases=phases, + equipment=equipment) + except Exception as e: + print(f"Error creating MatrixImpedanceBranch {name}: {e}") + print(buses) + return None + + def map_name(self, row): + name = row['SectionID'] + return name + + def map_buses(self, row, section_id_sections): + section_id = str(row['SectionID']) + section = section_id_sections[section_id] + from_bus_name = section['FromNodeID'] + to_bus_name = section['ToNodeID'] + + from_bus = self.system.get_component(component_type=DistributionBus, name=from_bus_name) + to_bus = self.system.get_component(component_type=DistributionBus, name=to_bus_name) + return [from_bus, to_bus] + + def map_length(self): + length = Distance(float(1.0),'m') + return length + + def map_phases(self, row, section_id_sections): + section_id = str(row['SectionID']) + section = section_id_sections[section_id] + phase = section['Phase'] + phases = [] + if 'A' in phase: + phases.append(Phase.A) + if 'B' in phase: + phases.append(Phase.B) + if 'C' in phase: + phases.append(Phase.C) + return phases + + def map_equipment(self, row, phases): + default_matrix = [ + [0.0, 0.0, 0.0], + [0.0, 0.0, 0.0], + [0.0, 0.0, 0.0], + ] + matrix = [row[:len(phases)] for row in default_matrix[:len(phases)]] + r_matrix = ResistancePULength( + matrix, + "ohm/mi", + ) + x_matrix = ReactancePULength( + matrix, + "ohm/mi", + ) + c_matrix = CapacitancePULength( + matrix, + "nanofarad/mi", + ) + ampacity = Current(600.0, "A") + line = MatrixImpedanceBranchEquipment.model_construct(name=row['SectionID'], + r_matrix=r_matrix, + x_matrix=x_matrix, + c_matrix=c_matrix, + ampacity=ampacity + ) + return line \ No newline at end of file diff --git a/src/ditto/readers/cyme/components/matrix_impedance_fuse.py b/src/ditto/readers/cyme/components/matrix_impedance_fuse.py index 2575184..745a71e 100644 --- a/src/ditto/readers/cyme/components/matrix_impedance_fuse.py +++ b/src/ditto/readers/cyme/components/matrix_impedance_fuse.py @@ -12,7 +12,7 @@ def __init__(self, system): cyme_file = 'Network' cyme_section = 'FUSE SETTING' - def parse(self, row, section_id_sections, equipment_data): + def parse(self, row, used_sections, section_id_sections, equipment_data): name = self.map_name(row) buses = self.map_buses(row, section_id_sections) @@ -20,7 +20,8 @@ def parse(self, row, section_id_sections, equipment_data): phases = self.map_phases(row, section_id_sections) is_closed = self.map_is_closed(row, phases) equipment = self.map_equipment(row, phases, equipment_data) - + + used_sections.add(name) return MatrixImpedanceFuse( name=name, buses=buses, diff --git a/src/ditto/readers/cyme/components/matrix_impedance_recloser.py b/src/ditto/readers/cyme/components/matrix_impedance_recloser.py index 4978053..e90fc9a 100644 --- a/src/ditto/readers/cyme/components/matrix_impedance_recloser.py +++ b/src/ditto/readers/cyme/components/matrix_impedance_recloser.py @@ -13,7 +13,7 @@ def __init__(self, system): cyme_file = 'Network' cyme_section = 'RECLOSER SETTING' - def parse(self, row, section_id_sections, equipment_data): + def parse(self, row, used_sections, section_id_sections, equipment_data): name = self.map_name(row) buses = self.map_buses(row, section_id_sections) @@ -23,6 +23,8 @@ def parse(self, row, section_id_sections, equipment_data): controller = self.map_controller(row) equipment = self.map_equipment(row, phases, equipment_data) + used_sections.add(name) + return MatrixImpedanceRecloser( name=name, buses=buses, diff --git a/src/ditto/readers/cyme/components/matrix_impedance_switch.py b/src/ditto/readers/cyme/components/matrix_impedance_switch.py index cb9f47c..0c35b15 100644 --- a/src/ditto/readers/cyme/components/matrix_impedance_switch.py +++ b/src/ditto/readers/cyme/components/matrix_impedance_switch.py @@ -12,7 +12,7 @@ def __init__(self, system): cyme_file = 'Network' cyme_section = 'SWITCH SETTING' - def parse(self, row, section_id_sections, equipment_data): + def parse(self, row, used_sections, section_id_sections, equipment_data): name = self.map_name(row) buses = self.map_buses(row, section_id_sections) @@ -20,7 +20,7 @@ def parse(self, row, section_id_sections, equipment_data): phases = self.map_phases(row, section_id_sections) is_closed = self.map_is_closed(row, phases) equipment = self.map_equipment(row, phases, equipment_data) - + used_sections.add(name) return MatrixImpedanceSwitch( name=name, buses=buses, diff --git a/src/ditto/readers/cyme/equipment/distribution_transformer_equipment.py b/src/ditto/readers/cyme/equipment/distribution_transformer_equipment.py index d2954ed..0cb7313 100644 --- a/src/ditto/readers/cyme/equipment/distribution_transformer_equipment.py +++ b/src/ditto/readers/cyme/equipment/distribution_transformer_equipment.py @@ -13,22 +13,16 @@ def __init__(self, system): cyme_file = 'Equipment' cyme_section = 'TRANSFORMER' - def parse(self, row, transformer_map): - network_row = None - if row['ID'] in transformer_map: - network_row = transformer_map[row['ID']] - - if network_row is None: - return None + def parse(self, row, network_row): name = self.map_name(row) pct_no_load_loss = self.map_pct_no_load_loss(row) pct_full_load_loss = self.map_pct_full_load_loss(row) - windings = self.map_windings(row, network_row) - winding_reactances = self.map_winding_reactances(row) is_center_tapped = self.map_is_center_tapped(row) - coupling_sequences = self.map_coupling(row) - + windings = self.map_windings(row, network_row, is_center_tapped) + winding_reactances = self.map_winding_reactances(row, is_center_tapped) + coupling_sequences = self.map_coupling(row, is_center_tapped) + return DistributionTransformerEquipment(name=name, pct_no_load_loss=pct_no_load_loss, pct_full_load_loss=pct_full_load_loss, @@ -55,14 +49,14 @@ def map_pct_full_load_loss(self, row): pct_full_load_loss = rated_current_sec*resistance_sec/100 return pct_full_load_loss - def map_winding_reactances(self, row): + def map_winding_reactances(self, row, is_center_tapped): xr_ratio = float(row['XR']) if xr_ratio == 0: xr_ratio = 0.01 rx_ratio = 1/xr_ratio reactance_pu = float(row['Z1'])/((1+rx_ratio**2)**0.5) transformer_type = row['Type'] - if transformer_type == '4': + if is_center_tapped: winding_reactances = [reactance_pu, reactance_pu, reactance_pu] else: winding_reactances = [reactance_pu] @@ -74,11 +68,8 @@ def map_is_center_tapped(self, row): return True return False - def map_windings(self, row, network_row): + def map_windings(self, row, network_row, is_center_tapped): windings = [] - is_center_tapped = False - if row['Type'] == '4': - is_center_tapped = True winding_mapper1 = WindingEquipmentMapper(self.system) winding_1 = winding_mapper1.parse(row, network_row, winding_number=1) @@ -92,9 +83,9 @@ def map_windings(self, row, network_row): windings.append(winding_3) return windings - def map_coupling(self, row): + def map_coupling(self, row, is_center_tapped): transformer_type = row['Type'] - if transformer_type == '4': + if is_center_tapped: coupling = [SequencePair(0,1), SequencePair(0,2), SequencePair(1,2)] @@ -122,16 +113,52 @@ def __init__(self, system): 10: 'YNG_Y', 11: 'Yg_Zg', 12: 'D_Zg', - 15: 'YG_CT', - 16: 'D_DCT', } + + # The documentation is confusing on where the below connection types are used + # It appears they are used in the equipment file although it is repoted in the network file + connection_map = { + 0: 'Yg_Yg', + 1: 'D_Yg', + 2: 'D_D', + 3: 'Y_Y', + 4: 'DO_DO', + 5: 'YO_D', + 6: 'Yg_D', + 7: 'D_Y', + 8: 'Y_D', + 9: 'Yg_Y', + 10: 'Y_Yg', + 11: 'Yg_Zg', + 12: 'D_Zg', + 13: 'Zg_Yg', + 14: 'Zg_D', + 15: 'Yg_CT', + 16: 'D_CT', + 17: 'Yg_DCT', + 18: 'D_DCT', + 19: 'Y_DCT', + 20: 'DO_DOCT', + 21: 'YO_DOCT', + 22: 'DO_YO', + 23: 'Yg_Dn', + 24: 'Y_Dn', + 25: 'D_Dn', + 26: 'Zg_Dn', + 27: 'Dn_Yg', + 28: 'Dn_Y', + 29: 'Dn_D', + 30: 'Dn_Dn', + 31: 'Dn_Zg', + 99: 'Equip_Connection', + } def parse(self, row, network_row, winding_number): name = self.map_name(row) resistance = self.map_resistance(row, winding_number) is_grounded = self.map_is_grounded(row, winding_number) rated_voltage = self.map_rated_voltage(row, winding_number) - voltage_type = self.map_voltage_type(row) + voltage_type = self.map_voltage_type(row, rated_voltage) rated_power = self.map_rated_power(row) num_phases = self.map_num_phases(row) connection_type = self.map_connection_type(row, winding_number) @@ -198,7 +225,11 @@ def map_rated_voltage(self, row, winding_number): voltage = Voltage(float(voltage), "kilovolt") return voltage - def map_voltage_type(self, row): + def map_voltage_type(self, row, rated_voltage): + # This is from the CYME documentation but appears to not be entirely correct + # Clearly L-L voltages still appear with a voltage type of 1 + if row["VoltageUnit"] == '1': + return VoltageTypes.LINE_TO_GROUND return VoltageTypes.LINE_TO_LINE def map_rated_power(self, row): @@ -239,7 +270,7 @@ def map_connection_type(self, row, winding_number): elif 'CT' == winding_type: connection_type = 'STAR' else: - raise ValueError("Unknown winding type: {}".format(winding_type)) + connection_type = 'STAR' return ConnectionType(connection_type) @@ -257,8 +288,8 @@ def map_tap_positions(self, row, winding_number, network_row): else: tap = network_row.get('PrimTap', None) if tap is None: - tap = network_row.get('PrimaryTapSettingA', 100) - tap = float(tap) - 100 + tap = network_row.get('PrimaryTapSettingA', 0) + tap = float(tap) / 100 elif winding_number == 2: if network_row is None: @@ -266,18 +297,20 @@ def map_tap_positions(self, row, winding_number, network_row): else: tap = network_row.get('SecTap', None) if tap is None: - tap = network_row.get('SecondaryTapSettingA', 100) - tap = float(tap) - 100 + tap = network_row.get('SecondaryTapSettingA', 0) + tap = float(tap) / 100 elif winding_number == 3: if network_row is None: tap = 0.0 else: tap = network_row.get('SecTap', None) if tap is None: - tap = network_row.get('SecondaryTapSettingA', 100) - tap = float(tap) - 100 + tap = network_row.get('SecondaryTapSettingA', 0) + tap = float(tap) / 100 + if row['Taps'] == '' or row['Taps'] is None: + tap = 0.0 for phase in range(1, num_phases + 1): - tap_positions.append(0.0) + tap_positions.append(tap) return tap_positions def map_total_taps(self, row): diff --git a/src/ditto/readers/cyme/equipment/matrix_impedance_fuse_equipment.py b/src/ditto/readers/cyme/equipment/matrix_impedance_fuse_equipment.py index cdc1a59..36ebad3 100644 --- a/src/ditto/readers/cyme/equipment/matrix_impedance_fuse_equipment.py +++ b/src/ditto/readers/cyme/equipment/matrix_impedance_fuse_equipment.py @@ -3,6 +3,7 @@ from gdm.distribution.equipment.matrix_impedance_fuse_equipment import MatrixImpedanceFuseEquipment from gdm.distribution.common.curve import TimeCurrentCurve from infrasys.quantities import Time +from gdm.distribution.enums import LineType class MatrixImpedanceFuseEquipmentMapper(CymeMapper): @@ -25,6 +26,7 @@ def parse(self, row, phases): name=name, delay=delay, tcc_curve=tcc_curve, + construction=LineType.OVERHEAD, r_matrix=r_matrix, x_matrix=x_matrix, c_matrix=c_matrix, @@ -36,10 +38,10 @@ def map_name(self, row): def map_r_matrix(self, phases): default_matrix = [ - [0.08820, 0.0312137, 0.0306264], - [0.0312137, 0.0901946, 0.0316143], - [0.0306264, 0.0316143, 0.0889665], - ] + [0.0, 0.0, 0.0], + [0.0, 0.0, 0.0], + [0.0, 0.0, 0.0], + ] matrix = [row[:len(phases)] for row in default_matrix[:len(phases)]] return ResistancePULength( matrix, @@ -48,9 +50,9 @@ def map_r_matrix(self, phases): def map_x_matrix(self, phases): default_matrix = [ - [0.20744, 0.0935314, 0.0760312], - [0.0935314, 0.200783, 0.0855879], - [0.0760312, 0.0855879, 0.204877], + [0.0, 0.0, 0.0], + [0.0, 0.0, 0.0], + [0.0, 0.0, 0.0], ] matrix = [row[:len(phases)] for row in default_matrix[:len(phases)]] return ReactancePULength( @@ -60,9 +62,9 @@ def map_x_matrix(self, phases): def map_c_matrix(self, phases): default_matrix = [ - [2.90301, -0.679335, -0.22313], - [-0.679335, 3.15896, -0.481416], - [-0.22313, -0.481416, 2.8965], + [0.0, 0.0, 0.0], + [0.0, 0.0, 0.0], + [0.0, 0.0, 0.0], ] matrix = [row[:len(phases)] for row in default_matrix[:len(phases)]] diff --git a/src/ditto/readers/cyme/equipment/matrix_impedance_recloser_equipment.py b/src/ditto/readers/cyme/equipment/matrix_impedance_recloser_equipment.py index 14555c4..12f38fd 100644 --- a/src/ditto/readers/cyme/equipment/matrix_impedance_recloser_equipment.py +++ b/src/ditto/readers/cyme/equipment/matrix_impedance_recloser_equipment.py @@ -1,7 +1,7 @@ from ditto.readers.cyme.cyme_mapper import CymeMapper -from gdm.quantities import Distance, Current, ResistancePULength, ReactancePULength, CapacitancePULength +from gdm.quantities import Current, ResistancePULength, ReactancePULength, CapacitancePULength from gdm.distribution.equipment.matrix_impedance_recloser_equipment import MatrixImpedanceRecloserEquipment - +from gdm.distribution.enums import LineType class MatrixImpedanceRecloserEquipmentMapper(CymeMapper): def __init__(self, system): @@ -19,6 +19,7 @@ def parse(self, row, phases): return MatrixImpedanceRecloserEquipment( name=name, + construction=LineType.OVERHEAD, r_matrix=r_matrix, x_matrix=x_matrix, c_matrix=c_matrix, @@ -30,10 +31,10 @@ def map_name(self, row): def map_r_matrix(self, phases): default_matrix = [ - [0.08820, 0.0312137, 0.0306264], - [0.0312137, 0.0901946, 0.0316143], - [0.0306264, 0.0316143, 0.0889665], - ] + [0.0, 0.0, 0.0], + [0.0, 0.0, 0.0], + [0.0, 0.0, 0.0], + ] matrix = [row[:len(phases)] for row in default_matrix[:len(phases)]] return ResistancePULength( matrix, @@ -42,9 +43,9 @@ def map_r_matrix(self, phases): def map_x_matrix(self, phases): default_matrix = [ - [0.20744, 0.0935314, 0.0760312], - [0.0935314, 0.200783, 0.0855879], - [0.0760312, 0.0855879, 0.204877], + [0.0, 0.0, 0.0], + [0.0, 0.0, 0.0], + [0.0, 0.0, 0.0], ] matrix = [row[:len(phases)] for row in default_matrix[:len(phases)]] return ReactancePULength( @@ -54,9 +55,9 @@ def map_x_matrix(self, phases): def map_c_matrix(self, phases): default_matrix = [ - [2.90301, -0.679335, -0.22313], - [-0.679335, 3.15896, -0.481416], - [-0.22313, -0.481416, 2.8965], + [0.0, 0.0, 0.0], + [0.0, 0.0, 0.0], + [0.0, 0.0, 0.0], ] matrix = [row[:len(phases)] for row in default_matrix[:len(phases)]] diff --git a/src/ditto/readers/cyme/equipment/matrix_impedance_switch_equipment.py b/src/ditto/readers/cyme/equipment/matrix_impedance_switch_equipment.py index efdc746..9743ba2 100644 --- a/src/ditto/readers/cyme/equipment/matrix_impedance_switch_equipment.py +++ b/src/ditto/readers/cyme/equipment/matrix_impedance_switch_equipment.py @@ -1,7 +1,7 @@ from ditto.readers.cyme.cyme_mapper import CymeMapper from gdm.quantities import Distance, Current, ResistancePULength, ReactancePULength, CapacitancePULength from gdm.distribution.equipment.matrix_impedance_switch_equipment import MatrixImpedanceSwitchEquipment - +from gdm.distribution.enums import LineType class MatrixImpedanceSwitchEquipmentMapper(CymeMapper): def __init__(self, system): @@ -19,6 +19,7 @@ def parse(self, row, phases): return MatrixImpedanceSwitchEquipment( name=name, + construction=LineType.OVERHEAD, r_matrix=r_matrix, x_matrix=x_matrix, c_matrix=c_matrix, @@ -30,9 +31,9 @@ def map_name(self, row): def map_r_matrix(self, phases): default_matrix = [ - [0.08820, 0.0312137, 0.0306264], - [0.0312137, 0.0901946, 0.0316143], - [0.0306264, 0.0316143, 0.0889665], + [0.0, 0.0, 0.0], + [0.0, 0.0, 0.0], + [0.0, 0.0, 0.0], ] matrix = [row[:len(phases)] for row in default_matrix[:len(phases)]] return ResistancePULength( @@ -42,9 +43,9 @@ def map_r_matrix(self, phases): def map_x_matrix(self, phases): default_matrix = [ - [0.20744, 0.0935314, 0.0760312], - [0.0935314, 0.200783, 0.0855879], - [0.0760312, 0.0855879, 0.204877], + [0.0, 0.0, 0.0], + [0.0, 0.0, 0.0], + [0.0, 0.0, 0.0], ] matrix = [row[:len(phases)] for row in default_matrix[:len(phases)]] return ReactancePULength( @@ -54,9 +55,9 @@ def map_x_matrix(self, phases): def map_c_matrix(self, phases): default_matrix = [ - [2.90301, -0.679335, -0.22313], - [-0.679335, 3.15896, -0.481416], - [-0.22313, -0.481416, 2.8965], + [0.0, 0.0, 0.0], + [0.0, 0.0, 0.0], + [0.0, 0.0, 0.0], ] matrix = [row[:len(phases)] for row in default_matrix[:len(phases)]] diff --git a/src/ditto/readers/cyme/equipment/phase_voltagesource_equipment.py b/src/ditto/readers/cyme/equipment/phase_voltagesource_equipment.py new file mode 100644 index 0000000..0a6695d --- /dev/null +++ b/src/ditto/readers/cyme/equipment/phase_voltagesource_equipment.py @@ -0,0 +1,26 @@ +from ditto.readers.cyme.cyme_mapper import CymeMapper +from gdm.distribution.equipment.phase_voltagesource_equipment import PhaseVoltageSourceEquipment +from gdm.quantities import Angle, Reactance, Resistance +from gdm.distribution.enums import VoltageTypes + + +class PhaseVoltageSourceEquipmentMapper(CymeMapper): + def __init__(self, system): + super().__init__(system) + + def parse(self, bus, feeder_voltage): + sources = [] + num_phases = len(bus.phases) + for i in range(num_phases): + source = PhaseVoltageSourceEquipment.model_construct( + name=f"phase-source-{i+1}", + r0=Resistance(0.001, "ohm"), + r1=Resistance(0.001, "ohm"), + x0=Reactance(0.001, "ohm"), + x1=Reactance(0.001, "ohm"), + voltage=feeder_voltage / 1.732 if num_phases == 3 else feeder_voltage, + voltage_type=VoltageTypes.LINE_TO_GROUND, + angle=Angle(i * (360.0 / num_phases), "degree"), + ) + sources.append(source) + return sources \ No newline at end of file diff --git a/src/ditto/readers/cyme/reader.py b/src/ditto/readers/cyme/reader.py index 7fb3497..59efa98 100644 --- a/src/ditto/readers/cyme/reader.py +++ b/src/ditto/readers/cyme/reader.py @@ -6,12 +6,25 @@ from ditto.readers.cyme.utils import read_cyme_data import ditto.readers.cyme as cyme_mapper from loguru import logger +from pydantic import ValidationError +from rich.console import Console +from infrasys import Component +from rich.table import Table +from functools import partial + + +from gdm.distribution.components.distribution_bus import DistributionBus +from gdm.distribution.components import DistributionVoltageSource +from gdm.distribution.components.distribution_transformer import DistributionTransformer +from gdm.quantities import Voltage +from gdm.distribution.enums import VoltageTypes class Reader(AbstractReader): # Order of components is important component_types = [ "DistributionBus", + "DistributionVoltageSource", "MatrixImpedanceRecloser", "MatrixImpedanceSwitch", "MatrixImpedanceFuse", @@ -22,16 +35,18 @@ class Reader(AbstractReader): "GeometryBranchByPhaseEquipment", "GeometryBranch", "GeometryBranchByPhase", - "DistributionTransformerEquipment", "DistributionTransformerByPhase", - "DistributionTransformer" + "DistributionTransformer", + "MatrixImpedanceBranch", ] - def __init__(self, network_file, equipment_file, load_file): + validation_errors = [] + + def __init__(self, network_file, equipment_file, load_file, feeder, load_model_id = None): self.system = DistributionSystem(auto_add_composed_components=True) - self.read(network_file, equipment_file, load_file) + self.read(network_file, equipment_file, load_file, feeder, load_model_id) - def read(self, network_file, equipment_file, load_file): + def read(self, network_file, equipment_file, load_file, feeder, load_model_id = None): # Section data read separately as it links to other tables section_id_sections = {} @@ -50,8 +65,9 @@ def read(self, network_file, equipment_file, load_file): node_feeder_map = {} feeder_voltage_map = {} + used_sections = set() - section_data = read_cyme_data(network_file,"SECTION", node_feeder_map, feeder_voltage_map, parse_feeders=True) + section_data = read_cyme_data(network_file,"SECTION", node_feeder_map=node_feeder_map, feeder_voltage_map=feeder_voltage_map, parse_feeders=True) section_id_sections = section_data.set_index("SectionID").to_dict(orient="index") from_node_sections = section_data.groupby("FromNodeID").apply(lambda df: df.to_dict(orient="records")).to_dict() @@ -76,81 +92,34 @@ def read(self, network_file, equipment_file, load_file): data = read_cyme_data(equipment_file, cyme_section) elif cyme_file == "Load": data = read_cyme_data(load_file, cyme_section) + if load_model_id is not None: + data = data[data['LoadModelID'] == load_model_id] + logger.info(f"Filtered Load data by LoadModelID: {load_model_id}") + else: + if len(data['LoadModelID'].unique()) > 1: + raise ValueError(f"Multiple LoadModelIDs found in load data: {data['LoadModelID'].unique()}. Please specify load_model_id") else: raise ValueError(f"Unknown CYME file {cyme_file}") - - args = [] - mapper_name = component_type + "Mapper" - if mapper_name == "DistributionCapacitorMapper": - - equipment_data = read_cyme_data(equipment_file, "SHUNT CAPACITOR") - equipment_data.index = equipment_data['ID'] - args = [section_id_sections, equipment_data] - - elif mapper_name == "DistributionBusMapper": - args = [from_node_sections, to_node_sections, node_feeder_map, feeder_voltage_map] - - elif mapper_name == "DistributionLoadMapper": - - equipment_data = read_cyme_data(load_file, "LOADS") - equipment_data.index = equipment_data['DeviceNumber'] - args = [section_id_sections, equipment_data] - - elif mapper_name == "GeometryBranchMapper": - args = [section_id_sections] - - elif mapper_name == "GeometryBranchByPhaseMapper": - args = [section_id_sections] - - elif mapper_name == "BareConductorEquipmentMapper": - args = [] - - elif mapper_name == "GeometryBranchEquipmentMapper": - args = [equipment_file] - - elif mapper_name == "MatrixImpedanceSwitchMapper": - equipment_data = read_cyme_data(equipment_file, "SWITCH") - equipment_data.index = equipment_data['ID'] - args = [section_id_sections, equipment_data] - - elif mapper_name == "MatrixImpedanceFuseMapper": - equipment_data = read_cyme_data(equipment_file, "FUSE") - equipment_data.index = equipment_data['ID'] - args = [section_id_sections, equipment_data] - - elif mapper_name == "MatrixImpedanceRecloserMapper": - equipment_data = read_cyme_data(equipment_file, "RECLOSER") - equipment_data.index = equipment_data['ID'] - args = [section_id_sections, equipment_data] - - elif mapper_name == "GeometryBranchByPhaseEquipmentMapper": - spacing_ids = read_cyme_data(equipment_file,"SPACING TABLE FOR LINE") - spacing_ids.index = spacing_ids['ID'] - args = [spacing_ids] - - elif mapper_name == "DistributionTransformerEquipmentMapper": - transformer_network_data = read_cyme_data(network_file, 'TRANSFORMER SETTING') - transformer_map = {} - for idx, row in transformer_network_data.iterrows(): - transformer_type = row['EqID'] - transformer_map[transformer_type] = row - - byphase_transformer_network_data = read_cyme_data(network_file, "TRANSFORMER BYPHASE SETTING") - for idx, row in byphase_transformer_network_data.iterrows(): - for phase in ['1', '2', '3']: - transformer_type = row['PhaseTransformerID' + phase] - if transformer_type is not None and transformer_type != '': - transformer_map[transformer_type] = row - args = [transformer_map] - - elif mapper_name == "DistributionTransformerMapper" or mapper_name == "DistributionTransformerByPhaseMapper": - transformer_equipment_data = read_cyme_data(equipment_file, "TRANSFORMER") - transformer_map = {} - for idx, row in transformer_equipment_data.iterrows(): - transformer_type = row['ID'] - transformer_map[transformer_type] = row - args = [section_id_sections, transformer_map] + argument_handler = { + "DistributionCapacitorMapper": lambda: [section_id_sections, read_cyme_data(equipment_file, "SHUNT CAPACITOR", index_col='ID')], + "DistributionBusMapper": lambda: [from_node_sections, to_node_sections, node_feeder_map, feeder_voltage_map], + "DistributionVoltageSourceMapper": lambda: [feeder_voltage_map], + "DistributionLoadMapper": lambda: [section_id_sections, read_cyme_data(load_file, "LOADS", index_col='DeviceNumber')], + "GeometryBranchMapper": lambda: [used_sections, section_id_sections], + "GeometryBranchByPhaseMapper": lambda: [used_sections, section_id_sections], + "BareConductorEquipmentMapper": lambda: [], + "GeometryBranchEquipmentMapper": lambda: [equipment_file], + "MatrixImpedanceSwitchMapper": lambda: [used_sections, section_id_sections, read_cyme_data(equipment_file, "SWITCH", index_col='ID')], + "MatrixImpedanceFuseMapper": lambda: [used_sections, section_id_sections, read_cyme_data(equipment_file, "FUSE", index_col='ID')], + "MatrixImpedanceRecloserMapper": lambda: [used_sections, section_id_sections, read_cyme_data(equipment_file, "RECLOSER", index_col='ID')], + "GeometryBranchByPhaseEquipmentMapper": lambda: [read_cyme_data(equipment_file,"SPACING TABLE FOR LINE", index_col='ID')], + "DistributionTransformerMapper": lambda: [used_sections, section_id_sections, read_cyme_data(equipment_file, "TRANSFORMER", index_col='ID').to_dict("index")], + "DistributionTransformerByPhaseMapper": lambda: [used_sections, section_id_sections, read_cyme_data(equipment_file, "TRANSFORMER", index_col='ID').to_dict("index")], + "MatrixImpedanceBranchMapper": lambda: [used_sections, section_id_sections], + } + + args = argument_handler.get(mapper_name, lambda: [])() def parse_row(row): model_entry = mapper.parse(row, *args) @@ -159,7 +128,148 @@ def parse_row(row): components = data.apply(parse_row, axis=1) components = [c for c in components if c is not None] components = [item for c in components for item in (c if isinstance(c, list) else [c])] + self.system.add_components(*components) + self.system = self.build_feeder(feeder) + + for component_type in self.system.get_component_types(): + components = self.system.get_components(component_type) + self._add_components(components) + + self._validate_model() + + def _add_components(self, components: list[Component]): + """Internal method to add components to the system.""" + + if components: + for component in components: + try: + component.__class__.model_validate(component.model_dump()) + except ValidationError as e: + for error in e.errors(): + self.validation_errors.append( + [ + component.name, + component.__class__.__name__, + error["loc"][0] if error["loc"] else "On model validation", + error["type"], + error["msg"], + ] + ) + + self.system.add_components(*components) + + def _validate_model(self): + if self.validation_errors: + error_table = Table(title="Validation warning summary") + error_table.add_column("Model", justify="right", style="cyan", no_wrap=True) + error_table.add_column("Type", style="green") + error_table.add_column("Field", justify="right", style="bright_magenta") + error_table.add_column("Error", style="bright_red") + error_table.add_column("Message", justify="right", style="turquoise2") + + for row in self.validation_errors: + print(row) + error_table.add_row(*row) + + console = Console() + console.print(error_table) + raise Exception( + "Validations errors occured when running the script. See the table above" + ) + def get_system(self) -> DistributionSystem: return self.system + + + + def _get_bus_connected_components( + self, type_lists, bus_name, component_type + ): + if "bus" in component_type.model_fields: + return list( + filter( + lambda x: x.bus.name == bus_name, + type_lists[component_type], + ) + ) + elif "buses" in component_type.model_fields: + return list( + filter( + lambda x: bus_name in [bus.name for bus in x.buses], + type_lists[component_type], + ) + ) + + def build_feeder(self, feeder_name): + + bus_queue = [] + + type_lists = {} + for component_type in self.system.get_component_types(): + type_lists[component_type] = list(self.system.get_components(component_type, filter_func=partial(filter_feeder, feeder_name=feeder_name))) + + voltage_sources = type_lists.get(DistributionVoltageSource, []) + feeder_dist_sys = DistributionSystem(auto_add_composed_components=True) + for vsource in voltage_sources: + feeder_dist_sys.add_component(vsource) + bus_queue.append(vsource.bus.name) + + while bus_queue: + current_bus_name = bus_queue.pop(0) + current_bus = self.system.get_component(DistributionBus, name=current_bus_name) + current_voltage = current_bus.rated_voltage + try: + feeder_dist_sys.add_component(current_bus) + except: + pass + for component_type in self.system.get_component_types(): + conn_objs = self._get_bus_connected_components(type_lists, current_bus.name, component_type) + if conn_objs: + if conn_objs != []: + for obj in conn_objs: + if hasattr(obj, 'buses'): + for bus in obj.buses: + if (bus.name != current_bus.name): + if component_type == DistributionTransformer: + for i, winding in enumerate(obj.equipment.windings): + voltage = winding.rated_voltage + voltage_type = winding.voltage_type + + if i > 0: + if voltage < current_voltage: + bus.voltage_type = voltage_type + bus.rated_voltage = voltage + + if (not feeder_dist_sys.has_component(bus)) and (bus.name not in bus_queue): + bus_queue.append(bus.name) + else: + bus.rated_voltage = current_voltage + if (not feeder_dist_sys.has_component(bus)) and (bus.name not in bus_queue): + bus_queue.append(bus.name) + elif hasattr(obj, 'bus'): + if (obj.bus.name not in bus_queue) and (not feeder_dist_sys.has_component(obj.bus)): + bus_queue.append(obj.bus.name) + try: + feeder_dist_sys.add_component(obj) + except: + pass + return feeder_dist_sys + + +def filter_feeder(object, feeder_name=None): + if hasattr(object, 'bus'): + if not hasattr(object.bus.feeder, "name"): + return False + if object.bus.feeder.name == feeder_name: + return True + return False + + elif hasattr(object, 'buses'): + for bus in object.buses: + if not hasattr(bus.feeder, "name"): + return False + if bus.feeder.name == feeder_name: + return True + return False \ No newline at end of file diff --git a/src/ditto/readers/opendss/common.py b/src/ditto/readers/opendss/common.py index 06a070d..4e59320 100644 --- a/src/ditto/readers/opendss/common.py +++ b/src/ditto/readers/opendss/common.py @@ -2,7 +2,8 @@ from typing import Any import ast -from gdm import Phase, DistributionVoltageSource +from gdm.distribution.enums import Phase +from gdm.distribution.components import DistributionVoltageSource from infrasys import Component, System import opendssdirect as odd From 0069b09445e81d55907f960bf16918cbcb38e0a1 Mon Sep 17 00:00:00 2001 From: "Zink, Zephyr" Date: Mon, 6 Oct 2025 13:33:11 -0600 Subject: [PATCH 18/50] neutral phasing, matrix defaults --- .../components/distribution_voltage_source.py | 2 +- .../cyme/components/geometry_branch.py | 66 +++--- .../components/matrix_impedance_branch.py | 28 ++- .../distribution_transformer_equipment.py | 4 +- .../equipment/geometry_branch_equipment.py | 214 +++++++++--------- .../matrix_impedance_fuse_equipment.py | 12 +- .../matrix_impedance_recloser_equipment.py | 12 +- .../matrix_impedance_switch_equipment.py | 12 +- src/ditto/readers/cyme/reader.py | 6 +- src/ditto/readers/cyme/utils.py | 7 +- 10 files changed, 194 insertions(+), 169 deletions(-) diff --git a/src/ditto/readers/cyme/components/distribution_voltage_source.py b/src/ditto/readers/cyme/components/distribution_voltage_source.py index 4edad23..a46575d 100644 --- a/src/ditto/readers/cyme/components/distribution_voltage_source.py +++ b/src/ditto/readers/cyme/components/distribution_voltage_source.py @@ -26,7 +26,7 @@ def parse(self, row, feeder_voltage_map): if feeder_voltage is None or feeder_voltage == '': return None - phases = bus.phases + phases = [phs for phs in bus.phases] equipment = self.map_equipment(bus, feeder_id, feeder_voltage) return DistributionVoltageSource.model_construct(name=name, diff --git a/src/ditto/readers/cyme/components/geometry_branch.py b/src/ditto/readers/cyme/components/geometry_branch.py index 52956c0..8aae20b 100644 --- a/src/ditto/readers/cyme/components/geometry_branch.py +++ b/src/ditto/readers/cyme/components/geometry_branch.py @@ -18,19 +18,15 @@ def parse(self, row, used_sections, section_id_sections): name = self.map_name(row) buses = self.map_buses(row,section_id_sections) length = self.map_length(row) - phases = self.map_phases(row, section_id_sections) equipment = self.map_equipment(row) - try: - used_sections.add(name) - return GeometryBranch.model_construct(name=name, - buses=buses, - length=length, - phases=phases, - equipment=equipment) - except Exception as e: - print(f"Error creating GeometryBranch {name}: {e}") - print(buses) - return None + phases = self.map_phases(row, section_id_sections, equipment, buses) + + used_sections.add(name) + return GeometryBranch.model_construct(name=name, + buses=buses, + length=length, + phases=phases, + equipment=equipment) def map_name(self, row): name = row['SectionID'] @@ -50,7 +46,7 @@ def map_length(self, row): length = Distance(float(row['Length']),'mile').to('km') return length - def map_phases(self, row, section_id_sections): + def map_phases(self, row, section_id_sections, equipment, buses): section_id = str(row['SectionID']) section = section_id_sections[section_id] phase = section['Phase'] @@ -61,7 +57,17 @@ def map_phases(self, row, section_id_sections): phases.append(Phase.B) if 'C' in phase: phases.append(Phase.C) - return phases + + if len(phases) == len(equipment.conductors): + return phases + elif len(phase) == len(equipment.conductors) - 1: + phases.append(Phase.N) + for bus in buses: + if bus.phases is not None and Phase.N not in bus.phases: + bus.phases.append(Phase.N) + return phases + else: + raise ValueError(f"Number of phases {len(phases)} does not match number of conductors {len(equipment.conductors)} for line {row['SectionID']}") def map_equipment(self, row): line_id = row['LineCableID'] @@ -79,19 +85,16 @@ def parse(self, row, used_sections, section_id_sections): name = self.map_name(row) buses = self.map_buses(row,section_id_sections) length = self.map_length(row) - phases = self.map_phases(row, section_id_sections) equipment = self.map_equipment(row) - try: - used_sections.add(name) - return GeometryBranch.model_construct(name=name, - buses=buses, - length=length, - phases=phases, - equipment=equipment) - except Exception as e: - print(f"Error creating GeometryBranch {name}: {e}") - print(buses) - return None + phases = self.map_phases(row, section_id_sections, equipment, buses) + + used_sections.add(name) + return GeometryBranch.model_construct(name=name, + buses=buses, + length=length, + phases=phases, + equipment=equipment) + def map_name(self, row): name = row['SectionID'] @@ -111,7 +114,7 @@ def map_length(self, row): length = Distance(float(row['Length']),'mile').to('km') return length - def map_phases(self, row, section_id_sections): + def map_phases(self, row, section_id_sections, equipment, buses): section_id = str(row['SectionID']) section = section_id_sections[section_id] phase = section['Phase'] @@ -122,6 +125,15 @@ def map_phases(self, row, section_id_sections): phases.append(Phase.B) if 'C' in phase: phases.append(Phase.C) + + if len(phases) == len(equipment.conductors): + return phases + elif len(phase) == len(equipment.conductors) - 1: + phases.append(Phase.N) + for bus in buses: + if bus.phases is not None and Phase.N not in bus.phases: + bus.phases.append(Phase.N) + return phases return phases def map_equipment(self, row): diff --git a/src/ditto/readers/cyme/components/matrix_impedance_branch.py b/src/ditto/readers/cyme/components/matrix_impedance_branch.py index ac77128..b8ef5c7 100644 --- a/src/ditto/readers/cyme/components/matrix_impedance_branch.py +++ b/src/ditto/readers/cyme/components/matrix_impedance_branch.py @@ -69,22 +69,32 @@ def map_phases(self, row, section_id_sections): return phases def map_equipment(self, row, phases): - default_matrix = [ - [0.0, 0.0, 0.0], - [0.0, 0.0, 0.0], - [0.0, 0.0, 0.0], - ] - matrix = [row[:len(phases)] for row in default_matrix[:len(phases)]] + + r = [ + [1e-6, 0.0, 0.0], + [0.0, 1e-6, 0.0], + [0.0, 0.0, 1e-6], + ] r_matrix = ResistancePULength( - matrix, + [row[:len(phases)] for row in r[:len(phases)]], "ohm/mi", ) + x = [ + [1e-4, 0.0, 0.0], + [0.0, 1e-4, 0.0], + [0.0, 0.0, 1e-4], + ] x_matrix = ReactancePULength( - matrix, + [row[:len(phases)] for row in x[:len(phases)]], "ohm/mi", ) + c = [ + [0.0, 0.0, 0.0], + [0.0, 0.0, 0.0], + [0.0, 0.0, 0.0], + ] c_matrix = CapacitancePULength( - matrix, + [row[:len(phases)] for row in c[:len(phases)]], "nanofarad/mi", ) ampacity = Current(600.0, "A") diff --git a/src/ditto/readers/cyme/equipment/distribution_transformer_equipment.py b/src/ditto/readers/cyme/equipment/distribution_transformer_equipment.py index 0cb7313..9d9d362 100644 --- a/src/ditto/readers/cyme/equipment/distribution_transformer_equipment.py +++ b/src/ditto/readers/cyme/equipment/distribution_transformer_equipment.py @@ -23,7 +23,7 @@ def parse(self, row, network_row): winding_reactances = self.map_winding_reactances(row, is_center_tapped) coupling_sequences = self.map_coupling(row, is_center_tapped) - return DistributionTransformerEquipment(name=name, + return DistributionTransformerEquipment.model_construct(name=name, pct_no_load_loss=pct_no_load_loss, pct_full_load_loss=pct_full_load_loss, windings=windings, @@ -166,7 +166,7 @@ def parse(self, row, network_row, winding_number): total_taps = self.map_total_taps(row) min_tap_pu = self.min_tap_pu(row) max_tap_pu = self.max_tap_pu(row) - return WindingEquipment(name=name, + return WindingEquipment.model_construct(name=name, resistance=resistance, is_grounded=is_grounded, rated_voltage=rated_voltage, diff --git a/src/ditto/readers/cyme/equipment/geometry_branch_equipment.py b/src/ditto/readers/cyme/equipment/geometry_branch_equipment.py index cee1ce7..9161f7e 100644 --- a/src/ditto/readers/cyme/equipment/geometry_branch_equipment.py +++ b/src/ditto/readers/cyme/equipment/geometry_branch_equipment.py @@ -4,7 +4,7 @@ from gdm.distribution.equipment.geometry_branch_equipment import GeometryBranchEquipment from gdm.distribution.equipment.bare_conductor_equipment import BareConductorEquipment from gdm.distribution.equipment.concentric_cable_equipment import ConcentricCableEquipment - +from loguru import logger class GeometryBranchEquipmentMapper(CymeMapper): def __init__(self, system): @@ -13,13 +13,13 @@ def __init__(self, system): cyme_file = 'Equipment' cyme_section = 'LINE' - def parse(self, row, equipment_file): + def parse(self, row, spacing_ids): name = self.map_name(row) - conductors = self.map_conductors(row, equipment_file) - horizontal_positions = self.map_horizontal_positions(row, equipment_file) - vertical_positions = self.map_vertical_positions(row, equipment_file) + conductors = self.map_conductors(row, spacing_ids) + horizontal_positions = self.map_horizontal_positions(row, spacing_ids) + vertical_positions = self.map_vertical_positions(row, spacing_ids) - return GeometryBranchEquipment(name=name, + return GeometryBranchEquipment.model_construct(name=name, conductors=conductors, horizontal_positions=horizontal_positions, vertical_positions=vertical_positions) @@ -28,59 +28,56 @@ def map_name(self, row): name = row['ID'] return name - def map_vertical_positions(self, row, equipment_file): + def map_vertical_positions(self, row, spacing_ids): spacing_id = row['SpacingID'] - spacing_ids = read_cyme_data(equipment_file,"SPACING TABLE FOR LINE") - for idx, row in spacing_ids.iterrows(): - if row['ID'] == spacing_id: - vertical_positions = [] - spacing = row - cond1_y = spacing['PosOfCond1_Y'] - cond2_y = spacing['PosOfCond2_Y'] - cond3_y = spacing['PosOfCond3_Y'] - neutral_y = spacing['PosOfNeutralCond_Y'] - if cond1_y != "": - y1 = float(cond1_y) - vertical_positions.append(y1) - if cond2_y != "": - y2 = float(cond2_y) - vertical_positions.append(y2) - if cond3_y != "": - y3 = float(cond3_y) - vertical_positions.append(y3) - if neutral_y != "": - y_n = float(neutral_y) - vertical_positions.append(y_n) - return Distance(vertical_positions, 'feet').to('m') + spacing = spacing_ids.loc[spacing_id] + + if not spacing.empty: + vertical_positions = [] + cond1_y = spacing['PosOfCond1_Y'] + cond2_y = spacing['PosOfCond2_Y'] + cond3_y = spacing['PosOfCond3_Y'] + neutral_y = spacing['PosOfNeutralCond_Y'] + if cond1_y != "": + y1 = float(cond1_y) + vertical_positions.append(y1) + if cond2_y != "": + y2 = float(cond2_y) + vertical_positions.append(y2) + if cond3_y != "": + y3 = float(cond3_y) + vertical_positions.append(y3) + if neutral_y != "": + y_n = float(neutral_y) + vertical_positions.append(y_n) + return Distance(vertical_positions, 'feet').to('m') return None - def map_horizontal_positions(self, row, equipment_file): + def map_horizontal_positions(self, row, spacing_ids): spacing_id = row['SpacingID'] - spacing_ids = read_cyme_data(equipment_file,"SPACING TABLE FOR LINE") - for idx, row in spacing_ids.iterrows(): - if row['ID'] == spacing_id: - spacing = row - horizontal_positions = [] - cond1_x = spacing['PosOfCond1_X'] - cond2_x = spacing['PosOfCond2_X'] - cond3_x = spacing['PosOfCond3_X'] - neutral_x = spacing['PosOfNeutralCond_X'] - if cond1_x != "": - x1 = float(cond1_x) - horizontal_positions.append(x1) - if cond2_x != "": - x2 = float(cond2_x) - horizontal_positions.append(x2) - if cond3_x != "": - x3 = float(cond3_x) - horizontal_positions.append(x3) - if neutral_x != "": - x_n = float(neutral_x) - horizontal_positions.append(x_n) - return Distance(horizontal_positions, 'feet').to('m') + spacing = spacing_ids.loc[spacing_id] + if not spacing.empty: + horizontal_positions = [] + cond1_x = spacing['PosOfCond1_X'] + cond2_x = spacing['PosOfCond2_X'] + cond3_x = spacing['PosOfCond3_X'] + neutral_x = spacing['PosOfNeutralCond_X'] + if cond1_x != "": + x1 = float(cond1_x) + horizontal_positions.append(x1) + if cond2_x != "": + x2 = float(cond2_x) + horizontal_positions.append(x2) + if cond3_x != "": + x3 = float(cond3_x) + horizontal_positions.append(x3) + if neutral_x != "": + x_n = float(neutral_x) + horizontal_positions.append(x_n) + return Distance(horizontal_positions, 'feet').to('m') return None - def map_conductors(self,row, equipment_file): + def map_conductors(self,row, spacing_ids): phase_conductor_name = row['PhaseCondID'] neutral_conductor_name = row['NeutralCondID'] try: @@ -93,24 +90,22 @@ def map_conductors(self,row, equipment_file): neutral_conductor = self.system.get_component(component_type=BareConductorEquipment, name="Default") spacing_id = row['SpacingID'] - spacing_ids = read_cyme_data(equipment_file,"SPACING TABLE FOR LINE") - for idx, row in spacing_ids.iterrows(): - if row['ID'] == spacing_id: - spacing = row - conductors = [] - cond1 = spacing['PosOfCond1_X'] - cond2 = spacing['PosOfCond2_X'] - cond3 = spacing['PosOfCond3_X'] - neutral = spacing['PosOfNeutralCond_X'] - if cond1 != "": - conductors.append(phase_conductor) - if cond2 != "": - conductors.append(phase_conductor) - if cond3 != "": - conductors.append(phase_conductor) - if neutral != "": - conductors.append(neutral_conductor) - return conductors + spacing = spacing_ids.loc[spacing_id] + if not spacing.empty: + conductors = [] + cond1 = spacing['PosOfCond1_X'] + cond2 = spacing['PosOfCond2_X'] + cond3 = spacing['PosOfCond3_X'] + neutral = spacing['PosOfNeutralCond_X'] + if cond1 != "": + conductors.append(phase_conductor) + if cond2 != "": + conductors.append(phase_conductor) + if cond3 != "": + conductors.append(phase_conductor) + if neutral != "": + conductors.append(neutral_conductor) + return conductors return None class ConcentricCableEquipmentMapper(CymeMapper): @@ -134,7 +129,7 @@ def parse(self, row): strand_ac_resistance = self.map_strand_ac_resistance(row) num_neutral_strands = self.map_num_neutral_strands(row) rated_voltage = self.map_rated_voltage(row) - return ConcentricCableEquipment(name=name, + return ConcentricCableEquipment.model_construct(name=name, strand_diameter=strand_diameter, conductor_diameter=conductor_diameter, cable_diameter=cable_diameter, @@ -190,7 +185,7 @@ def parse(self, row): emergency_ampacity = self.map_emergency_ampacity(row) ac_resistance = self.map_ac_resistance(row) dc_resistance = self.map_dc_resistance(row) - return BareConductorEquipment(name=name, + return BareConductorEquipment.model_construct(name=name, conductor_diameter=conductor_diameter, conductor_gmr=conductor_gmr, ampacity=ampacity, @@ -247,7 +242,7 @@ def parse(self, row, spacing_ids): horizontal_positions = self.map_horizontal_positions(row, spacing_ids) vertical_positions = self.map_vertical_positions(row, spacing_ids) - return GeometryBranchEquipment(name=name, + return GeometryBranchEquipment.model_construct(name=name, conductors=conductors, horizontal_positions=horizontal_positions, vertical_positions=vertical_positions) @@ -257,51 +252,58 @@ def map_name(self, row): return name def map_vertical_positions(self, row, spacing_ids): + phase_A_conductor_name = row['CondID_A'] + phase_B_conductor_name = row['CondID_B'] + phase_C_conductor_name = row['CondID_C'] + neutral_conductor_name = row['CondID_N1'] + spacing_id = row['SpacingID'] - row = spacing_ids.loc[spacing_id] + spacing = spacing_ids.loc[spacing_id] - if not row.empty: + if not spacing.empty: vertical_positions = [] - spacing = row cond1_y = spacing['PosOfCond1_Y'] cond2_y = spacing['PosOfCond2_Y'] cond3_y = spacing['PosOfCond3_Y'] neutral_y = spacing['PosOfNeutralCond_Y'] - if cond1_y != "": + if cond1_y != "" and phase_A_conductor_name != "NONE": y1 = float(cond1_y) vertical_positions.append(y1) - if cond2_y != "": + if cond2_y != "" and phase_B_conductor_name != "NONE": y2 = float(cond2_y) vertical_positions.append(y2) - if cond3_y != "": + if cond3_y != "" and phase_C_conductor_name != "NONE": y3 = float(cond3_y) vertical_positions.append(y3) - if neutral_y != "": + if neutral_y != "" and neutral_conductor_name != "NONE": y_n = float(neutral_y) vertical_positions.append(y_n) return Distance(vertical_positions, 'feet').to('m') return None def map_horizontal_positions(self, row, spacing_ids): + phase_A_conductor_name = row['CondID_A'] + phase_B_conductor_name = row['CondID_B'] + phase_C_conductor_name = row['CondID_C'] + neutral_conductor_name = row['CondID_N1'] spacing_id = row['SpacingID'] - row = spacing_ids.loc[spacing_id] - if not row.empty: - spacing = row + spacing = spacing_ids.loc[spacing_id] + if not spacing.empty: horizontal_positions = [] cond1_x = spacing['PosOfCond1_X'] cond2_x = spacing['PosOfCond2_X'] cond3_x = spacing['PosOfCond3_X'] neutral_x = spacing['PosOfNeutralCond_X'] - if cond1_x != "": + if cond1_x != "" and phase_A_conductor_name != "NONE": x1 = float(cond1_x) horizontal_positions.append(x1) - if cond2_x != "": + if cond2_x != "" and phase_B_conductor_name != "NONE": x2 = float(cond2_x) horizontal_positions.append(x2) - if cond3_x != "": + if cond3_x != "" and phase_C_conductor_name != "NONE": x3 = float(cond3_x) horizontal_positions.append(x3) - if neutral_x != "": + if neutral_x != "" and neutral_conductor_name != "NONE": x_n = float(neutral_x) horizontal_positions.append(x_n) return Distance(horizontal_positions, 'feet').to('m') @@ -312,40 +314,38 @@ def map_conductors(self,row, spacing_ids): phase_B_conductor_name = row['CondID_B'] phase_C_conductor_name = row['CondID_C'] neutral_conductor_name = row['CondID_N1'] - try: + + phase_A_conductor = None + phase_B_conductor = None + phase_C_conductor = None + neutral_conductor = None + + if phase_A_conductor_name != "NONE": phase_A_conductor = self.system.get_component(component_type=BareConductorEquipment, name=phase_A_conductor_name) - except Exception as e: - phase_A_conductor = self.system.get_component(component_type=BareConductorEquipment, name="Default") - try: + if phase_B_conductor_name != "NONE": phase_B_conductor = self.system.get_component(component_type=BareConductorEquipment, name=phase_B_conductor_name) - except Exception as e: - phase_B_conductor = self.system.get_component(component_type=BareConductorEquipment, name="Default") - try: + if phase_C_conductor_name != "NONE": phase_C_conductor = self.system.get_component(component_type=BareConductorEquipment, name=phase_C_conductor_name) - except Exception as e: - phase_C_conductor = self.system.get_component(component_type=BareConductorEquipment, name="Default") - try: + if neutral_conductor_name != "NONE": neutral_conductor = self.system.get_component(component_type=BareConductorEquipment, name=neutral_conductor_name) - except Exception as e: - neutral_conductor = self.system.get_component(component_type=BareConductorEquipment, name="Default") spacing_id = row['SpacingID'] - row = spacing_ids.loc[spacing_id] - if not row.empty: - spacing = row + spacing = spacing_ids.loc[spacing_id] + if not spacing.empty: conductors = [] cond1 = spacing['PosOfCond1_X'] cond2 = spacing['PosOfCond2_X'] cond3 = spacing['PosOfCond3_X'] neutral = spacing['PosOfNeutralCond_X'] - if cond1 != "" and phase_A_conductor != "NONE": + if cond1 != "" and phase_A_conductor is not None: conductors.append(phase_A_conductor) - if cond2 != "" and phase_B_conductor != "NONE": + if cond2 != "" and phase_B_conductor is not None: conductors.append(phase_B_conductor) - if cond3 != "" and phase_C_conductor != "NONE": + if cond3 != "" and phase_C_conductor is not None: conductors.append(phase_C_conductor) - if neutral != "" and neutral_conductor != "NONE": + if neutral != "" and neutral_conductor is not None: conductors.append(neutral_conductor) + return conductors return None \ No newline at end of file diff --git a/src/ditto/readers/cyme/equipment/matrix_impedance_fuse_equipment.py b/src/ditto/readers/cyme/equipment/matrix_impedance_fuse_equipment.py index 36ebad3..7030710 100644 --- a/src/ditto/readers/cyme/equipment/matrix_impedance_fuse_equipment.py +++ b/src/ditto/readers/cyme/equipment/matrix_impedance_fuse_equipment.py @@ -38,9 +38,9 @@ def map_name(self, row): def map_r_matrix(self, phases): default_matrix = [ - [0.0, 0.0, 0.0], - [0.0, 0.0, 0.0], - [0.0, 0.0, 0.0], + [1e-6, 0.0, 0.0], + [0.0, 1e-6, 0.0], + [0.0, 0.0, 1e-6], ] matrix = [row[:len(phases)] for row in default_matrix[:len(phases)]] return ResistancePULength( @@ -50,9 +50,9 @@ def map_r_matrix(self, phases): def map_x_matrix(self, phases): default_matrix = [ - [0.0, 0.0, 0.0], - [0.0, 0.0, 0.0], - [0.0, 0.0, 0.0], + [1e-4, 0.0, 0.0], + [0.0, 1e-4, 0.0], + [0.0, 0.0, 1e-4], ] matrix = [row[:len(phases)] for row in default_matrix[:len(phases)]] return ReactancePULength( diff --git a/src/ditto/readers/cyme/equipment/matrix_impedance_recloser_equipment.py b/src/ditto/readers/cyme/equipment/matrix_impedance_recloser_equipment.py index 12f38fd..2ef2180 100644 --- a/src/ditto/readers/cyme/equipment/matrix_impedance_recloser_equipment.py +++ b/src/ditto/readers/cyme/equipment/matrix_impedance_recloser_equipment.py @@ -31,9 +31,9 @@ def map_name(self, row): def map_r_matrix(self, phases): default_matrix = [ - [0.0, 0.0, 0.0], - [0.0, 0.0, 0.0], - [0.0, 0.0, 0.0], + [1e-6, 0.0, 0.0], + [0.0, 1e-6, 0.0], + [0.0, 0.0, 1e-6], ] matrix = [row[:len(phases)] for row in default_matrix[:len(phases)]] return ResistancePULength( @@ -43,9 +43,9 @@ def map_r_matrix(self, phases): def map_x_matrix(self, phases): default_matrix = [ - [0.0, 0.0, 0.0], - [0.0, 0.0, 0.0], - [0.0, 0.0, 0.0], + [1e-4, 0.0, 0.0], + [0.0, 1e-4, 0.0], + [0.0, 0.0, 1e-4], ] matrix = [row[:len(phases)] for row in default_matrix[:len(phases)]] return ReactancePULength( diff --git a/src/ditto/readers/cyme/equipment/matrix_impedance_switch_equipment.py b/src/ditto/readers/cyme/equipment/matrix_impedance_switch_equipment.py index 9743ba2..c55de43 100644 --- a/src/ditto/readers/cyme/equipment/matrix_impedance_switch_equipment.py +++ b/src/ditto/readers/cyme/equipment/matrix_impedance_switch_equipment.py @@ -31,9 +31,9 @@ def map_name(self, row): def map_r_matrix(self, phases): default_matrix = [ - [0.0, 0.0, 0.0], - [0.0, 0.0, 0.0], - [0.0, 0.0, 0.0], + [1e-6, 0.0, 0.0], + [0.0, 1e-6, 0.0], + [0.0, 0.0, 1e-6], ] matrix = [row[:len(phases)] for row in default_matrix[:len(phases)]] return ResistancePULength( @@ -43,9 +43,9 @@ def map_r_matrix(self, phases): def map_x_matrix(self, phases): default_matrix = [ - [0.0, 0.0, 0.0], - [0.0, 0.0, 0.0], - [0.0, 0.0, 0.0], + [1e-4, 0.0, 0.0], + [0.0, 1e-4, 0.0], + [0.0, 0.0, 1e-4], ] matrix = [row[:len(phases)] for row in default_matrix[:len(phases)]] return ReactancePULength( diff --git a/src/ditto/readers/cyme/reader.py b/src/ditto/readers/cyme/reader.py index 59efa98..0e9a1ea 100644 --- a/src/ditto/readers/cyme/reader.py +++ b/src/ditto/readers/cyme/reader.py @@ -109,7 +109,7 @@ def read(self, network_file, equipment_file, load_file, feeder, load_model_id = "GeometryBranchMapper": lambda: [used_sections, section_id_sections], "GeometryBranchByPhaseMapper": lambda: [used_sections, section_id_sections], "BareConductorEquipmentMapper": lambda: [], - "GeometryBranchEquipmentMapper": lambda: [equipment_file], + "GeometryBranchEquipmentMapper": lambda: [read_cyme_data(equipment_file,"SPACING TABLE FOR LINE", index_col='ID')], "MatrixImpedanceSwitchMapper": lambda: [used_sections, section_id_sections, read_cyme_data(equipment_file, "SWITCH", index_col='ID')], "MatrixImpedanceFuseMapper": lambda: [used_sections, section_id_sections, read_cyme_data(equipment_file, "FUSE", index_col='ID')], "MatrixImpedanceRecloserMapper": lambda: [used_sections, section_id_sections, read_cyme_data(equipment_file, "RECLOSER", index_col='ID')], @@ -270,6 +270,6 @@ def filter_feeder(object, feeder_name=None): for bus in object.buses: if not hasattr(bus.feeder, "name"): return False - if bus.feeder.name == feeder_name: - return True + if object.buses[0].feeder.name == feeder_name and object.buses[1].feeder.name == feeder_name: + return True return False \ No newline at end of file diff --git a/src/ditto/readers/cyme/utils.py b/src/ditto/readers/cyme/utils.py index 5d474b9..1a2806f 100644 --- a/src/ditto/readers/cyme/utils.py +++ b/src/ditto/readers/cyme/utils.py @@ -1,7 +1,8 @@ import pandas as pd from gdm.distribution.components.distribution_feeder import DistributionFeeder +from gdm.quantities import Voltage -def read_cyme_data(cyme_file, cyme_section, node_feeder_map = None, feeder_voltage_map = None, parse_feeders=False): +def read_cyme_data(cyme_file, cyme_section, index_col=None, node_feeder_map = None, feeder_voltage_map = None, parse_feeders=False): all_data = [] headers = None with open(cyme_file) as f: @@ -21,7 +22,7 @@ def read_cyme_data(cyme_file, cyme_section, node_feeder_map = None, feeder_volta if parse_feeders: if line.startswith("FEEDER"): feeder_id = line.split(",")[0].split("=")[1].strip() - feeder_voltage_map[feeder_id] = line.split(",")[9].strip() + feeder_voltage_map[feeder_id] = None else: feeder_id = None # For SECTION Feeder headers @@ -46,4 +47,6 @@ def read_cyme_data(cyme_file, cyme_section, node_feeder_map = None, feeder_volta raise Exception(f"Failed to parse line: {line}") data = pd.DataFrame(all_data, columns=headers) + if index_col is not None: + data.set_index(index_col, inplace=True, drop=False) return data From 4bc784b2f7db68d7e0f2e5081ef26f68420029af Mon Sep 17 00:00:00 2001 From: "Zink, Zephyr" Date: Tue, 14 Oct 2025 11:47:59 -0600 Subject: [PATCH 19/50] length unit fixes --- src/ditto/readers/cyme/components/geometry_branch.py | 4 ++-- src/ditto/readers/cyme/components/matrix_impedance_branch.py | 2 +- src/ditto/readers/cyme/components/matrix_impedance_fuse.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ditto/readers/cyme/components/geometry_branch.py b/src/ditto/readers/cyme/components/geometry_branch.py index 8aae20b..9f05ab0 100644 --- a/src/ditto/readers/cyme/components/geometry_branch.py +++ b/src/ditto/readers/cyme/components/geometry_branch.py @@ -43,7 +43,7 @@ def map_buses(self, row, section_id_sections): return [from_bus, to_bus] def map_length(self, row): - length = Distance(float(row['Length']),'mile').to('km') + length = Distance(float(row['Length']),'foot').to('km') return length def map_phases(self, row, section_id_sections, equipment, buses): @@ -111,7 +111,7 @@ def map_buses(self, row, section_id_sections): return [from_bus, to_bus] def map_length(self, row): - length = Distance(float(row['Length']),'mile').to('km') + length = Distance(float(row['Length']),'foot').to('km') return length def map_phases(self, row, section_id_sections, equipment, buses): diff --git a/src/ditto/readers/cyme/components/matrix_impedance_branch.py b/src/ditto/readers/cyme/components/matrix_impedance_branch.py index b8ef5c7..6d70a74 100644 --- a/src/ditto/readers/cyme/components/matrix_impedance_branch.py +++ b/src/ditto/readers/cyme/components/matrix_impedance_branch.py @@ -52,7 +52,7 @@ def map_buses(self, row, section_id_sections): return [from_bus, to_bus] def map_length(self): - length = Distance(float(1.0),'m') + length = Distance(0.001,'kilometer') return length def map_phases(self, row, section_id_sections): diff --git a/src/ditto/readers/cyme/components/matrix_impedance_fuse.py b/src/ditto/readers/cyme/components/matrix_impedance_fuse.py index 745a71e..48a7067 100644 --- a/src/ditto/readers/cyme/components/matrix_impedance_fuse.py +++ b/src/ditto/readers/cyme/components/matrix_impedance_fuse.py @@ -46,7 +46,7 @@ def map_buses(self, row, section_id_sections): return [from_bus, to_bus] def map_length(self, row): - length = Distance(0.001,'km') + length = Distance(0.001,'kilometer') return length def map_phases(self, row, section_id_sections): From 6bdf1f2e651ecc9010089e35189030741d97a1c8 Mon Sep 17 00:00:00 2001 From: tarekelgindy Date: Thu, 16 Oct 2025 15:11:40 -0600 Subject: [PATCH 20/50] adding functionality for cables to be read into matrix_impedance_branch objects in CYME --- src/ditto/readers/cyme/__init__.py | 3 + .../components/matrix_impedance_branch.py | 68 ++++++ .../matrix_impedance_branch_equipment.py | 79 +++++++ src/ditto/readers/cyme/reader.py | 205 ++++++++++-------- 4 files changed, 266 insertions(+), 89 deletions(-) create mode 100644 src/ditto/readers/cyme/components/matrix_impedance_branch.py create mode 100644 src/ditto/readers/cyme/equipment/matrix_impedance_branch_equipment.py diff --git a/src/ditto/readers/cyme/__init__.py b/src/ditto/readers/cyme/__init__.py index 06bb65b..588cc57 100644 --- a/src/ditto/readers/cyme/__init__.py +++ b/src/ditto/readers/cyme/__init__.py @@ -2,8 +2,11 @@ from ditto.readers.cyme.components.distribution_capacitor import DistributionCapacitorMapper from ditto.readers.cyme.components.distribution_load import DistributionLoadMapper from ditto.readers.cyme.equipment.geometry_branch_equipment import BareConductorEquipmentMapper +from ditto.readers.cyme.equipment.geometry_branch_equipment import ConcentricCableEquipmentMapper from ditto.readers.cyme.equipment.geometry_branch_equipment import GeometryBranchEquipmentMapper +from ditto.readers.cyme.equipment.matrix_impedance_branch_equipment import MatrixImpedanceBranchEquipmentMapper from ditto.readers.cyme.equipment.geometry_branch_equipment import GeometryBranchByPhaseEquipmentMapper +from ditto.readers.cyme.components.matrix_impedance_branch import MatrixImpedanceBranchMapper from ditto.readers.cyme.components.geometry_branch import GeometryBranchMapper from ditto.readers.cyme.components.geometry_branch import GeometryBranchByPhaseMapper from ditto.readers.cyme.equipment.distribution_transformer_equipment import DistributionTransformerEquipmentMapper diff --git a/src/ditto/readers/cyme/components/matrix_impedance_branch.py b/src/ditto/readers/cyme/components/matrix_impedance_branch.py new file mode 100644 index 0000000..4699bb0 --- /dev/null +++ b/src/ditto/readers/cyme/components/matrix_impedance_branch.py @@ -0,0 +1,68 @@ +from gdm.quantities import Distance +from ditto.readers.cyme.cyme_mapper import CymeMapper +from gdm.distribution.equipment.matrix_impedance_branch_equipment import MatrixImpedanceBranchEquipment +from gdm.distribution.components.matrix_impedance_branch import MatrixImpedanceBranch +from gdm.distribution.components.distribution_bus import DistributionBus +from gdm.distribution.enums import Phase + + +class MatrixImpedanceBranchMapper(CymeMapper): + def __init__(self, system): + super().__init__(system) + + cyme_file = 'Network' + cyme_section = 'UNDERGROUNDLINE SETTING' + + def parse(self, row, section_id_sections): + name = self.map_name(row) + buses = self.map_buses(row,section_id_sections) + length = self.map_length(row) + phases = self.map_phases(row, section_id_sections) + equipment = self.map_equipment(row, phases) + try: + return MatrixImpedanceBranch(name=name, + buses=buses, + length=length, + phases=phases, + equipment=equipment) + except Exception as e: + print(f"Error creating GeometryBranch {name}: {e}") + print(buses) + return None + + def map_name(self, row): + name = row['SectionID'] + return name + + def map_buses(self, row, section_id_sections): + section_id = str(row['SectionID']) + section = section_id_sections[section_id] + from_bus_name = section['FromNodeID'] + to_bus_name = section['ToNodeID'] + + from_bus = self.system.get_component(component_type=DistributionBus, name=from_bus_name) + to_bus = self.system.get_component(component_type=DistributionBus, name=to_bus_name) + return [from_bus, to_bus] + + def map_length(self, row): + length = Distance(float(row['Length']),'mile').to('km') + return length + + def map_phases(self, row, section_id_sections): + section_id = str(row['SectionID']) + section = section_id_sections[section_id] + phase = section['Phase'] + phases = [] + if 'A' in phase: + phases.append(Phase.A) + if 'B' in phase: + phases.append(Phase.B) + if 'C' in phase: + phases.append(Phase.C) + return phases + + def map_equipment(self, row, phases): + line_id = row['LineCableID'] + equipment_name = f"{line_id}_{len(phases)}" + line = self.system.get_component(component_type=MatrixImpedanceBranchEquipment, name=equipment_name) + return line \ No newline at end of file diff --git a/src/ditto/readers/cyme/equipment/matrix_impedance_branch_equipment.py b/src/ditto/readers/cyme/equipment/matrix_impedance_branch_equipment.py new file mode 100644 index 0000000..1db6335 --- /dev/null +++ b/src/ditto/readers/cyme/equipment/matrix_impedance_branch_equipment.py @@ -0,0 +1,79 @@ +from gdm.quantities import Distance +from ditto.readers.cyme.cyme_mapper import CymeMapper +from gdm.distribution.equipment.matrix_impedance_branch_equipment import MatrixImpedanceBranchEquipment +from gdm.distribution.enums import Phase +import numpy as np + +class MatrixImpedanceBranchEquipmentMapper(CymeMapper): + def __init__(self, system): + super().__init__(system) + + cyme_file = 'Equipment' + cyme_section = 'CABLE' + + def _sequence_impedance_to_phase_impedance_matrix(self, r1, r0, phases=3): + """ + Return the phase resistance matrix given + positive-sequence r1 and zero-sequence r0. + Assumes r2 = r1 (typical for transposed lines/cables). + """ + if phases == 3: + r_s = (r0 + 2*r1) / 3.0 # self term + r_m = (r0 - r1) / 3.0 # mutual term + R = np.array([[r_s, r_m, r_m], + [r_m, r_s, r_m], + [r_m, r_m, r_s]], dtype=float) + elif phases == 2: + r_s = (r0 + 2*r1) / 3.0 # self term + r_m = (r0 - r1) / 3.0 # mutual term + R = np.array([[r_s, r_m], + [r_m, r_s]], dtype=float) + elif phases == 1: + r_s = (r0 + 2*r1) / 3.0 # self term + R = np.array([[r_s]], dtype=float) + return R + + def parse(self, row, phases): + name = self.map_name(row, phases) + r_matrix = self.map_r_matrix(row, phases) + x_matrix = self.map_x_matrix(row, phases) + c_matrix = self.map_c_matrix(row, phases) + ampacity = self.map_ampacity(row) + try: + return MatrixImpedanceBranchEquipment(name=name, + r_matrix=r_matrix, + x_matrix=x_matrix, + c_matrix=c_matrix, + ampacity=ampacity) + except Exception as e: + print(f"Error creating MatrixImpedanceBranchEquipment {name}: {e}") + return None + + def map_name(self, row, phases): + name = f"{row['ID']}_{phases}" + return name + + def map_r_matrix(self, row, phases): + r1 = float(row['R1']) + r0 = float(row['R0']) + matrix = self._sequence_impedance_to_phase_impedance_matrix(r1, r0, phases) + return matrix + + def map_x_matrix(self, row, phases): + x1 = float(row['X1']) + x0 = float(row['X0']) + matrix = self._sequence_impedance_to_phase_impedance_matrix(x1, x0, phases) + return matrix + + def map_c_matrix(self, row, phases): + b1 = float(row['B1']) + b0 = float(row['B0']) + susceptance_matrix = self._sequence_impedance_to_phase_impedance_matrix(b1, b0, phases) + # Convert susceptance to capacitance: C = B / (2 * pi * f) + frequency = 60 # Hz + capacitance_matrix = susceptance_matrix / (2 * np.pi * frequency) + return capacitance_matrix + + def map_ampacity(self, row): + ampacity = float(row['Amps']) + return ampacity \ No newline at end of file diff --git a/src/ditto/readers/cyme/reader.py b/src/ditto/readers/cyme/reader.py index 7fb3497..4e57f94 100644 --- a/src/ditto/readers/cyme/reader.py +++ b/src/ditto/readers/cyme/reader.py @@ -2,6 +2,8 @@ from gdm.distribution.equipment.bare_conductor_equipment import BareConductorEquipment from gdm.distribution.components.base.distribution_component_base import DistributionComponentBase from gdm.distribution.distribution_system import DistributionSystem +from gdm.distribution.components.matrix_impedance_branch import MatrixImpedanceBranch +from gdm.distribution.equipment.matrix_impedance_branch_equipment import MatrixImpedanceBranchEquipment from ditto.readers.reader import AbstractReader from ditto.readers.cyme.utils import read_cyme_data import ditto.readers.cyme as cyme_mapper @@ -18,9 +20,11 @@ class Reader(AbstractReader): "DistributionCapacitor", "DistributionLoad", "BareConductorEquipment", + "MatrixImpedanceBranchEquipment", "GeometryBranchEquipment", "GeometryBranchByPhaseEquipment", "GeometryBranch", + "MatrixImpedanceBranch", "GeometryBranchByPhase", "DistributionTransformerEquipment", "DistributionTransformerByPhase", @@ -68,98 +72,121 @@ def read(self, network_file, equipment_file, load_file): cyme_file = mapper.cyme_file cyme_section = mapper.cyme_section + all_cyme_sections = mapper.cyme_section + if isinstance(all_cyme_sections, str): + all_cyme_sections = [all_cyme_sections] + if not isinstance(all_cyme_sections, list): + raise ValueError(f"cyme_section must be a string or list of strings. Got {type(all_cyme_sections)}") + for cyme_section in all_cyme_sections: + + data = None + if cyme_file == "Network": + data = read_cyme_data(network_file, cyme_section) + elif cyme_file == "Equipment": + data = read_cyme_data(equipment_file, cyme_section) + elif cyme_file == "Load": + data = read_cyme_data(load_file, cyme_section) + else: + raise ValueError(f"Unknown CYME file {cyme_file}") - data = None - if cyme_file == "Network": - data = read_cyme_data(network_file, cyme_section) - elif cyme_file == "Equipment": - data = read_cyme_data(equipment_file, cyme_section) - elif cyme_file == "Load": - data = read_cyme_data(load_file, cyme_section) - else: - raise ValueError(f"Unknown CYME file {cyme_file}") - - args = [] - mapper_name = component_type + "Mapper" - if mapper_name == "DistributionCapacitorMapper": - - equipment_data = read_cyme_data(equipment_file, "SHUNT CAPACITOR") - equipment_data.index = equipment_data['ID'] - args = [section_id_sections, equipment_data] - - elif mapper_name == "DistributionBusMapper": - args = [from_node_sections, to_node_sections, node_feeder_map, feeder_voltage_map] - - elif mapper_name == "DistributionLoadMapper": - - equipment_data = read_cyme_data(load_file, "LOADS") - equipment_data.index = equipment_data['DeviceNumber'] - args = [section_id_sections, equipment_data] - - elif mapper_name == "GeometryBranchMapper": - args = [section_id_sections] - - elif mapper_name == "GeometryBranchByPhaseMapper": - args = [section_id_sections] - - elif mapper_name == "BareConductorEquipmentMapper": args = [] - - elif mapper_name == "GeometryBranchEquipmentMapper": - args = [equipment_file] - - elif mapper_name == "MatrixImpedanceSwitchMapper": - equipment_data = read_cyme_data(equipment_file, "SWITCH") - equipment_data.index = equipment_data['ID'] - args = [section_id_sections, equipment_data] - - elif mapper_name == "MatrixImpedanceFuseMapper": - equipment_data = read_cyme_data(equipment_file, "FUSE") - equipment_data.index = equipment_data['ID'] - args = [section_id_sections, equipment_data] - - elif mapper_name == "MatrixImpedanceRecloserMapper": - equipment_data = read_cyme_data(equipment_file, "RECLOSER") - equipment_data.index = equipment_data['ID'] - args = [section_id_sections, equipment_data] - - elif mapper_name == "GeometryBranchByPhaseEquipmentMapper": - spacing_ids = read_cyme_data(equipment_file,"SPACING TABLE FOR LINE") - spacing_ids.index = spacing_ids['ID'] - args = [spacing_ids] - - elif mapper_name == "DistributionTransformerEquipmentMapper": - transformer_network_data = read_cyme_data(network_file, 'TRANSFORMER SETTING') - transformer_map = {} - for idx, row in transformer_network_data.iterrows(): - transformer_type = row['EqID'] - transformer_map[transformer_type] = row - - byphase_transformer_network_data = read_cyme_data(network_file, "TRANSFORMER BYPHASE SETTING") - for idx, row in byphase_transformer_network_data.iterrows(): - for phase in ['1', '2', '3']: - transformer_type = row['PhaseTransformerID' + phase] - if transformer_type is not None and transformer_type != '': - transformer_map[transformer_type] = row - args = [transformer_map] - - elif mapper_name == "DistributionTransformerMapper" or mapper_name == "DistributionTransformerByPhaseMapper": - transformer_equipment_data = read_cyme_data(equipment_file, "TRANSFORMER") - transformer_map = {} - for idx, row in transformer_equipment_data.iterrows(): - transformer_type = row['ID'] - transformer_map[transformer_type] = row - args = [section_id_sections, transformer_map] - - def parse_row(row): - model_entry = mapper.parse(row, *args) - return model_entry - - components = data.apply(parse_row, axis=1) - components = [c for c in components if c is not None] - components = [item for c in components for item in (c if isinstance(c, list) else [c])] - self.system.add_components(*components) + mapper_name = component_type + "Mapper" + if mapper_name == "DistributionCapacitorMapper": + + equipment_data = read_cyme_data(equipment_file, "SHUNT CAPACITOR") + equipment_data.index = equipment_data['ID'] + args = [section_id_sections, equipment_data] + + elif mapper_name == "DistributionBusMapper": + args = [from_node_sections, to_node_sections, node_feeder_map, feeder_voltage_map] + + elif mapper_name == "DistributionLoadMapper": + + equipment_data = read_cyme_data(load_file, "LOADS") + equipment_data.index = equipment_data['DeviceNumber'] + args = [section_id_sections, equipment_data] + + elif mapper_name == "GeometryBranchMapper": + args = [section_id_sections] + + elif mapper_name == "GeometryBranchByPhaseMapper": + args = [section_id_sections] + + elif mapper_name == "BareConductorEquipmentMapper": + args = [] + + elif mapper_name == "ConcentricCableEquipmentMapper": + args = [equipment_file] + + elif mapper_name == "GeometryBranchEquipmentMapper": + args = [equipment_file] + + elif mapper_name == "MatrixImpedanceBranchEquipmentMapper": + args = [] + + elif mapper_name == "MatrixImpedanceBranchMapper": + args = [section_id_sections] + + elif mapper_name == "MatrixImpedanceSwitchMapper": + equipment_data = read_cyme_data(equipment_file, "SWITCH") + equipment_data.index = equipment_data['ID'] + args = [section_id_sections, equipment_data] + + elif mapper_name == "MatrixImpedanceFuseMapper": + equipment_data = read_cyme_data(equipment_file, "FUSE") + equipment_data.index = equipment_data['ID'] + args = [section_id_sections, equipment_data] + + elif mapper_name == "MatrixImpedanceRecloserMapper": + equipment_data = read_cyme_data(equipment_file, "RECLOSER") + equipment_data.index = equipment_data['ID'] + args = [section_id_sections, equipment_data] + + elif mapper_name == "GeometryBranchByPhaseEquipmentMapper": + spacing_ids = read_cyme_data(equipment_file,"SPACING TABLE FOR LINE") + spacing_ids.index = spacing_ids['ID'] + args = [spacing_ids] + + elif mapper_name == "DistributionTransformerEquipmentMapper": + transformer_network_data = read_cyme_data(network_file, 'TRANSFORMER SETTING') + transformer_map = {} + for idx, row in transformer_network_data.iterrows(): + transformer_type = row['EqID'] + transformer_map[transformer_type] = row + + byphase_transformer_network_data = read_cyme_data(network_file, "TRANSFORMER BYPHASE SETTING") + for idx, row in byphase_transformer_network_data.iterrows(): + for phase in ['1', '2', '3']: + transformer_type = row['PhaseTransformerID' + phase] + if transformer_type is not None and transformer_type != '': + transformer_map[transformer_type] = row + args = [transformer_map] + + elif mapper_name == "DistributionTransformerMapper" or mapper_name == "DistributionTransformerByPhaseMapper": + transformer_equipment_data = read_cyme_data(equipment_file, "TRANSFORMER") + transformer_map = {} + for idx, row in transformer_equipment_data.iterrows(): + transformer_type = row['ID'] + transformer_map[transformer_type] = row + args = [section_id_sections, transformer_map] + + def parse_row(row): + model_entry = mapper.parse(row, *args) + return model_entry + + if mapper_name == 'MatrixImpedanceBranchEquipmentMapper': + for phases in range(1,4): + args = [phases] + components = data.apply(parse_row, axis=1) + components = [c for c in components if c is not None] + components = [item for c in components for item in (c if isinstance(c, list) else [c])] + self.system.add_components(*components) + else: + components = data.apply(parse_row, axis=1) + components = [c for c in components if c is not None] + components = [item for c in components for item in (c if isinstance(c, list) else [c])] + self.system.add_components(*components) def get_system(self) -> DistributionSystem: return self.system From b4b38efe03060f8ad46e5f815c4e6be2cebb6c1e Mon Sep 17 00:00:00 2001 From: tarekelgindy Date: Thu, 16 Oct 2025 15:12:50 -0600 Subject: [PATCH 21/50] adding functionality for concentric neutral cables to be added. Not used since spacings not provided in CYME --- src/ditto/enumerations.py | 1 + .../equipment/geometry_branch_equipment.py | 79 ++++++++++++++----- 2 files changed, 62 insertions(+), 18 deletions(-) diff --git a/src/ditto/enumerations.py b/src/ditto/enumerations.py index 43446c3..8323bb1 100644 --- a/src/ditto/enumerations.py +++ b/src/ditto/enumerations.py @@ -10,6 +10,7 @@ class OpenDSSFileTypes(str, Enum): LINES_FILE = "Lines.dss" LOADS_FILE = "Loads.dss" WIRES_FILE = "WireData.dss" + CABLES_FILE = "CableData.dss" LINE_GEOMETRIES_FILE = "LineGeometry.dss" SWITCH_CODES_FILE = "SwitchCodes.dss" SWITCH_FILE = "Switches.dss" diff --git a/src/ditto/readers/cyme/equipment/geometry_branch_equipment.py b/src/ditto/readers/cyme/equipment/geometry_branch_equipment.py index cee1ce7..e7005b6 100644 --- a/src/ditto/readers/cyme/equipment/geometry_branch_equipment.py +++ b/src/ditto/readers/cyme/equipment/geometry_branch_equipment.py @@ -1,4 +1,4 @@ -from gdm.quantities import Current, Distance, ResistancePULength, Distance +from gdm.quantities import Current, Distance, ResistancePULength, Distance, Voltage from ditto.readers.cyme.utils import read_cyme_data from ditto.readers.cyme.cyme_mapper import CymeMapper from gdm.distribution.equipment.geometry_branch_equipment import GeometryBranchEquipment @@ -120,19 +120,20 @@ def __init__(self, system): cyme_file = 'Equipment' cyme_section = 'CABLE' - def parse(self, row): + def parse(self, row, equipment_file): name = self.map_name(row) - strand_diameter = self.map_strand_diameter(row) - conductor_diameter = self.map_conductor_diameter(row) - cable_diameter = self.map_cable_diameter(row) - insulation_thickness = self.map_insulation_thickness(row) - insulation_diameter = self.map_insulation_diameter(row) + strand_diameter = self.map_strand_diameter(row, equipment_file) + conductor_diameter = self.map_conductor_diameter(row, equipment_file) + cable_diameter = self.map_cable_diameter(row, equipment_file) + insulation_thickness = self.map_insulation_thickness(row, equipment_file) + insulation_diameter = conductor_diameter+2*insulation_thickness + Distance(0.001,'mm') + cable_diameter = insulation_diameter + 2* strand_diameter amapcity = self.map_ampacity(row) - conductor_gmr = self.map_conductor_gmr(row) - strand_gmr = self.map_strand_gmr(row) - phase_ac_resistance = self.map_phase_ac_resistance(row) - strand_ac_resistance = self.map_strand_ac_resistance(row) - num_neutral_strands = self.map_num_neutral_strands(row) + conductor_gmr = conductor_diameter/2 *0.7788 + strand_gmr = strand_diameter/2 * 0.7788 + phase_ac_resistance = self.map_phase_ac_resistance(row, equipment_file) + strand_ac_resistance = self.map_strand_ac_resistance(row, equipment_file) + num_neutral_strands = self.map_num_neutral_strands(row, equipment_file) rated_voltage = self.map_rated_voltage(row) return ConcentricCableEquipment(name=name, strand_diameter=strand_diameter, @@ -152,9 +153,9 @@ def map_name(self, row): name = row['ID'] return name - def map_conductor_diameter(self, row): + def map_conductor_diameter(self, row, equipment_file): cable_name = row['ID'] - conductor_info = read_cyme_data(self.system.equipment_file,"CABLE CONDUCTOR") + conductor_info = read_cyme_data(equipment_file,"CABLE CONDUCTOR") for idx, row in conductor_info.iterrows(): if row['ID'] == cable_name: conductor_diameter = float(row['Diameter']) @@ -162,14 +163,56 @@ def map_conductor_diameter(self, row): return None - def map_strand_diameter(self, row): + def map_strand_diameter(self, row, equipment_file): cable_name = row['ID'] - strand_info = read_cyme_data(self.system.equipment_file,"CABLE CONCENTRIC NEUTRAL") + strand_info = read_cyme_data(equipment_file,"CABLE CONCENTRIC NEUTRAL") for idx, row in strand_info.iterrows(): if row['ID'] == cable_name: - strand_diameter = float(row['Diameter']) + strand_diameter = float(row['Thickness']) return Distance(strand_diameter,'inch').to('mm') - return None + return Distance(0.01,'mm') + + def map_insulation_thickness(self, row, equipment_file): + # TODO: Understand how this is actually represented + return Distance(0.001,'mm') + + def map_insulation_diameter(self, row, equipment_file): + # TODO: Understand how this is actually represented + pass + + def map_conductor_gmr(self, row, equipment_file): + pass + + def map_strand_gmr(self, row, equipment_file): + pass + + def map_cable_diameter(self, row, equipment_file): + # TODO: should this be changed? + pass + + def map_phase_ac_resistance(self, row, equipment_file): + resistance = ResistancePULength(float(row['R1']), 'ohm/mile').to('ohm/km') + return resistance + + def map_strand_ac_resistance(self, row, equipment_file): + # Using the same as for Cable. Need to check this... + resistance = ResistancePULength(float(row['R1']), 'ohm/mile').to('ohm/km') + return resistance + + def map_num_neutral_strands(self, row, equipment_file): + cable_name = row['ID'] + strand_info = read_cyme_data(equipment_file,"CABLE CONCENTRIC NEUTRAL") + for idx, row in strand_info.iterrows(): + if row['ID'] == cable_name: + strand_number = float(row['NumberOfWires']) + return strand_number + + return 1 + + def map_rated_voltage(self, row): + # No voltage included. Need to remove this as a required field + return Voltage(23.0, "kilovolts") + def map_ampacity(self, row): ampacity = Current(float(row['Amps']),'amp') From a3b3eacda7d440da8ac1442c839842980a450519 Mon Sep 17 00:00:00 2001 From: tarekelgindy Date: Thu, 16 Oct 2025 15:16:52 -0600 Subject: [PATCH 22/50] small update from nominal voltage to rated voltage --- .../opendss/equipment/distribution_transformer_equipment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ditto/writers/opendss/equipment/distribution_transformer_equipment.py b/src/ditto/writers/opendss/equipment/distribution_transformer_equipment.py index 623d7d1..067006d 100644 --- a/src/ditto/writers/opendss/equipment/distribution_transformer_equipment.py +++ b/src/ditto/writers/opendss/equipment/distribution_transformer_equipment.py @@ -39,7 +39,7 @@ def map_windings(self): num_phases = winding.num_phases # nominal_voltage - nom_voltage = winding.nominal_voltage.to("kV").magnitude + nom_voltage = winding.rated_voltage.to("kV").magnitude kvs.append(nom_voltage if num_phases == 1 else nom_voltage * 1.732) # resistance pctRs.append(winding.resistance) From cca3dfd078030edea892d7a5dddb3cb506dce7a8 Mon Sep 17 00:00:00 2001 From: tarekelgindy Date: Thu, 16 Oct 2025 15:17:37 -0600 Subject: [PATCH 23/50] adding concentric neutral cable writing capability in opendss --- src/ditto/writers/opendss/__init__.py | 1 + .../equipment/concentric_cable_equipment.py | 70 +++++++++++++++++++ src/ditto/writers/opendss/write.py | 3 - 3 files changed, 71 insertions(+), 3 deletions(-) create mode 100644 src/ditto/writers/opendss/equipment/concentric_cable_equipment.py diff --git a/src/ditto/writers/opendss/__init__.py b/src/ditto/writers/opendss/__init__.py index 6f3a798..f782cdf 100644 --- a/src/ditto/writers/opendss/__init__.py +++ b/src/ditto/writers/opendss/__init__.py @@ -13,6 +13,7 @@ ) from ditto.writers.opendss.equipment.geometry_branch_equipment import GeometryBranchEquipmentMapper from ditto.writers.opendss.equipment.bare_conductor_equipment import BareConductorEquipmentMapper +from ditto.writers.opendss.equipment.concentric_cable_equipment import ConcentricCableEquipmentMapper from ditto.writers.opendss.components.distribution_capacitor import DistributionCapacitorMapper from ditto.writers.opendss.components.distribution_load import DistributionLoadMapper from ditto.writers.opendss.components.distribution_transformer import DistributionTransformerMapper diff --git a/src/ditto/writers/opendss/equipment/concentric_cable_equipment.py b/src/ditto/writers/opendss/equipment/concentric_cable_equipment.py new file mode 100644 index 0000000..3f36693 --- /dev/null +++ b/src/ditto/writers/opendss/equipment/concentric_cable_equipment.py @@ -0,0 +1,70 @@ +from ditto.writers.opendss.opendss_mapper import OpenDSSMapper +from ditto.enumerations import OpenDSSFileTypes + + +class ConcentricCableEquipmentMapper(OpenDSSMapper): + def __init__(self, model): + super().__init__(model) + + altdss_name = "CNData" + altdss_composition_name = None + opendss_file = OpenDSSFileTypes.WIRES_FILE.value + + def map_name(self): + self.opendss_dict["Name"] = self.model.name + + def map_strand_diameter(self): + self.opendss_dict["DiaStrand"] = self.model.strand_diameter.magnitude + + def map_conductor_diameter(self): + self.opendss_dict["Radius"] = self.model.conductor_diameter.magnitude / 2 + rad_units = str(self.model.conductor_diameter.units) + if rad_units not in self.length_units_map: + raise ValueError(f"{rad_units} not mapped for OpenDSS") + self.opendss_dict["RadUnits"] = self.length_units_map[rad_units] + + def map_cable_diameter(self): + self.opendss_dict["DiaCable"] = self.model.cable_diameter.magnitude + + def map_insulation_thickness(self): + self.opendss_dict["InsLayer"] = self.model.insulation_thickness.magnitude + + def map_insulation_diameter(self): + self.opendss_dict["DiaIns"] = self.model.insulation_diameter.magnitude + + def map_ampacity(self): + ampacity_amps = self.model.ampacity.to("ampere") + self.opendss_dict["NormAmps"] = ampacity_amps.magnitude + + def map_conductor_gmr(self): + self.opendss_dict["GMRAC"] = self.model.conductor_gmr.magnitude + gmr_units = str(self.model.conductor_gmr.units) + if gmr_units not in self.length_units_map: + raise ValueError(f"{gmr_units} not mapped for OpenDSS") + self.opendss_dict["GMRUnits"] = self.length_units_map[gmr_units] + + def map_strand_gmr(self): + self.opendss_dict["GMRStrand"] = self.model.strand_gmr.magnitude + + def map_phase_ac_resistance(self): + resistance = self.model.phase_ac_resistance.to("ohms/km") + self.opendss_dict["RAC"] = resistance.magnitude + self.opendss_dict["RUnits"] = "km" + + def map_strand_ac_resistance(self): + resistance = self.model.strand_ac_resistance.to("ohms/km") + self.opendss_dict["RStrand"] = resistance.magnitude + self.opendss_dict["RUnits"] = "km" + + def map_num_neutral_strands(self): + self.opendss_dict["k"] = self.model.num_neutral_strands + + def map_rated_voltage(self): + pass + + def map_insulation(self): + pass + + def map_loading_limit(self): + # Not mapped in OpenDSS + pass diff --git a/src/ditto/writers/opendss/write.py b/src/ditto/writers/opendss/write.py index 28afdf9..0880cce 100644 --- a/src/ditto/writers/opendss/write.py +++ b/src/ditto/writers/opendss/write.py @@ -76,9 +76,6 @@ def write( # noqa # Example mapper is class DistributionBusMapper for model in components: - # Example model is instance of DistributionBus - if not isinstance(model, DistributionComponentBase) and not (isinstance(model, BareConductorEquipment) or isinstance(model, ConcentricCableEquipment)): - continue model_map = mapper(model) model_map.populate_opendss_dictionary() dss_string = self._get_dss_string(model_map) From 1e8b830a5537f20e040de5d402ea6ce23f531c1d Mon Sep 17 00:00:00 2001 From: tarekelgindy Date: Thu, 16 Oct 2025 15:31:49 -0600 Subject: [PATCH 24/50] cyme reader to also export to geojson and include loads --- tests/test_cyme/test_cyme_reader.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_cyme/test_cyme_reader.py b/tests/test_cyme/test_cyme_reader.py index 8f9a6ba..db55cbb 100644 --- a/tests/test_cyme/test_cyme_reader.py +++ b/tests/test_cyme/test_cyme_reader.py @@ -17,6 +17,7 @@ cyme_network_name = "Network.txt" cyme_equipment_name = "Equipment.txt" +cyme_load_name = "Load.txt" target_files = set([cyme_network_name, cyme_equipment_name]) matching_folders = [] @@ -33,11 +34,12 @@ def test_cyme_reader(cyme_folder: Path, tmp_path): if not export_path.exists(): export_path.mkdir(parents=True, exist_ok=True) - reader = Reader(cyme_folder / cyme_network_name, cyme_folder / cyme_equipment_name) + reader = Reader(cyme_folder / cyme_network_name, cyme_folder / cyme_equipment_name, cyme_folder / cyme_load_name) writer = Writer(reader.get_system()) writer.write(export_path / "opendss", separate_substations=False, separate_feeders=False) system = reader.get_system() json_path = (export_path / cyme_folder.stem.lower()).with_suffix(".json") system.to_json(json_path, overwrite=True, indent=4) + system.to_geojson(export_path / (cyme_folder.stem.lower() + ".geojson")) assert json_path.exists(), "Failed to export the json file" From 8679948a41bc762f6f6d5226ceaaf7675f710312 Mon Sep 17 00:00:00 2001 From: tarekelgindy Date: Thu, 16 Oct 2025 17:04:24 -0600 Subject: [PATCH 25/50] not checking for phase mismatches for the moment... --- src/ditto/readers/cyme/components/geometry_branch.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ditto/readers/cyme/components/geometry_branch.py b/src/ditto/readers/cyme/components/geometry_branch.py index 9f05ab0..150dec5 100644 --- a/src/ditto/readers/cyme/components/geometry_branch.py +++ b/src/ditto/readers/cyme/components/geometry_branch.py @@ -67,7 +67,8 @@ def map_phases(self, row, section_id_sections, equipment, buses): bus.phases.append(Phase.N) return phases else: - raise ValueError(f"Number of phases {len(phases)} does not match number of conductors {len(equipment.conductors)} for line {row['SectionID']}") + return phases + #raise ValueError(f"Number of phases {len(phases)} does not match number of conductors {len(equipment.conductors)} for line {row['SectionID']}") def map_equipment(self, row): line_id = row['LineCableID'] From 80144d798398fad759a23b060efbcc46f2f9d139 Mon Sep 17 00:00:00 2001 From: tarekelgindy Date: Fri, 17 Oct 2025 13:15:26 -0600 Subject: [PATCH 26/50] minor fixes to cyme reader --- .../readers/cyme/components/distribution_voltage_source.py | 4 ++-- src/ditto/readers/cyme/components/matrix_impedance_branch.py | 1 + src/ditto/readers/cyme/reader.py | 1 - 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ditto/readers/cyme/components/distribution_voltage_source.py b/src/ditto/readers/cyme/components/distribution_voltage_source.py index a46575d..9cad714 100644 --- a/src/ditto/readers/cyme/components/distribution_voltage_source.py +++ b/src/ditto/readers/cyme/components/distribution_voltage_source.py @@ -11,7 +11,7 @@ def __init__(self, cyme_model): super().__init__(cyme_model) cyme_file = 'Network' - cyme_section = 'HEADNODES' + cyme_section = 'SOURCE' def parse(self, row, feeder_voltage_map): name = self.map_name(row) @@ -21,7 +21,7 @@ def parse(self, row, feeder_voltage_map): return None feeder_id = feeder.name - feeder_voltage = feeder_voltage_map.get(feeder_id) + feeder_voltage = float(row['OperatingVoltageA']) if feeder_voltage is None or feeder_voltage == '': return None diff --git a/src/ditto/readers/cyme/components/matrix_impedance_branch.py b/src/ditto/readers/cyme/components/matrix_impedance_branch.py index 7bbe6d5..a9469b2 100644 --- a/src/ditto/readers/cyme/components/matrix_impedance_branch.py +++ b/src/ditto/readers/cyme/components/matrix_impedance_branch.py @@ -24,6 +24,7 @@ def parse(self, row, used_sections, section_id_sections, cyme_section): length = self.map_length(row, cyme_section) phases = self.map_phases(row, section_id_sections) equipment = self.map_equipment(row, phases, cyme_section) + used_sections.add(name) try: return MatrixImpedanceBranch(name=name, buses=buses, diff --git a/src/ditto/readers/cyme/reader.py b/src/ditto/readers/cyme/reader.py index f1ca9a9..0c3834d 100644 --- a/src/ditto/readers/cyme/reader.py +++ b/src/ditto/readers/cyme/reader.py @@ -41,7 +41,6 @@ class Reader(AbstractReader): "GeometryBranchByPhase", "DistributionTransformerByPhase", "DistributionTransformer", - "MatrixImpedanceBranch", ] validation_errors = [] From 5b8cf3eb1e029a384fadfca5609e2f197fed5c9f Mon Sep 17 00:00:00 2001 From: tarekelgindy Date: Fri, 17 Oct 2025 13:32:13 -0600 Subject: [PATCH 27/50] removing spaces in opendss outputs --- src/ditto/enumerations.py | 4 ++-- src/ditto/writers/opendss/__init__.py | 5 +++-- .../writers/opendss/components/distribution_branch.py | 6 +++--- src/ditto/writers/opendss/components/distribution_bus.py | 2 +- .../writers/opendss/components/distribution_capacitor.py | 4 ++-- src/ditto/writers/opendss/components/distribution_load.py | 4 ++-- .../opendss/components/distribution_transformer.py | 8 ++++---- src/ditto/writers/opendss/components/geometry_branch.py | 2 +- .../writers/opendss/components/matrix_impedance_branch.py | 2 +- .../writers/opendss/components/matrix_impedance_fuse.py | 2 +- .../writers/opendss/components/matrix_impedance_switch.py | 2 +- .../opendss/components/sequence_impedance_branch.py | 2 +- .../writers/opendss/equipment/bare_conductor_equipment.py | 2 +- .../opendss/equipment/concentric_cable_equipment.py | 2 +- .../equipment/distribution_transformer_equipment.py | 2 +- .../opendss/equipment/geometry_branch_equipment.py | 4 ++-- .../equipment/matrix_impedance_branch_equipment.py | 2 +- .../equipment/sequence_impedance_branch_equipment.py | 2 +- 18 files changed, 29 insertions(+), 28 deletions(-) diff --git a/src/ditto/enumerations.py b/src/ditto/enumerations.py index 8323bb1..9306a57 100644 --- a/src/ditto/enumerations.py +++ b/src/ditto/enumerations.py @@ -6,11 +6,11 @@ class OpenDSSFileTypes(str, Enum): COORDINATE_FILE = "BusCoords.dss" TRANSFORMERS_FILE = "Transformers.dss" CAPACITORS_FILE = "Capacitors.dss" + WIRES_FILE = "WireData.dss" + CABLES_FILE = "CableData.dss" LINECODES_FILE = "LineCodes.dss" LINES_FILE = "Lines.dss" LOADS_FILE = "Loads.dss" - WIRES_FILE = "WireData.dss" - CABLES_FILE = "CableData.dss" LINE_GEOMETRIES_FILE = "LineGeometry.dss" SWITCH_CODES_FILE = "SwitchCodes.dss" SWITCH_FILE = "Switches.dss" diff --git a/src/ditto/writers/opendss/__init__.py b/src/ditto/writers/opendss/__init__.py index f782cdf..6ff55a5 100644 --- a/src/ditto/writers/opendss/__init__.py +++ b/src/ditto/writers/opendss/__init__.py @@ -1,4 +1,6 @@ from ditto.writers.opendss.components.distribution_bus import DistributionBusMapper +from ditto.writers.opendss.equipment.bare_conductor_equipment import BareConductorEquipmentMapper +from ditto.writers.opendss.equipment.concentric_cable_equipment import ConcentricCableEquipmentMapper from ditto.writers.opendss.components.distribution_branch import DistributionBranchMapper from ditto.writers.opendss.components.sequence_impedance_branch import ( SequenceImpedanceBranchMapper, @@ -12,8 +14,7 @@ MatrixImpedanceBranchEquipmentMapper, ) from ditto.writers.opendss.equipment.geometry_branch_equipment import GeometryBranchEquipmentMapper -from ditto.writers.opendss.equipment.bare_conductor_equipment import BareConductorEquipmentMapper -from ditto.writers.opendss.equipment.concentric_cable_equipment import ConcentricCableEquipmentMapper + from ditto.writers.opendss.components.distribution_capacitor import DistributionCapacitorMapper from ditto.writers.opendss.components.distribution_load import DistributionLoadMapper from ditto.writers.opendss.components.distribution_transformer import DistributionTransformerMapper diff --git a/src/ditto/writers/opendss/components/distribution_branch.py b/src/ditto/writers/opendss/components/distribution_branch.py index ef4c041..3825721 100644 --- a/src/ditto/writers/opendss/components/distribution_branch.py +++ b/src/ditto/writers/opendss/components/distribution_branch.py @@ -13,11 +13,11 @@ def __init__(self, model): opendss_file = OpenDSSFileTypes.LINES_FILE.value def map_name(self): - self.opendss_dict["Name"] = self.model.name + self.opendss_dict["Name"] = self.model.name.replace(" ","_") def map_buses(self): - self.opendss_dict["Bus1"] = self.model.buses[0].name - self.opendss_dict["Bus2"] = self.model.buses[1].name + self.opendss_dict["Bus1"] = self.model.buses[0].name.replace(" ","_") + self.opendss_dict["Bus2"] = self.model.buses[1].name.replace(" ","_") for phase in self.model.phases: if phase != Phase.N: self.opendss_dict["Bus1"] += self.phase_map[phase] diff --git a/src/ditto/writers/opendss/components/distribution_bus.py b/src/ditto/writers/opendss/components/distribution_bus.py index 2107128..023cf09 100644 --- a/src/ditto/writers/opendss/components/distribution_bus.py +++ b/src/ditto/writers/opendss/components/distribution_bus.py @@ -11,7 +11,7 @@ def __init__(self, model): opendss_file = OpenDSSFileTypes.COORDINATE_FILE.value def map_name(self): - self.opendss_dict["Name"] = self.model.name + self.opendss_dict["Name"] = self.model.name.replace(" ", "_") def map_coordinate(self): if hasattr(self.model.coordinate, "x"): diff --git a/src/ditto/writers/opendss/components/distribution_capacitor.py b/src/ditto/writers/opendss/components/distribution_capacitor.py index 0cd2c0f..121dd49 100644 --- a/src/ditto/writers/opendss/components/distribution_capacitor.py +++ b/src/ditto/writers/opendss/components/distribution_capacitor.py @@ -13,10 +13,10 @@ def __init__(self, model): opendss_file = OpenDSSFileTypes.CAPACITORS_FILE.value def map_name(self): - self.opendss_dict["Name"] = self.model.name + self.opendss_dict["Name"] = self.model.name.replace(" ", "_") def map_bus(self): - self.opendss_dict["Bus1"] = self.model.bus.name + self.opendss_dict["Bus1"] = self.model.bus.name.replace(" ","_") num_phases = len(self.model.phases) for phase in self.model.phases: self.opendss_dict["Bus1"] += self.phase_map[phase] diff --git a/src/ditto/writers/opendss/components/distribution_load.py b/src/ditto/writers/opendss/components/distribution_load.py index 40de9cb..1a6608f 100644 --- a/src/ditto/writers/opendss/components/distribution_load.py +++ b/src/ditto/writers/opendss/components/distribution_load.py @@ -15,12 +15,12 @@ def __init__(self, model): opendss_file = OpenDSSFileTypes.LOADS_FILE.value def map_name(self): - self.opendss_dict["Name"] = self.model.name + self.opendss_dict["Name"] = self.model.name.replace(" ","_") # TODO: Want to set the Yearly attribute here, but need to access the system. Is that possible? def map_bus(self): num_phases = len(self.model.phases) - self.opendss_dict["Bus1"] = self.model.bus.name + self.opendss_dict["Bus1"] = self.model.bus.name.replace(" ","_") for phase in self.model.phases: self.opendss_dict["Bus1"] += self.phase_map[phase] # TODO: Should we include the phases its connected to here? diff --git a/src/ditto/writers/opendss/components/distribution_transformer.py b/src/ditto/writers/opendss/components/distribution_transformer.py index a0c14c9..42cd213 100644 --- a/src/ditto/writers/opendss/components/distribution_transformer.py +++ b/src/ditto/writers/opendss/components/distribution_transformer.py @@ -11,7 +11,7 @@ def __init__(self, model): opendss_file = OpenDSSFileTypes.TRANSFORMERS_FILE.value def map_name(self): - self.opendss_dict["Name"] = self.model.name + self.opendss_dict["Name"] = self.model.name.replace(" ","_") def map_buses(self): buses = [] @@ -21,7 +21,7 @@ def map_buses(self): if is_center_tapped: for i in range(len(self.model.buses)): bus = self.model.buses[i] - buses.append(bus.name) + buses.append(bus.name.replace(" ","_")) dss_phases = "" for phase in self.model.winding_phases[0]: dss_phases += self.phase_map[phase] @@ -31,7 +31,7 @@ def map_buses(self): else: for bus in self.model.buses: - buses.append(bus.name) + buses.append(bus.name.replace(" ","_")) for winding_phases in self.model.winding_phases: dss_phases = "" for phase in winding_phases: @@ -48,4 +48,4 @@ def map_winding_phases(self): def map_equipment(self): equipment = self.model.equipment - self.opendss_dict["XfmrCode"] = equipment.name + self.opendss_dict["XfmrCode"] = equipment.name.replace(" ","_") diff --git a/src/ditto/writers/opendss/components/geometry_branch.py b/src/ditto/writers/opendss/components/geometry_branch.py index 812ac09..e341ee3 100644 --- a/src/ditto/writers/opendss/components/geometry_branch.py +++ b/src/ditto/writers/opendss/components/geometry_branch.py @@ -11,4 +11,4 @@ def __init__(self, model): opendss_file = OpenDSSFileTypes.LINES_FILE.value def map_equipment(self): - self.opendss_dict["Geometry"] = self.model.equipment.name + self.opendss_dict["Geometry"] = self.model.equipment.name.replace(" ", "_") diff --git a/src/ditto/writers/opendss/components/matrix_impedance_branch.py b/src/ditto/writers/opendss/components/matrix_impedance_branch.py index 8413576..dea0b11 100644 --- a/src/ditto/writers/opendss/components/matrix_impedance_branch.py +++ b/src/ditto/writers/opendss/components/matrix_impedance_branch.py @@ -11,4 +11,4 @@ def __init__(self, model): opendss_file = OpenDSSFileTypes.LINES_FILE.value def map_equipment(self): - self.opendss_dict["LineCode"] = self.model.equipment.name + self.opendss_dict["LineCode"] = self.model.equipment.name.replace(" ", "_") diff --git a/src/ditto/writers/opendss/components/matrix_impedance_fuse.py b/src/ditto/writers/opendss/components/matrix_impedance_fuse.py index e8fb87a..b923a7f 100644 --- a/src/ditto/writers/opendss/components/matrix_impedance_fuse.py +++ b/src/ditto/writers/opendss/components/matrix_impedance_fuse.py @@ -11,7 +11,7 @@ def __init__(self, model): opendss_file = OpenDSSFileTypes.FUSE_FILE.value def map_equipment(self): - self.opendss_dict["LineCode"] = self.model.equipment.name + self.opendss_dict["LineCode"] = self.model.equipment.name.replace(" ", "_") def map_is_closed(self): # Require every phase to be enabled for the OpenDSS line to be enabled. diff --git a/src/ditto/writers/opendss/components/matrix_impedance_switch.py b/src/ditto/writers/opendss/components/matrix_impedance_switch.py index ea37181..fa2cfe9 100644 --- a/src/ditto/writers/opendss/components/matrix_impedance_switch.py +++ b/src/ditto/writers/opendss/components/matrix_impedance_switch.py @@ -11,7 +11,7 @@ def __init__(self, model): opendss_file = OpenDSSFileTypes.SWITCH_FILE.value def map_equipment(self): - self.opendss_dict["LineCode"] = self.model.equipment.name + self.opendss_dict["LineCode"] = self.model.equipment.name.replace(" ", "_") def map_is_closed(self): # Require every phase to be enabled for the OpenDSS line to be enabled. diff --git a/src/ditto/writers/opendss/components/sequence_impedance_branch.py b/src/ditto/writers/opendss/components/sequence_impedance_branch.py index a895903..f6556f3 100644 --- a/src/ditto/writers/opendss/components/sequence_impedance_branch.py +++ b/src/ditto/writers/opendss/components/sequence_impedance_branch.py @@ -11,4 +11,4 @@ def __init__(self, model): opendss_file = OpenDSSFileTypes.LINES_FILE.value def map_equipment(self): - self.opendss_dict["LineCode"] = self.model.equipment.name + self.opendss_dict["LineCode"] = self.model.equipment.name.replace(" ", "_") diff --git a/src/ditto/writers/opendss/equipment/bare_conductor_equipment.py b/src/ditto/writers/opendss/equipment/bare_conductor_equipment.py index d73c855..f8ec14e 100644 --- a/src/ditto/writers/opendss/equipment/bare_conductor_equipment.py +++ b/src/ditto/writers/opendss/equipment/bare_conductor_equipment.py @@ -11,7 +11,7 @@ def __init__(self, model): opendss_file = OpenDSSFileTypes.WIRES_FILE.value def map_name(self): - self.opendss_dict["Name"] = self.model.name + self.opendss_dict["Name"] = self.model.name.replace(" ","_") def map_conductor_diameter(self): self.opendss_dict["Radius"] = self.model.conductor_diameter.magnitude / 2 diff --git a/src/ditto/writers/opendss/equipment/concentric_cable_equipment.py b/src/ditto/writers/opendss/equipment/concentric_cable_equipment.py index 3f36693..3a5a605 100644 --- a/src/ditto/writers/opendss/equipment/concentric_cable_equipment.py +++ b/src/ditto/writers/opendss/equipment/concentric_cable_equipment.py @@ -11,7 +11,7 @@ def __init__(self, model): opendss_file = OpenDSSFileTypes.WIRES_FILE.value def map_name(self): - self.opendss_dict["Name"] = self.model.name + self.opendss_dict["Name"] = self.model.name.replace(" ","_") def map_strand_diameter(self): self.opendss_dict["DiaStrand"] = self.model.strand_diameter.magnitude diff --git a/src/ditto/writers/opendss/equipment/distribution_transformer_equipment.py b/src/ditto/writers/opendss/equipment/distribution_transformer_equipment.py index 067006d..3171b8f 100644 --- a/src/ditto/writers/opendss/equipment/distribution_transformer_equipment.py +++ b/src/ditto/writers/opendss/equipment/distribution_transformer_equipment.py @@ -11,7 +11,7 @@ def __init__(self, model): opendss_file = OpenDSSFileTypes.TRANSFORMERS_FILE.value def map_name(self): - self.opendss_dict["Name"] = self.model.name + self.opendss_dict["Name"] = self.model.name.replace(" ","_") def map_pct_no_load_loss(self): self.opendss_dict["pctNoLoadLoss"] = self.model.pct_no_load_loss diff --git a/src/ditto/writers/opendss/equipment/geometry_branch_equipment.py b/src/ditto/writers/opendss/equipment/geometry_branch_equipment.py index de90a5f..0047669 100644 --- a/src/ditto/writers/opendss/equipment/geometry_branch_equipment.py +++ b/src/ditto/writers/opendss/equipment/geometry_branch_equipment.py @@ -14,7 +14,7 @@ def __init__(self, model): opendss_file = OpenDSSFileTypes.LINECODES_FILE.value def map_name(self): - self.opendss_dict["Name"] = self.model.name + self.opendss_dict["Name"] = self.model.name.replace(" ","_") def map_common(self): units = [] @@ -55,5 +55,5 @@ def map_conductors(self): # conductor_type = 'tsdata' else: raise ValueError(f"Unknown conductor type {conductor}") - all_conductors.append(f"{conductor_type}.{conductor.name}") + all_conductors.append(f"{conductor_type}.{conductor.name.replace(' ','_')}") self.opendss_dict["Conductors"] = all_conductors diff --git a/src/ditto/writers/opendss/equipment/matrix_impedance_branch_equipment.py b/src/ditto/writers/opendss/equipment/matrix_impedance_branch_equipment.py index 33f13da..caaae65 100644 --- a/src/ditto/writers/opendss/equipment/matrix_impedance_branch_equipment.py +++ b/src/ditto/writers/opendss/equipment/matrix_impedance_branch_equipment.py @@ -19,7 +19,7 @@ def map_common(self): self.opendss_dict["Units"] = "km" def map_name(self): - self.opendss_dict["Name"] = self.model.name + self.opendss_dict["Name"] = self.model.name.replace(" ", "_") def map_r_matrix(self): r_matrix_ohms = self.model.r_matrix.to("ohm/km") diff --git a/src/ditto/writers/opendss/equipment/sequence_impedance_branch_equipment.py b/src/ditto/writers/opendss/equipment/sequence_impedance_branch_equipment.py index 11238c6..1ca57a1 100644 --- a/src/ditto/writers/opendss/equipment/sequence_impedance_branch_equipment.py +++ b/src/ditto/writers/opendss/equipment/sequence_impedance_branch_equipment.py @@ -11,7 +11,7 @@ def __init__(self, model): opendss_file = OpenDSSFileTypes.LINECODES_FILE.value def map_name(self): - self.opendss_dict["Name"] = self.model.name + self.opendss_dict["Name"] = self.model.name.replace(" ", "_") def map_common(self): self.opendss_dict["Units"] = "km" From 91c85001a17081034c9e8cbd7ffd095333471eca Mon Sep 17 00:00:00 2001 From: tarekelgindy Date: Fri, 17 Oct 2025 13:33:54 -0600 Subject: [PATCH 28/50] opendss fixes for changing nominal voltage to rated voltage --- .../opendss/components/distribution_vsource.py | 18 ++++++++++-------- src/ditto/writers/opendss/write.py | 4 +++- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/ditto/writers/opendss/components/distribution_vsource.py b/src/ditto/writers/opendss/components/distribution_vsource.py index 599775f..ca7d499 100644 --- a/src/ditto/writers/opendss/components/distribution_vsource.py +++ b/src/ditto/writers/opendss/components/distribution_vsource.py @@ -1,5 +1,7 @@ from ditto.writers.opendss.opendss_mapper import OpenDSSMapper from ditto.enumerations import OpenDSSFileTypes +from gdm.quantities import Voltage + class DistributionVoltageSourceMapper(OpenDSSMapper): @@ -11,10 +13,10 @@ def __init__(self, model): opendss_file = OpenDSSFileTypes.MASTER_FILE.value def map_name(self): - self.opendss_dict["Name"] = self.model.name + self.opendss_dict["Name"] = self.model.name.replace(" ", "_") def map_bus(self): - self.opendss_dict["Bus1"] = self.model.bus.name + self.opendss_dict["Bus1"] = self.model.bus.name.replace(" ","_") for phase in self.model.phases: self.opendss_dict["Bus1"] += self.phase_map[phase] @@ -31,7 +33,7 @@ def map_equipment(self): voltage = self.model.equipment.sources[0].voltage angle = self.model.equipment.sources[0].angle num_phases = len(self.model.phases) - nominal_voltage = self.model.bus.nominal_voltage + rated_voltage = self.model.bus.rated_voltage for phase_source in self.model.equipment.sources: r1 += phase_source.r1 @@ -44,14 +46,14 @@ def map_equipment(self): r0 = r0.to("ohm") x1 = x1.to("ohm") x0 = x0.to("ohm") - voltage = voltage.to("kilovolt") - nominal_voltage = nominal_voltage.to("kilovolt") + # convert voltage from float to to quantity in kV + voltage = Voltage(voltage, "kilovolt") + rated_voltage = rated_voltage.to("kilovolt") angle = angle.to("degree") - v_mag = voltage.magnitude if num_phases == 1 else voltage.magnitude * 1.732 - v_nom = nominal_voltage.magnitude if num_phases == 1 else nominal_voltage.magnitude * 1.732 + v_nom = rated_voltage.magnitude self.opendss_dict["Angle"] = angle.magnitude - self.opendss_dict["pu"] = v_mag / v_nom + self.opendss_dict["pu"] = 1.0 self.opendss_dict["BasekV"] = v_nom self.opendss_dict["Z0"] = complex(r0.magnitude, x0.magnitude) self.opendss_dict["Z1"] = complex(r1.magnitude, x1.magnitude) diff --git a/src/ditto/writers/opendss/write.py b/src/ditto/writers/opendss/write.py index 0880cce..f5a3e63 100644 --- a/src/ditto/writers/opendss/write.py +++ b/src/ditto/writers/opendss/write.py @@ -44,7 +44,7 @@ def _get_voltage_bases(self) -> list[float]: voltage_bases = [] buses: list[DistributionBus] = list(self.system.get_components(DistributionBus)) for bus in buses: - voltage_bases.append(bus.nominal_voltage.to("kilovolt").magnitude * 1.732) + voltage_bases.append(bus.rated_voltage.to("kilovolt").magnitude * 1.732) return list(set(voltage_bases)) def write( # noqa @@ -53,6 +53,7 @@ def write( # noqa separate_substations: bool = True, separate_feeders: bool = True, ): + output_folder = output_path base_redirect = set() feeders_redirect = defaultdict(set) substations_redirect = defaultdict(set) @@ -61,6 +62,7 @@ def write( # noqa component_types = self.system.get_component_types() seen_equipment = set() + for component_type in component_types: # Example component_type is DistributionBus components = self.system.get_components(component_type) From 2e0200343fe06557a3b0b5d90ecfcfd2f4456331 Mon Sep 17 00:00:00 2001 From: tarekelgindy Date: Fri, 17 Oct 2025 14:59:41 -0600 Subject: [PATCH 29/50] adding reclosers to opendss writer --- src/ditto/enumerations.py | 2 ++ src/ditto/writers/opendss/__init__.py | 4 ++++ .../components/matrix_impedance_recloser.py | 18 ++++++++++++++++++ .../matrix_impedance_recloser_equipment.py | 17 +++++++++++++++++ 4 files changed, 41 insertions(+) create mode 100644 src/ditto/writers/opendss/components/matrix_impedance_recloser.py create mode 100644 src/ditto/writers/opendss/equipment/matrix_impedance_recloser_equipment.py diff --git a/src/ditto/enumerations.py b/src/ditto/enumerations.py index 9306a57..c8e47e2 100644 --- a/src/ditto/enumerations.py +++ b/src/ditto/enumerations.py @@ -16,3 +16,5 @@ class OpenDSSFileTypes(str, Enum): SWITCH_FILE = "Switches.dss" FUSE_CODES_FILE = "FuseCodes.dss" FUSE_FILE = "Fuses.dss" + RECLOSER_CODES_FILE = "RecloserCodes.dss" + RECLOSER_FILE = "Reclosers.dss" diff --git a/src/ditto/writers/opendss/__init__.py b/src/ditto/writers/opendss/__init__.py index 6ff55a5..e24f784 100644 --- a/src/ditto/writers/opendss/__init__.py +++ b/src/ditto/writers/opendss/__init__.py @@ -30,3 +30,7 @@ MatrixImpedanceFuseEquipmentMapper, ) from ditto.writers.opendss.components.matrix_impedance_fuse import MatrixImpedanceFuseMapper +from ditto.writers.opendss.equipment.matrix_impedance_recloser_equipment import ( + MatrixImpedanceRecloserEquipmentMapper, +) +from ditto.writers.opendss.components.matrix_impedance_recloser import MatrixImpedanceRecloserMapper diff --git a/src/ditto/writers/opendss/components/matrix_impedance_recloser.py b/src/ditto/writers/opendss/components/matrix_impedance_recloser.py new file mode 100644 index 0000000..39ac2ac --- /dev/null +++ b/src/ditto/writers/opendss/components/matrix_impedance_recloser.py @@ -0,0 +1,18 @@ +from ditto.writers.opendss.components.distribution_branch import DistributionBranchMapper +from ditto.enumerations import OpenDSSFileTypes + + +class MatrixImpedanceRecloserMapper(DistributionBranchMapper): + def __init__(self, model): + super().__init__(model) + + altdss_name = "Line_LineCode" + altdss_composition_name = "Line" + opendss_file = OpenDSSFileTypes.RECLOSER_FILE.value + + def map_equipment(self): + self.opendss_dict["LineCode"] = self.model.equipment.name.replace(" ", "_") + + def map_is_closed(self): + # Require every phase to be enabled for the OpenDSS line to be enabled. + self.opendss_dict["Switch"] = "true" diff --git a/src/ditto/writers/opendss/equipment/matrix_impedance_recloser_equipment.py b/src/ditto/writers/opendss/equipment/matrix_impedance_recloser_equipment.py new file mode 100644 index 0000000..56e656d --- /dev/null +++ b/src/ditto/writers/opendss/equipment/matrix_impedance_recloser_equipment.py @@ -0,0 +1,17 @@ +from ditto.writers.opendss.equipment.matrix_impedance_branch_equipment import ( + MatrixImpedanceBranchEquipmentMapper, +) +from ditto.enumerations import OpenDSSFileTypes + + +class MatrixImpedanceRecloserEquipmentMapper(MatrixImpedanceBranchEquipmentMapper): + def __init__(self, model): + super().__init__(model) + + altdss_name = "LineCode_ZMatrixCMatrix" + altdss_composition_name = "LineCode" + opendss_file = OpenDSSFileTypes.RECLOSER_CODES_FILE.value + + def map_controller(self): + # Not mapped in OpenDSS + pass From 8cd05fb02b6114f5c4d41e48a7b899661491573a Mon Sep 17 00:00:00 2001 From: "Zink, Zephyr" Date: Mon, 20 Oct 2025 10:16:53 -0600 Subject: [PATCH 30/50] substation parsing, reconcile with tarek's changes --- .gitignore | 2 + .../cyme/components/distribution_bus.py | 12 +-- .../cyme/components/distribution_load.py | 19 ++-- .../components/distribution_voltage_source.py | 3 +- .../readers/cyme/equipment/load_equipment.py | 3 +- .../phase_voltagesource_equipment.py | 6 +- src/ditto/readers/cyme/reader.py | 100 +++++++++++++----- src/ditto/readers/cyme/utils.py | 26 +++-- 8 files changed, 122 insertions(+), 49 deletions(-) diff --git a/.gitignore b/.gitignore index fa3d34f..0fcf934 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +.DS_Store + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/src/ditto/readers/cyme/components/distribution_bus.py b/src/ditto/readers/cyme/components/distribution_bus.py index 3209c72..2ddad59 100644 --- a/src/ditto/readers/cyme/components/distribution_bus.py +++ b/src/ditto/readers/cyme/components/distribution_bus.py @@ -12,21 +12,21 @@ def __init__(self, cyme_model): cyme_file = 'Network' cyme_section = 'NODE' - def parse(self, row, from_node_sections, to_node_sections, node_feeder_map, feeder_voltage_map): + def parse(self, row, from_node_sections, to_node_sections, node_feeder_map, node_substation_map): name = self.map_name(row) feeder = node_feeder_map.get(name, None) - feeder_name = None - if feeder is not None: - feeder_name = feeder.name + substation = node_substation_map.get(name, None) + coordinate = self.map_coordinate(row) phases = self.map_phases(row, from_node_sections, to_node_sections) - rated_voltage = self.map_rated_voltage(row, phases, feeder_voltage_map.get(feeder_name)) + rated_voltage = self.map_rated_voltage(row) voltage_limits = self.map_voltagelimits(row) voltage_type = self.map_voltage_type(row) return DistributionBus.model_construct(name=name, coordinate=coordinate, rated_voltage=rated_voltage, feeder=feeder, + substation=substation, phases=phases, voltagelimits=voltage_limits, voltage_type=voltage_type) @@ -40,7 +40,7 @@ def map_coordinate(self, row): crs = None return Location(x=X, y=Y, crs=crs) - def map_rated_voltage(self, row, phases, feeder_voltage): + def map_rated_voltage(self, row): #return PositiveVoltage(float(row['UserDefinedBaseVoltage']), "kilovolts") return Voltage(float(12.47), "kilovolts") diff --git a/src/ditto/readers/cyme/components/distribution_load.py b/src/ditto/readers/cyme/components/distribution_load.py index c506ff2..5adff2a 100644 --- a/src/ditto/readers/cyme/components/distribution_load.py +++ b/src/ditto/readers/cyme/components/distribution_load.py @@ -10,28 +10,33 @@ class DistributionLoadMapper(CymeMapper): def __init__(self, system): super().__init__(system) + cyme_file = 'Load' cyme_section = 'CUSTOMER LOADS' - - def parse(self, row, section_id_sections, equipment_file): + def parse(self, row, section_id_sections, equipment_file, load_record): name = self.map_name(row) bus = self.map_bus(row, section_id_sections) phases = self.map_phases(row) equipment = self.map_equipment(row, equipment_file) - if len(list(self.system.list_components_by_name(DistributionLoad, name))) > 0: - existing_load = self.system.get_component(component_type=DistributionLoad, name=name) - existing_load.equipment.phase_loads.real_power += equipment.phase_loads.real_power - existing_load.equipment.phase_loads.reactive_power += equipment.phase_loads.reactive_power + + if load_record.get(name) is not None: + existing_load = load_record.get(name) + existing_load.equipment.phase_loads[0].real_power += equipment.phase_loads[0].real_power + existing_load.equipment.phase_loads[0].reactive_power += equipment.phase_loads[0].reactive_power return None if len(phases) == 0: logger.warning(f"Load {name} has no kW values. Skipping...") return None - return DistributionLoad.model_construct(name=name, + + load = DistributionLoad.model_construct(name=name, bus=bus, phases=phases, equipment=equipment) + load_record[name] = load + return load + def map_name(self, row): load_phase = row["LoadPhase"] diff --git a/src/ditto/readers/cyme/components/distribution_voltage_source.py b/src/ditto/readers/cyme/components/distribution_voltage_source.py index 9cad714..dd3aee4 100644 --- a/src/ditto/readers/cyme/components/distribution_voltage_source.py +++ b/src/ditto/readers/cyme/components/distribution_voltage_source.py @@ -31,6 +31,7 @@ def parse(self, row, feeder_voltage_map): return DistributionVoltageSource.model_construct(name=name, feeder=feeder, + substation=substation, bus=bus, phases=phases, equipment=equipment) @@ -50,7 +51,7 @@ def map_bus(self, row): def map_equipment(self, bus, feeder, feeder_voltage): mapper = PhaseVoltageSourceEquipmentMapper(self.system) - sources = mapper.parse(bus, feeder_voltage) + sources = mapper.parse(bus, voltage) return VoltageSourceEquipment.model_construct( name=feeder+bus.name+"-source", sources=sources diff --git a/src/ditto/readers/cyme/equipment/load_equipment.py b/src/ditto/readers/cyme/equipment/load_equipment.py index efed37f..d35b1fb 100644 --- a/src/ditto/readers/cyme/equipment/load_equipment.py +++ b/src/ditto/readers/cyme/equipment/load_equipment.py @@ -17,7 +17,7 @@ def parse(self, row, network_row): # Connection is not included in LOOADS but in CONSUMER LOADS connection_type = self.map_connection_type(row) phase_loads = self.map_phase_loads(network_row) - return LoadEquipment(name=name, + return LoadEquipment.model_construct(name=name, phase_loads=phase_loads, connection_type=connection_type) @@ -40,7 +40,6 @@ def map_connection_type(self, row): def map_phase_loads(self, row): # Get the PhaseLoadEquipment with the same name as the Load - name = row['DeviceNumber'] phase_load_equipment_mapper = PhaseLoadEquipmentMapper(self.system) phase_load_equipment = phase_load_equipment_mapper.parse(row) phase_loads = [phase_load_equipment] diff --git a/src/ditto/readers/cyme/equipment/phase_voltagesource_equipment.py b/src/ditto/readers/cyme/equipment/phase_voltagesource_equipment.py index 0a6695d..48b0f4c 100644 --- a/src/ditto/readers/cyme/equipment/phase_voltagesource_equipment.py +++ b/src/ditto/readers/cyme/equipment/phase_voltagesource_equipment.py @@ -8,17 +8,17 @@ class PhaseVoltageSourceEquipmentMapper(CymeMapper): def __init__(self, system): super().__init__(system) - def parse(self, bus, feeder_voltage): + def parse(self, bus, source_voltage): sources = [] num_phases = len(bus.phases) for i in range(num_phases): source = PhaseVoltageSourceEquipment.model_construct( - name=f"phase-source-{i+1}", + name=f"{bus.name}-phase-source-{i+1}", r0=Resistance(0.001, "ohm"), r1=Resistance(0.001, "ohm"), x0=Reactance(0.001, "ohm"), x1=Reactance(0.001, "ohm"), - voltage=feeder_voltage / 1.732 if num_phases == 3 else feeder_voltage, + voltage=source_voltage / 1.732 if num_phases >= 3 else source_voltage, voltage_type=VoltageTypes.LINE_TO_GROUND, angle=Angle(i * (360.0 / num_phases), "degree"), ) diff --git a/src/ditto/readers/cyme/reader.py b/src/ditto/readers/cyme/reader.py index 0c3834d..c50aedc 100644 --- a/src/ditto/readers/cyme/reader.py +++ b/src/ditto/readers/cyme/reader.py @@ -19,8 +19,9 @@ from gdm.distribution.components import DistributionVoltageSource from gdm.distribution.components.distribution_transformer import DistributionTransformer from gdm.quantities import Voltage -from gdm.distribution.enums import VoltageTypes +from gdm.distribution.enums import VoltageTypes, ConnectionType +from infrasys.exceptions import ISAlreadyAttached class Reader(AbstractReader): # Order of components is important @@ -45,11 +46,11 @@ class Reader(AbstractReader): validation_errors = [] - def __init__(self, network_file, equipment_file, load_file, feeder=None, load_model_id = None): + def __init__(self, network_file, equipment_file, load_file, feeders, substations, load_model_id = None): self.system = DistributionSystem(auto_add_composed_components=True) - self.read(network_file, equipment_file, load_file, feeder, load_model_id) + self.read(network_file, equipment_file, load_file, feeders, substations, load_model_id) - def read(self, network_file, equipment_file, load_file, feeder, load_model_id = None): + def read(self, network_file, equipment_file, load_file, feeders, substations, load_model_id = None): # Section data read separately as it links to other tables section_id_sections = {} @@ -67,10 +68,12 @@ def read(self, network_file, equipment_file, load_file, feeder, load_model_id = self.system.add_component(default_conductor) node_feeder_map = {} - feeder_voltage_map = {} + node_substation_map = {} + network_voltage_map = {} + load_record = {} used_sections = set() - section_data = read_cyme_data(network_file,"SECTION", node_feeder_map=node_feeder_map, feeder_voltage_map=feeder_voltage_map, parse_feeders=True) + section_data = read_cyme_data(network_file,"SECTION", node_feeder_map=node_feeder_map, network_voltage_map=network_voltage_map, node_substation_map=node_substation_map, parse_feeders=True, parse_substation=True) section_id_sections = section_data.set_index("SectionID").to_dict(orient="index") from_node_sections = section_data.groupby("FromNodeID").apply(lambda df: df.to_dict(orient="records")).to_dict() @@ -83,6 +86,7 @@ def read(self, network_file, equipment_file, load_file, feeder, load_model_id = mapper_name = component_type + "Mapper" if not hasattr(cyme_mapper, mapper_name): logger.warning(f"Mapper {mapper_name} not found. Skipping.") + mapper = getattr(cyme_mapper, mapper_name)(self.system) cyme_file = mapper.cyme_file @@ -147,8 +151,11 @@ def parse_row(row): components = [c for c in components if c is not None] components = [item for c in components for item in (c if isinstance(c, list) else [c])] self.system.add_components(*components) - if feeder is not None: - self.system = self.build_feeder(feeder) + truncated_network = None + if feeders is not None or substations is not None: + truncated_network = self.truncate_distribution_system(feeders, substations) + if truncated_network is not None: + self.system = truncated_network for component_type in self.system.get_component_types(): components = self.system.get_components(component_type) @@ -219,26 +226,48 @@ def _get_bus_connected_components( ) ) - def build_feeder(self, feeder_name): + def truncate_distribution_system(self, feeders, substations): + truncated_network = DistributionSystem(auto_add_composed_components=True) + if substations is not None: + for substation in substations: + truncated_network = self.build_network(substation, network_type='substation', network_dist_sys=truncated_network) + if feeders is not None: + for feeder in feeders: + truncated_network = self.build_network(feeder, network_type='feeder', network_dist_sys=truncated_network) + return truncated_network + + + def build_network(self, network_name, network_type='feeder', network_dist_sys=None): bus_queue = [] type_lists = {} - for component_type in self.system.get_component_types(): - type_lists[component_type] = list(self.system.get_components(component_type, filter_func=partial(filter_feeder, feeder_name=feeder_name))) + if network_type == 'feeder': + for component_type in self.system.get_component_types(): + type_lists[component_type] = list(self.system.get_components(component_type, filter_func=partial(filter_feeder, feeder_name=network_name))) + elif network_type == 'substation': + for component_type in self.system.get_component_types(): + type_lists[component_type] = list(self.system.get_components(component_type, filter_func=partial(filter_substation, substation_name=network_name))) voltage_sources = type_lists.get(DistributionVoltageSource, []) - feeder_dist_sys = DistributionSystem(auto_add_composed_components=True) + + print(voltage_sources) + for vsource in voltage_sources: - feeder_dist_sys.add_component(vsource) + vsource.bus.rated_voltage = vsource.equipment.sources[0].voltage * 1.732 if len(vsource.phases) == 3 else vsource.equipment[0].voltage bus_queue.append(vsource.bus.name) - + try: + network_dist_sys.add_component(vsource) + except: + pass + print(bus_queue) while bus_queue: current_bus_name = bus_queue.pop(0) current_bus = self.system.get_component(DistributionBus, name=current_bus_name) current_voltage = current_bus.rated_voltage + current_voltage_type = current_bus.voltage_type try: - feeder_dist_sys.add_component(current_bus) + network_dist_sys.add_component(current_bus) except: pass for component_type in self.system.get_component_types(): @@ -246,33 +275,39 @@ def build_feeder(self, feeder_name): if conn_objs: if conn_objs != []: for obj in conn_objs: + if network_dist_sys.has_component(obj): + continue if hasattr(obj, 'buses'): - for bus in obj.buses: + for j, bus in enumerate(obj.buses): if (bus.name != current_bus.name): if component_type == DistributionTransformer: for i, winding in enumerate(obj.equipment.windings): voltage = winding.rated_voltage voltage_type = winding.voltage_type - - if i > 0: - if voltage < current_voltage: + if (voltage_type == VoltageTypes.LINE_TO_GROUND) and ((voltage == Voltage(12.47, 'kilovolt')) or (voltage == Voltage(12.0, 'kilovolt')) or (voltage == Voltage(0.208, 'kilovolt'))): + print("Changing voltage type to LINE_TO_LINE",voltage, winding.connection_type) + winding.voltage_type = VoltageTypes.LINE_TO_LINE + voltage_type = winding.voltage_type + if i == j: + if voltage != current_voltage: bus.voltage_type = voltage_type bus.rated_voltage = voltage - if (not feeder_dist_sys.has_component(bus)) and (bus.name not in bus_queue): + if (not network_dist_sys.has_component(bus)) and (bus.name not in bus_queue): bus_queue.append(bus.name) else: bus.rated_voltage = current_voltage - if (not feeder_dist_sys.has_component(bus)) and (bus.name not in bus_queue): + bus.voltage_type = current_voltage_type + if (not network_dist_sys.has_component(bus)) and (bus.name not in bus_queue): bus_queue.append(bus.name) elif hasattr(obj, 'bus'): - if (obj.bus.name not in bus_queue) and (not feeder_dist_sys.has_component(obj.bus)): + if (obj.bus.name not in bus_queue) and (not network_dist_sys.has_component(obj.bus)): bus_queue.append(obj.bus.name) try: - feeder_dist_sys.add_component(obj) + network_dist_sys.add_component(obj) except: pass - return feeder_dist_sys + return network_dist_sys def filter_feeder(object, feeder_name=None): @@ -289,4 +324,21 @@ def filter_feeder(object, feeder_name=None): return False if object.buses[0].feeder.name == feeder_name and object.buses[1].feeder.name == feeder_name: return True + return False + + +def filter_substation(object, substation_name=None): + if hasattr(object, 'bus'): + if not hasattr(object.bus.substation, "name"): + return False + if object.bus.substation.name == substation_name: + return True + return False + + elif hasattr(object, 'buses'): + for bus in object.buses: + if not hasattr(bus.substation, "name"): + return False + if object.buses[0].substation.name == substation_name or object.buses[1].substation.name == substation_name: + return True return False \ No newline at end of file diff --git a/src/ditto/readers/cyme/utils.py b/src/ditto/readers/cyme/utils.py index 1a2806f..04bb2fa 100644 --- a/src/ditto/readers/cyme/utils.py +++ b/src/ditto/readers/cyme/utils.py @@ -1,14 +1,18 @@ import pandas as pd from gdm.distribution.components.distribution_feeder import DistributionFeeder +from gdm.distribution.components.distribution_substation import DistributionSubstation from gdm.quantities import Voltage -def read_cyme_data(cyme_file, cyme_section, index_col=None, node_feeder_map = None, feeder_voltage_map = None, parse_feeders=False): +def read_cyme_data(cyme_file, cyme_section, index_col=None, node_feeder_map = None, network_voltage_map = None, node_substation_map = None, parse_feeders=False, parse_substation=False): all_data = [] headers = None with open(cyme_file) as f: reading = False feeder_id = None feeder_object_map = {} + substation_id = None + substation_object_map = {} + for line in f: if line.startswith(f"[{cyme_section}]"): reading = True @@ -19,10 +23,14 @@ def read_cyme_data(cyme_file, cyme_section, index_col=None, node_feeder_map = No headers = line_header.split(",") continue elif line.startswith("FORMAT") or line.startswith("FEEDER") or line.startswith("SUBSTATION"): + if parse_substation: + if line.startswith("SUBSTATION"): + substation_id = line.split(",")[0].split("=")[1].strip() + else: + substation_id = None if parse_feeders: if line.startswith("FEEDER"): feeder_id = line.split(",")[0].split("=")[1].strip() - feeder_voltage_map[feeder_id] = None else: feeder_id = None # For SECTION Feeder headers @@ -34,14 +42,20 @@ def read_cyme_data(cyme_file, cyme_section, index_col=None, node_feeder_map = No try: line = line.strip() line_data = line.split(",") - if parse_feeders and (feeder_id is not None): - if cyme_section == 'SECTION': - node1 = line_data[1].strip() - node2 = line_data[3].strip() + if cyme_section == 'SECTION': + node1 = line_data[1].strip() + node2 = line_data[3].strip() + if parse_feeders and (feeder_id is not None): feeder = feeder_object_map.get(feeder_id, DistributionFeeder(name = feeder_id)) node_feeder_map[node1] = feeder node_feeder_map[node2] = feeder feeder_object_map[feeder_id] = feeder + if parse_substation and (substation_id is not None): + substation = substation_object_map.get(substation_id, DistributionSubstation(name = substation_id, feeders = [])) + node_substation_map[node1] = substation + node_substation_map[node2] = substation + substation_object_map[substation_id] = substation + all_data.append(line.split(",")) except: raise Exception(f"Failed to parse line: {line}") From 707668d99aeebdb9963c3439472a6684f79609d5 Mon Sep 17 00:00:00 2001 From: "Zink, Zephyr" Date: Wed, 22 Oct 2025 17:33:02 -0600 Subject: [PATCH 31/50] three winding transformers, voltage assignment rework, winding fixes --- src/ditto/readers/cyme/__init__.py | 4 +- .../components/distribution_transformer.py | 72 ++++ .../components/distribution_voltage_source.py | 17 +- .../cyme/components/matrix_impedance_fuse.py | 2 +- .../components/matrix_impedance_recloser.py | 2 +- .../components/matrix_impedance_switch.py | 2 +- .../distribution_transformer_equipment.py | 26 +- ...ion_transformer_three_winding_equipment.py | 307 ++++++++++++++++++ .../phase_voltagesource_equipment.py | 3 +- src/ditto/readers/cyme/reader.py | 204 ++++-------- src/ditto/readers/cyme/utils.py | 2 +- 11 files changed, 474 insertions(+), 167 deletions(-) create mode 100644 src/ditto/readers/cyme/equipment/distribution_transformer_three_winding_equipment.py diff --git a/src/ditto/readers/cyme/__init__.py b/src/ditto/readers/cyme/__init__.py index eecaf28..b5c0560 100644 --- a/src/ditto/readers/cyme/__init__.py +++ b/src/ditto/readers/cyme/__init__.py @@ -11,7 +11,9 @@ from ditto.readers.cyme.components.geometry_branch import GeometryBranchByPhaseMapper from ditto.readers.cyme.equipment.distribution_transformer_equipment import DistributionTransformerEquipmentMapper from ditto.readers.cyme.equipment.distribution_transformer_equipment import WindingEquipmentMapper -from ditto.readers.cyme.components.distribution_transformer import DistributionTransformerByPhaseMapper, DistributionTransformerMapper +from ditto.readers.cyme.equipment.distribution_transformer_three_winding_equipment import DistributionTransformerThreeWindingEquipmentMapper +from ditto.readers.cyme.equipment.distribution_transformer_three_winding_equipment import ThreeWindingEquipmentMapper +from ditto.readers.cyme.components.distribution_transformer import DistributionTransformerByPhaseMapper, DistributionTransformerMapper, DistributionTransformerThreeWindingMapper from ditto.readers.cyme.components.matrix_impedance_switch import MatrixImpedanceSwitchMapper from ditto.readers.cyme.equipment.matrix_impedance_switch_equipment import MatrixImpedanceSwitchEquipmentMapper from ditto.readers.cyme.components.matrix_impedance_fuse import MatrixImpedanceFuseMapper diff --git a/src/ditto/readers/cyme/components/distribution_transformer.py b/src/ditto/readers/cyme/components/distribution_transformer.py index 2e57b37..e88efdc 100644 --- a/src/ditto/readers/cyme/components/distribution_transformer.py +++ b/src/ditto/readers/cyme/components/distribution_transformer.py @@ -1,6 +1,7 @@ from loguru import logger from ditto.readers.cyme.cyme_mapper import CymeMapper from ditto.readers.cyme.equipment.distribution_transformer_equipment import DistributionTransformerEquipmentMapper +from ditto.readers.cyme.equipment.distribution_transformer_three_winding_equipment import DistributionTransformerThreeWindingEquipmentMapper from gdm.distribution.components.distribution_bus import DistributionBus from gdm.distribution.components.distribution_transformer import DistributionTransformer from gdm.distribution.equipment.distribution_transformer_equipment import DistributionTransformerEquipment @@ -150,6 +151,77 @@ def map_winding_phases(self, row, phase, equipment_row): def map_equipment(self, row, phase, equipment_row): mapper = DistributionTransformerEquipmentMapper(self.system) + if equipment_row is not None: + equipment = mapper.parse(equipment_row, row) + if equipment is not None: + return equipment + return None + + + +class DistributionTransformerThreeWindingMapper(CymeMapper): + def __init__(self, system): + super().__init__(system) + + cyme_file = 'Network' + cyme_section = 'THREE WINDING TRANSFORMER SETTING' + + def parse(self, row, used_sections, section_id_sections, equipment_data): + equipment_row = equipment_data.get(row['EqID'], None) + name = self.map_name(row) + buses = self.map_buses(row, section_id_sections) + winding_phases = self.map_winding_phases(row, section_id_sections) + equipment = self.map_equipment(row, equipment_row) + try: + used_sections.add(name) + return DistributionTransformer.model_construct(name=name, + buses=buses, + winding_phases=winding_phases, + equipment=equipment) + except Exception as e: + logger.warning(f"Failed to create DistributionTransformer {name}: {e}") + return None + + def map_name(self, row): + name = row['SectionID'] + return name + + def map_buses(self, row, section_id_sections): + section_id = str(row['SectionID']) + section = section_id_sections[section_id] + from_bus_name = section['FromNodeID'] + to_bus_name = section['ToNodeID'] + tertiary_bus = row['TertiaryNodeID'] + + from_bus = self.system.get_component(component_type=DistributionBus, name=from_bus_name) + to_bus = self.system.get_component(component_type=DistributionBus, name=to_bus_name) + + tertiary_bus = self.system.get_component(component_type=DistributionBus, name=tertiary_bus) + if tertiary_bus.phases == []: + tertiary_bus.phases = to_bus.phases + + return [from_bus, to_bus, tertiary_bus] + + def map_winding_phases(self, row, section_id_sections): + section_id = str(row['SectionID']) + section = section_id_sections[section_id] + phase = section['Phase'] + windings_list = [] + + num_windings = 3 + for i in range(num_windings): + winding_phases = [] + if 'A' in phase: + winding_phases.append(Phase.A) + if 'B' in phase: + winding_phases.append(Phase.B) + if 'C' in phase: + winding_phases.append(Phase.C) + windings_list.append(winding_phases) + return windings_list + + def map_equipment(self, row, equipment_row): + mapper = DistributionTransformerThreeWindingEquipmentMapper(self.system) if equipment_row is not None: equipment = mapper.parse(equipment_row, row) if equipment is not None: diff --git a/src/ditto/readers/cyme/components/distribution_voltage_source.py b/src/ditto/readers/cyme/components/distribution_voltage_source.py index dd3aee4..682190f 100644 --- a/src/ditto/readers/cyme/components/distribution_voltage_source.py +++ b/src/ditto/readers/cyme/components/distribution_voltage_source.py @@ -13,21 +13,18 @@ def __init__(self, cyme_model): cyme_file = 'Network' cyme_section = 'SOURCE' - def parse(self, row, feeder_voltage_map): + def parse(self, row): name = self.map_name(row) bus = self.map_bus(row) feeder = bus.feeder - if feeder is None: - return None - feeder_id = feeder.name - - feeder_voltage = float(row['OperatingVoltageA']) + substation = bus.substation + voltage = float(row['OperatingVoltageA']) - if feeder_voltage is None or feeder_voltage == '': + if voltage is None or voltage == '': return None phases = [phs for phs in bus.phases] - equipment = self.map_equipment(bus, feeder_id, feeder_voltage) + equipment = self.map_equipment(bus, voltage) return DistributionVoltageSource.model_construct(name=name, feeder=feeder, @@ -49,11 +46,11 @@ def map_bus(self, row): bus = self.system.get_component(DistributionBus, bus_name) return bus - def map_equipment(self, bus, feeder, feeder_voltage): + def map_equipment(self, bus, voltage): mapper = PhaseVoltageSourceEquipmentMapper(self.system) sources = mapper.parse(bus, voltage) return VoltageSourceEquipment.model_construct( - name=feeder+bus.name+"-source", + name=bus.name+"-source", sources=sources ) \ No newline at end of file diff --git a/src/ditto/readers/cyme/components/matrix_impedance_fuse.py b/src/ditto/readers/cyme/components/matrix_impedance_fuse.py index 48a7067..4eae8a9 100644 --- a/src/ditto/readers/cyme/components/matrix_impedance_fuse.py +++ b/src/ditto/readers/cyme/components/matrix_impedance_fuse.py @@ -65,7 +65,7 @@ def map_phases(self, row, section_id_sections): def map_is_closed(self, row, phases): is_closed = [] for phase in phases: - if row['ConnectionStatus'] == '0': + if row['NStatus'] == '0': is_closed.append(True) else: is_closed.append(False) diff --git a/src/ditto/readers/cyme/components/matrix_impedance_recloser.py b/src/ditto/readers/cyme/components/matrix_impedance_recloser.py index e90fc9a..1a52193 100644 --- a/src/ditto/readers/cyme/components/matrix_impedance_recloser.py +++ b/src/ditto/readers/cyme/components/matrix_impedance_recloser.py @@ -69,7 +69,7 @@ def map_phases(self, row, section_id_sections): def map_is_closed(self, row, phases): is_closed = [] for phase in phases: - if row['ConnectionStatus'] == '0': + if row['NStatus'] == '0': is_closed.append(True) else: is_closed.append(False) diff --git a/src/ditto/readers/cyme/components/matrix_impedance_switch.py b/src/ditto/readers/cyme/components/matrix_impedance_switch.py index 0c35b15..8bcb866 100644 --- a/src/ditto/readers/cyme/components/matrix_impedance_switch.py +++ b/src/ditto/readers/cyme/components/matrix_impedance_switch.py @@ -64,7 +64,7 @@ def map_phases(self, row, section_id_sections): def map_is_closed(self, row, phases): is_closed = [] for phase in phases: - if row['ConnectionStatus'] == '0': + if row['NStatus'] == '0': is_closed.append(True) else: is_closed.append(False) diff --git a/src/ditto/readers/cyme/equipment/distribution_transformer_equipment.py b/src/ditto/readers/cyme/equipment/distribution_transformer_equipment.py index 9d9d362..1268d26 100644 --- a/src/ditto/readers/cyme/equipment/distribution_transformer_equipment.py +++ b/src/ditto/readers/cyme/equipment/distribution_transformer_equipment.py @@ -43,10 +43,12 @@ def map_pct_no_load_loss(self, row): def map_pct_full_load_loss(self, row): # Need to compute rated current and rated resistance to compute full load loss - rated_current_sec = float(row['KVA'])/float(row['KVLLsec']) - resistance_pu = float(row['Z1'])/float((1+float(row['XR'])**2)**0.5) - resistance_sec = float(resistance_pu)*float(float(row['KVLLsec'])**2) / float(row['KVA']) - pct_full_load_loss = rated_current_sec*resistance_sec/100 + rated_current_sec = float(row['KVA']) * 1000 / (float(row['KVLLsec']) * 1000) + resistance_pu = float(row['Z1']) / 100 / ((1 + float(row['XR'])**2)**0.5) + resistance_sec = resistance_pu * (float(row['KVLLsec'])**2 * 1000) / float(row['KVA']) + + full_load_loss = rated_current_sec**2 * resistance_sec + pct_full_load_loss = 100 * full_load_loss / (float(row['KVA']) * 1000) return pct_full_load_loss def map_winding_reactances(self, row, is_center_tapped): @@ -54,8 +56,7 @@ def map_winding_reactances(self, row, is_center_tapped): if xr_ratio == 0: xr_ratio = 0.01 rx_ratio = 1/xr_ratio - reactance_pu = float(row['Z1'])/((1+rx_ratio**2)**0.5) - transformer_type = row['Type'] + reactance_pu = float(row['Z1']) / 100 / ((1+rx_ratio**2)**0.5) if is_center_tapped: winding_reactances = [reactance_pu, reactance_pu, reactance_pu] else: @@ -84,7 +85,6 @@ def map_windings(self, row, network_row, is_center_tapped): return windings def map_coupling(self, row, is_center_tapped): - transformer_type = row['Type'] if is_center_tapped: coupling = [SequencePair(0,1), SequencePair(0,2), @@ -185,14 +185,14 @@ def map_name(self, row): def map_resistance(self, row, winding_number): xr_ratio = float(row['XR']) - resistance_pu = float(row['Z1'])/((1+xr_ratio**2)**0.5) + resistance_pu = float(row['Z1']) / 100 / ((1 + xr_ratio**2)**0.5) if winding_number == 1: - resistance = resistance_pu*float(row['KVLLprim'])**2 / float(row['KVA']) + resistance = resistance_pu * float(row['KVLLprim'])**2 / float(row['KVA']) elif winding_number == 2: - resistance = resistance_pu*float(row['KVLLsec'])**2 / float(row['KVA']) + resistance = resistance_pu * float(row['KVLLsec'])**2 / float(row['KVA']) elif winding_number == 3: - resistance = resistance_pu*float(row['KVLLsec'])**2 / float(row['KVA']) - return resistance/100 + resistance = resistance_pu * float(row['KVLLsec'])**2 / float(row['KVA']) + return resistance def map_is_grounded(self, row, winding_number): @@ -228,7 +228,7 @@ def map_rated_voltage(self, row, winding_number): def map_voltage_type(self, row, rated_voltage): # This is from the CYME documentation but appears to not be entirely correct # Clearly L-L voltages still appear with a voltage type of 1 - if row["VoltageUnit"] == '1': + if row["VoltageUnit"] == '1' or row["VoltageUnit"] == "3": return VoltageTypes.LINE_TO_GROUND return VoltageTypes.LINE_TO_LINE diff --git a/src/ditto/readers/cyme/equipment/distribution_transformer_three_winding_equipment.py b/src/ditto/readers/cyme/equipment/distribution_transformer_three_winding_equipment.py new file mode 100644 index 0000000..776c4f5 --- /dev/null +++ b/src/ditto/readers/cyme/equipment/distribution_transformer_three_winding_equipment.py @@ -0,0 +1,307 @@ +from loguru import logger +from ditto.readers.cyme.cyme_mapper import CymeMapper +from gdm.distribution.equipment.distribution_transformer_equipment import DistributionTransformerEquipment +from gdm.distribution.equipment.distribution_transformer_equipment import WindingEquipment +from gdm.quantities import ActivePower, ReactivePower, Voltage +from gdm.distribution.common.sequence_pair import SequencePair +from gdm.distribution.enums import ConnectionType, VoltageTypes + +class DistributionTransformerThreeWindingEquipmentMapper(CymeMapper): + def __init__(self, system): + super().__init__(system) + + cyme_file = 'Equipment' + cyme_section = 'THREE WINDING TRANSFORMER' + + def parse(self, row, network_row): + + name = self.map_name(row) + pct_no_load_loss = self.map_pct_no_load_loss(row) + pct_full_load_loss = self.map_pct_full_load_loss(row) + is_center_tapped = self.map_is_center_tapped(row) + windings = self.map_windings(row, network_row) + winding_reactances = self.map_winding_reactances(row) + coupling_sequences = self.map_coupling(row) + + return DistributionTransformerEquipment.model_construct(name=name, + pct_no_load_loss=pct_no_load_loss, + pct_full_load_loss=pct_full_load_loss, + windings=windings, + winding_reactances=winding_reactances, + is_center_tapped=is_center_tapped, + coupling_sequences=coupling_sequences) + + def map_name(self, row): + name = row['ID'] + return name + + def map_pct_no_load_loss(self, row): + no_load_loss = float(row['NoLoadLosses']) + kva = float(row['PrimaryRatedCapacity']) + pct_no_load_loss = no_load_loss/kva*100 + return pct_no_load_loss + + def map_pct_full_load_loss(self, row): + + I1 = float(row['PrimaryRatedCapacity']) * 1000 / (float(row['PrimaryVoltage']) * 1000) + I2 = float(row['SecondaryRatedCapacity']) * 1000 / (float(row['PrimaryVoltage']) * 1000) + I3 = float(row['TertiaryRatedCapacity']) * 1000 / (float(row['PrimaryVoltage']) * 1000) + + Rpu_12 = float(row['PrimaryToSecondaryZ1']) / 100 / ((1 + float(row['PrimaryToSecondaryXR1'])**2)**0.5) + Rpu_13 = float(row['PrimaryToTertiaryZ1']) / 100 / ((1 + float(row['PrimaryToTertiaryXR1'])**2)**0.5) + Rpu_23 = float(row['SecondaryToTertiaryZ1']) / 100 / ((1 + float(row['SecondaryToTertiaryXR1'])**2)**0.5) + + R12 = Rpu_12 * (float(row['PrimaryVoltage'])**2 * 1000) / float(row['PrimaryRatedCapacity']) + R13 = Rpu_13 * (float(row['PrimaryVoltage'])**2 * 1000) / float(row['PrimaryRatedCapacity']) + R23 = Rpu_23 * (float(row['PrimaryVoltage'])**2 * 1000) / float(row['PrimaryRatedCapacity']) + + R1 = (R12 + R13 - R23)/2 + R2 = (R12 + R23 - R13)/2 + R3 = (R13 + R23 - R12)/2 + + full_load_loss = (I1**2 * R1 + I2**2 * R2 + I3**2 * R3) + + va = float(row['PrimaryRatedCapacity']) * 1000 + pct_full_load_loss = 100 * full_load_loss / va + return pct_full_load_loss + + def map_winding_reactances(self, row): + winding_reactances = [] + + xr_ratio12 = float(row['PrimaryToSecondaryXR1']) + if xr_ratio12 == 0: + xr_ratio12 = 0.01 + rx_ratio12 = 1/xr_ratio12 + reactance_pu12 = float(row['PrimaryToSecondaryZ1']) / 100 /((1+rx_ratio12**2)**0.5) + winding_reactances.append(reactance_pu12) + + xr_ratio13 = float(row['PrimaryToTertiaryXR1']) + if xr_ratio13 == 0: + xr_ratio13 = 0.01 + rx_ratio13 = 1/xr_ratio13 + reactance_pu13 = float(row['PrimaryToTertiaryZ1']) / 100 /((1+rx_ratio13**2)**0.5) + winding_reactances.append(reactance_pu13) + + xr_ratio23 = float(row['SecondaryToTertiaryXR1']) + if xr_ratio23 == 0: + xr_ratio23 = 0.01 + rx_ratio23 = 1/xr_ratio23 + reactance_pu23 = float(row['SecondaryToTertiaryZ1']) / 100 /((1+rx_ratio23**2)**0.5) + winding_reactances.append(reactance_pu23) + + return winding_reactances + + def map_is_center_tapped(self, row): + return False + + def map_windings(self, row, network_row): + windings = [] + + winding_mapper1 = ThreeWindingEquipmentMapper(self.system) + winding_1 = winding_mapper1.parse(row, network_row, winding_number=1) + windings.append(winding_1) + + winding_mapper2 = ThreeWindingEquipmentMapper(self.system) + winding_2 = winding_mapper2.parse(row, network_row, winding_number=2) + windings.append(winding_2) + + winding_mapper3 = ThreeWindingEquipmentMapper(self.system) + winding_3 = winding_mapper3.parse(row, network_row, winding_number=3) + windings.append(winding_3) + + return windings + + def map_coupling(self, row): + + coupling = [SequencePair(0,1), + SequencePair(0,2), + SequencePair(1,2)] + return coupling + +class ThreeWindingEquipmentMapper(CymeMapper): + def __init__(self, system): + super().__init__(system) + + cyme_file = 'Equipment' + cyme_section = 'THREE WINDING TRANSFORMER' + connection_map = { + 0: 'Yg', + 1: 'Y', + 2: 'Delta', + 3: 'Open Delta', + 4: 'Closed Delta', + 5: 'Zg', + 6: 'CT', + 7: 'Dg', + } + + def parse(self, row, network_row, winding_number): + name = self.map_name(row) + resistance = self.map_resistance(row, winding_number) + is_grounded = self.map_is_grounded(row, winding_number) + rated_voltage = self.map_rated_voltage(row, winding_number) + voltage_type = self.map_voltage_type(row) + rated_power = self.map_rated_power(row, winding_number) + num_phases = self.map_num_phases(row) + connection_type = self.map_connection_type(row, winding_number) + tap_positions = self.map_tap_positions(row, winding_number, network_row) + total_taps = self.map_total_taps(row) + min_tap_pu = self.min_tap_pu(row) + max_tap_pu = self.max_tap_pu(row) + return WindingEquipment.model_construct(name=name, + resistance=resistance, + is_grounded=is_grounded, + rated_voltage=rated_voltage, + voltage_type=voltage_type, + rated_power=rated_power, + num_phases=num_phases, + connection_type=connection_type, + tap_positions=tap_positions, + total_taps=total_taps, + min_tap_pu=min_tap_pu, + max_tap_pu=max_tap_pu) + + def map_name(self, row): + name = row['ID'] + return name + + def map_resistance(self, row, winding_number): + + Rpu_12 = float(row['PrimaryToSecondaryZ1']) / 100 / ((1 + float(row['PrimaryToSecondaryXR1'])**2)**0.5) + Rpu_13 = float(row['PrimaryToTertiaryZ1']) / 100 / ((1 + float(row['PrimaryToTertiaryXR1'])**2)**0.5) + Rpu_23 = float(row['SecondaryToTertiaryZ1']) / 100 / ((1 + float(row['SecondaryToTertiaryXR1'])**2)**0.5) + + R12 = Rpu_12 * (float(row['SecondaryVoltage'])**2 * 1000) / float(row['SecondaryRatedCapacity']) + R13 = Rpu_13 * (float(row['TertiaryVoltage'])**2 * 1000) / float(row['TertiaryRatedCapacity']) + R23 = Rpu_23 * (float(row['TertiaryVoltage'])**2 * 1000) / float(row['TertiaryRatedCapacity']) + + + R1 = max(0.5 * (R12 + R13 - R23), 1e-6) + R2 = max(0.5 * (R12 + R23 - R13), 1e-6) + R3 = max(0.5 * (R13 + R23 - R12), 1e-6) + + if winding_number == 1: + return R1 + elif winding_number == 2: + return R2 + elif winding_number == 3: + return R3 + + + def map_is_grounded(self, row, winding_number): + + connection_type = None + if winding_number == 1: + connection_type = row['PrimaryConnection'] + elif winding_number == 2: + connection_type = row['SecondaryConnection'] + elif winding_number == 3: + connection_type = row['TertiaryConnection'] + + winding_type = self.connection_map.get(int(connection_type), 'Y') + if 'Yg' in winding_type: + grounded = True + elif 'Dg' in winding_type: + grounded = True + elif 'Zg' in winding_type: + grounded = True + else: + grounded = False + return grounded + + def map_rated_voltage(self, row, winding_number): + + if winding_number == 1: + voltage = Voltage(float(row['PrimaryVoltage']), "kilovolt") + elif winding_number == 2: + voltage = Voltage(float(row['SecondaryVoltage']), "kilovolt") + elif winding_number == 3: + voltage = Voltage(float(row['TertiaryVoltage']), "kilovolt") + + return voltage + + def map_voltage_type(self, row): + return VoltageTypes.LINE_TO_LINE + + def map_rated_power(self, row, winding_number): + if winding_number == 1: + power = ActivePower(float(row['PrimaryRatedCapacity']), "kilowatt") + elif winding_number == 2: + power = ActivePower(float(row['SecondaryRatedCapacity']), "kilowatt") + elif winding_number == 3: + power = ActivePower(float(row['TertiaryRatedCapacity']), "kilowatt") + return power + + def map_num_phases(self, row): + num_phases = 3 + return num_phases + + def map_connection_type(self, row, winding_number): + + connection_type = None + if winding_number == 1: + connection_type = row['PrimaryConnection'] + elif winding_number == 2: + connection_type = row['SecondaryConnection'] + elif winding_number == 3: + connection_type = row['TertiaryConnection'] + + winding_type = self.connection_map.get(int(connection_type), 'Y') + if winding_type == 'Open Delta': + connection_type = 'OPEN_DELTA' + elif 'Delta' in winding_type: + connection_type = 'DELTA' + elif 'Z' in winding_type: + connection_type = 'ZIG_ZAG' + elif 'Y' in winding_type: + connection_type = 'STAR' + elif 'D' in winding_type: + connection_type = 'DELTA' + elif 'CT' == winding_type: + connection_type = 'STAR' + else: + connection_type = 'STAR' + + return ConnectionType(connection_type) + + def map_tap_positions(self, row, winding_number, network_row): + + num_phases = 3 + tap_location = network_row['LTC1_TapLocation'] + if tap_location == '': + return [0.0 for _ in range(num_phases)] + if int(tap_location) != winding_number: + return [0.0 for _ in range(num_phases)] + + + if network_row is None: + tap = 0.0 + else: + tap = network_row['LTC1_InitialTapPosition'] + tap = float(tap) / 100 + tap_positions = [] + for _ in range(1, num_phases + 1): + tap_positions.append(tap) + return tap_positions + + def map_total_taps(self, row): + taps = row['LTC1_NumberOfTaps'] + if taps == '' or taps is None: + taps = 0 + total_taps = int(taps) + return total_taps + + def min_tap_pu(self, row): + min_tap_pu = row['LTC1_MinimumRegulationRange'] + if min_tap_pu == '' or min_tap_pu is None: + return 0.9 + min_tap_pu = 1 - float(min_tap_pu)/100 + return float(min_tap_pu) + + def max_tap_pu(self, row): + max_tap_pu = row['LTC1_MaximumRegulationRange'] + if max_tap_pu == '' or max_tap_pu is None: + return 1.1 + max_tap_pu = 1 + float(max_tap_pu)/100 + return float(max_tap_pu) + diff --git a/src/ditto/readers/cyme/equipment/phase_voltagesource_equipment.py b/src/ditto/readers/cyme/equipment/phase_voltagesource_equipment.py index 48b0f4c..d89a42a 100644 --- a/src/ditto/readers/cyme/equipment/phase_voltagesource_equipment.py +++ b/src/ditto/readers/cyme/equipment/phase_voltagesource_equipment.py @@ -2,6 +2,7 @@ from gdm.distribution.equipment.phase_voltagesource_equipment import PhaseVoltageSourceEquipment from gdm.quantities import Angle, Reactance, Resistance from gdm.distribution.enums import VoltageTypes +from gdm.quantities import Voltage class PhaseVoltageSourceEquipmentMapper(CymeMapper): @@ -18,7 +19,7 @@ def parse(self, bus, source_voltage): r1=Resistance(0.001, "ohm"), x0=Reactance(0.001, "ohm"), x1=Reactance(0.001, "ohm"), - voltage=source_voltage / 1.732 if num_phases >= 3 else source_voltage, + voltage=Voltage(source_voltage, 'kilovolt'), voltage_type=VoltageTypes.LINE_TO_GROUND, angle=Angle(i * (360.0 / num_phases), "degree"), ) diff --git a/src/ditto/readers/cyme/reader.py b/src/ditto/readers/cyme/reader.py index c50aedc..88c1f65 100644 --- a/src/ditto/readers/cyme/reader.py +++ b/src/ditto/readers/cyme/reader.py @@ -13,6 +13,7 @@ from infrasys import Component from rich.table import Table from functools import partial +from collections import defaultdict from gdm.distribution.components.distribution_bus import DistributionBus @@ -26,7 +27,7 @@ class Reader(AbstractReader): # Order of components is important component_types = [ - "DistributionBus", + "DistributionBus", # First as other components connect to buses "DistributionVoltageSource", "MatrixImpedanceRecloser", "MatrixImpedanceSwitch", @@ -38,19 +39,20 @@ class Reader(AbstractReader): "GeometryBranchEquipment", "GeometryBranchByPhaseEquipment", "GeometryBranch", - "MatrixImpedanceBranch", "GeometryBranchByPhase", "DistributionTransformerByPhase", "DistributionTransformer", + "DistributionTransformerThreeWinding", + "MatrixImpedanceBranch", # This must be last as it includes a catch-all for unrecognized branches ] validation_errors = [] - def __init__(self, network_file, equipment_file, load_file, feeders, substations, load_model_id = None): + def __init__(self, network_file, equipment_file, load_file, load_model_id = None): self.system = DistributionSystem(auto_add_composed_components=True) - self.read(network_file, equipment_file, load_file, feeders, substations, load_model_id) + self.read(network_file, equipment_file, load_file, load_model_id) - def read(self, network_file, equipment_file, load_file, feeders, substations, load_model_id = None): + def read(self, network_file, equipment_file, load_file, load_model_id = None): # Section data read separately as it links to other tables section_id_sections = {} @@ -79,8 +81,6 @@ def read(self, network_file, equipment_file, load_file, feeders, substations, lo from_node_sections = section_data.groupby("FromNodeID").apply(lambda df: df.to_dict(orient="records")).to_dict() to_node_sections = section_data.groupby("ToNodeID").apply(lambda df: df.to_dict(orient="records")).to_dict() - - for component_type in self.component_types: logger.info(f"Parsing Type: {component_type}") mapper_name = component_type + "Mapper" @@ -116,9 +116,9 @@ def read(self, network_file, equipment_file, load_file, feeders, substations, lo argument_handler = { "DistributionCapacitorMapper": lambda: [section_id_sections, read_cyme_data(equipment_file, "SHUNT CAPACITOR", index_col='ID')], - "DistributionBusMapper": lambda: [from_node_sections, to_node_sections, node_feeder_map, feeder_voltage_map], - "DistributionVoltageSourceMapper": lambda: [feeder_voltage_map], - "DistributionLoadMapper": lambda: [section_id_sections, read_cyme_data(load_file, "LOADS", index_col='DeviceNumber')], + "DistributionBusMapper": lambda: [from_node_sections, to_node_sections, node_feeder_map, node_substation_map], + "DistributionVoltageSourceMapper": lambda: [], + "DistributionLoadMapper": lambda: [section_id_sections, read_cyme_data(load_file, "LOADS", index_col='DeviceNumber'), load_record], "GeometryBranchMapper": lambda: [used_sections, section_id_sections], "GeometryBranchByPhaseMapper": lambda: [used_sections, section_id_sections], "BareConductorEquipmentMapper": lambda: [], @@ -127,6 +127,7 @@ def read(self, network_file, equipment_file, load_file, feeders, substations, lo "MatrixImpedanceFuseMapper": lambda: [used_sections, section_id_sections, read_cyme_data(equipment_file, "FUSE", index_col='ID')], "MatrixImpedanceRecloserMapper": lambda: [used_sections, section_id_sections, read_cyme_data(equipment_file, "RECLOSER", index_col='ID')], "GeometryBranchByPhaseEquipmentMapper": lambda: [read_cyme_data(equipment_file,"SPACING TABLE FOR LINE", index_col='ID')], + "DistributionTransformerThreeWindingMapper": lambda: [used_sections, section_id_sections, read_cyme_data(equipment_file, "THREE WINDING TRANSFORMER", index_col='ID').to_dict("index")], "DistributionTransformerMapper": lambda: [used_sections, section_id_sections, read_cyme_data(equipment_file, "TRANSFORMER", index_col='ID').to_dict("index")], "DistributionTransformerByPhaseMapper": lambda: [used_sections, section_id_sections, read_cyme_data(equipment_file, "TRANSFORMER", index_col='ID').to_dict("index")], "MatrixImpedanceBranchMapper": lambda: [used_sections, section_id_sections, cyme_section], @@ -151,11 +152,10 @@ def parse_row(row): components = [c for c in components if c is not None] components = [item for c in components for item in (c if isinstance(c, list) else [c])] self.system.add_components(*components) - truncated_network = None - if feeders is not None or substations is not None: - truncated_network = self.truncate_distribution_system(feeders, substations) - if truncated_network is not None: - self.system = truncated_network + + + self.system = self.assign_bus_voltages(network_dist_sys=self.system) + for component_type in self.system.get_component_types(): components = self.system.get_components(component_type) @@ -182,7 +182,6 @@ def _add_components(self, components: list[Component]): ] ) - self.system.add_components(*components) def _validate_model(self): if self.validation_errors: @@ -207,138 +206,67 @@ def get_system(self) -> DistributionSystem: return self.system - - def _get_bus_connected_components( - self, type_lists, bus_name, component_type - ): - if "bus" in component_type.model_fields: - return list( - filter( - lambda x: x.bus.name == bus_name, - type_lists[component_type], - ) - ) - elif "buses" in component_type.model_fields: - return list( - filter( - lambda x: bus_name in [bus.name for bus in x.buses], - type_lists[component_type], - ) - ) - - def truncate_distribution_system(self, feeders, substations): - truncated_network = DistributionSystem(auto_add_composed_components=True) - if substations is not None: - for substation in substations: - truncated_network = self.build_network(substation, network_type='substation', network_dist_sys=truncated_network) - if feeders is not None: - for feeder in feeders: - truncated_network = self.build_network(feeder, network_type='feeder', network_dist_sys=truncated_network) - return truncated_network - - def build_network(self, network_name, network_type='feeder', network_dist_sys=None): + def assign_bus_voltages(self, network_dist_sys=None): - bus_queue = [] + bus_queue = set() + observed_buses = set() + observed_components = set() - type_lists = {} - if network_type == 'feeder': - for component_type in self.system.get_component_types(): - type_lists[component_type] = list(self.system.get_components(component_type, filter_func=partial(filter_feeder, feeder_name=network_name))) - elif network_type == 'substation': - for component_type in self.system.get_component_types(): - type_lists[component_type] = list(self.system.get_components(component_type, filter_func=partial(filter_substation, substation_name=network_name))) + bus_obj_map = defaultdict(list) + for component_type in self.system.get_component_types(): + component_list = list(self.system.get_components(component_type)) + for comp in component_list: + if hasattr(comp, "buses"): + for bus in comp.buses: + bus_obj_map[bus.name].append(comp) - voltage_sources = type_lists.get(DistributionVoltageSource, []) + voltage_sources = list(self.system.get_components(DistributionVoltageSource)) - print(voltage_sources) for vsource in voltage_sources: - vsource.bus.rated_voltage = vsource.equipment.sources[0].voltage * 1.732 if len(vsource.phases) == 3 else vsource.equipment[0].voltage - bus_queue.append(vsource.bus.name) - try: - network_dist_sys.add_component(vsource) - except: - pass - print(bus_queue) + vsource.bus.rated_voltage = vsource.equipment.sources[0].voltage * 1.732 if len(vsource.phases) > 1 else vsource.equipment[0].voltage + bus_queue.add(vsource.bus.name) while bus_queue: - current_bus_name = bus_queue.pop(0) + current_bus_name = bus_queue.pop() + if current_bus_name in observed_buses: + continue + current_bus = self.system.get_component(DistributionBus, name=current_bus_name) current_voltage = current_bus.rated_voltage current_voltage_type = current_bus.voltage_type - try: - network_dist_sys.add_component(current_bus) - except: - pass - for component_type in self.system.get_component_types(): - conn_objs = self._get_bus_connected_components(type_lists, current_bus.name, component_type) - if conn_objs: - if conn_objs != []: - for obj in conn_objs: - if network_dist_sys.has_component(obj): - continue - if hasattr(obj, 'buses'): - for j, bus in enumerate(obj.buses): - if (bus.name != current_bus.name): - if component_type == DistributionTransformer: - for i, winding in enumerate(obj.equipment.windings): - voltage = winding.rated_voltage - voltage_type = winding.voltage_type - if (voltage_type == VoltageTypes.LINE_TO_GROUND) and ((voltage == Voltage(12.47, 'kilovolt')) or (voltage == Voltage(12.0, 'kilovolt')) or (voltage == Voltage(0.208, 'kilovolt'))): - print("Changing voltage type to LINE_TO_LINE",voltage, winding.connection_type) - winding.voltage_type = VoltageTypes.LINE_TO_LINE - voltage_type = winding.voltage_type - if i == j: - if voltage != current_voltage: - bus.voltage_type = voltage_type - bus.rated_voltage = voltage - - if (not network_dist_sys.has_component(bus)) and (bus.name not in bus_queue): - bus_queue.append(bus.name) - else: - bus.rated_voltage = current_voltage - bus.voltage_type = current_voltage_type - if (not network_dist_sys.has_component(bus)) and (bus.name not in bus_queue): - bus_queue.append(bus.name) - elif hasattr(obj, 'bus'): - if (obj.bus.name not in bus_queue) and (not network_dist_sys.has_component(obj.bus)): - bus_queue.append(obj.bus.name) - try: - network_dist_sys.add_component(obj) - except: - pass - return network_dist_sys - + observed_buses.add(current_bus.name) + + conn_objs = bus_obj_map[current_bus.name] + for obj in conn_objs: + if obj.name in observed_components: + continue + observed_components.add(obj.name) + component_type = obj.__class__.__name__ + + for j, bus in enumerate(obj.buses): + if (bus.name == current_bus.name): + continue + if component_type == 'DistributionTransformer': + for i, winding in enumerate(obj.equipment.windings): + voltage = winding.rated_voltage + voltage_type = winding.voltage_type + if (voltage_type == VoltageTypes.LINE_TO_GROUND) and ((voltage == Voltage(12.47, 'kilovolt')) or (voltage == Voltage(12.0, 'kilovolt')) or (voltage == Voltage(0.208, 'kilovolt'))): + # Hacked in but no better way found yet + winding.voltage_type = VoltageTypes.LINE_TO_LINE + voltage_type = winding.voltage_type + if i == j: + if voltage != current_voltage: + bus.voltage_type = voltage_type + bus.rated_voltage = voltage + + if (not bus.name in observed_buses): + bus_queue.add(bus.name) + else: + bus.rated_voltage = current_voltage + bus.voltage_type = current_voltage_type + if (not bus.name in observed_buses): + bus_queue.add(bus.name) -def filter_feeder(object, feeder_name=None): - if hasattr(object, 'bus'): - if not hasattr(object.bus.feeder, "name"): - return False - if object.bus.feeder.name == feeder_name: - return True - return False - - elif hasattr(object, 'buses'): - for bus in object.buses: - if not hasattr(bus.feeder, "name"): - return False - if object.buses[0].feeder.name == feeder_name and object.buses[1].feeder.name == feeder_name: - return True - return False - + return network_dist_sys -def filter_substation(object, substation_name=None): - if hasattr(object, 'bus'): - if not hasattr(object.bus.substation, "name"): - return False - if object.bus.substation.name == substation_name: - return True - return False - - elif hasattr(object, 'buses'): - for bus in object.buses: - if not hasattr(bus.substation, "name"): - return False - if object.buses[0].substation.name == substation_name or object.buses[1].substation.name == substation_name: - return True - return False \ No newline at end of file diff --git a/src/ditto/readers/cyme/utils.py b/src/ditto/readers/cyme/utils.py index 04bb2fa..4b17fdf 100644 --- a/src/ditto/readers/cyme/utils.py +++ b/src/ditto/readers/cyme/utils.py @@ -1,7 +1,7 @@ import pandas as pd from gdm.distribution.components.distribution_feeder import DistributionFeeder from gdm.distribution.components.distribution_substation import DistributionSubstation -from gdm.quantities import Voltage + def read_cyme_data(cyme_file, cyme_section, index_col=None, node_feeder_map = None, network_voltage_map = None, node_substation_map = None, parse_feeders=False, parse_substation=False): all_data = [] From c5e049903397fe2bd485101eb709985f7c497c32 Mon Sep 17 00:00:00 2001 From: tarekelgindy Date: Tue, 4 Nov 2025 19:38:25 -0700 Subject: [PATCH 32/50] ensuring unique linecodes per phase --- .../cyme/components/matrix_impedance_fuse.py | 20 +++++++------------ .../components/matrix_impedance_recloser.py | 19 +++++++----------- .../components/matrix_impedance_switch.py | 19 +++++++----------- .../matrix_impedance_branch_equipment.py | 9 +++++---- .../matrix_impedance_fuse_equipment.py | 6 +++--- .../matrix_impedance_recloser_equipment.py | 6 +++--- .../matrix_impedance_switch_equipment.py | 6 +++--- 7 files changed, 35 insertions(+), 50 deletions(-) diff --git a/src/ditto/readers/cyme/components/matrix_impedance_fuse.py b/src/ditto/readers/cyme/components/matrix_impedance_fuse.py index 4eae8a9..73b9ab3 100644 --- a/src/ditto/readers/cyme/components/matrix_impedance_fuse.py +++ b/src/ditto/readers/cyme/components/matrix_impedance_fuse.py @@ -1,5 +1,5 @@ from ditto.readers.cyme.cyme_mapper import CymeMapper -from ditto.readers.cyme.equipment.matrix_impedance_fuse_equipment import MatrixImpedanceFuseEquipmentMapper +from ditto.readers.cyme.equipment.matrix_impedance_fuse_equipment import MatrixImpedanceFuseEquipment from gdm.distribution.components.matrix_impedance_fuse import MatrixImpedanceFuse from gdm.distribution.components.distribution_bus import DistributionBus from gdm.quantities import Distance @@ -12,14 +12,14 @@ def __init__(self, system): cyme_file = 'Network' cyme_section = 'FUSE SETTING' - def parse(self, row, used_sections, section_id_sections, equipment_data): + def parse(self, row, used_sections, section_id_sections): name = self.map_name(row) buses = self.map_buses(row, section_id_sections) length = self.map_length(row) phases = self.map_phases(row, section_id_sections) is_closed = self.map_is_closed(row, phases) - equipment = self.map_equipment(row, phases, equipment_data) + equipment = self.map_equipment(row, phases) used_sections.add(name) return MatrixImpedanceFuse( @@ -72,16 +72,10 @@ def map_is_closed(self, row, phases): return is_closed - def map_equipment(self, row, phases,equipment_data): - fuse_id = row['EqID'] - mapper = MatrixImpedanceFuseEquipmentMapper(self.system) - equipment_row = equipment_data.loc[fuse_id] - if equipment_row is not None: - equipment = mapper.parse(equipment_row, phases) - if equipment is not None: - return equipment - return None - + def map_equipment(self, row, phases): + fuse_id = f"{row['EqID']}_{len(phases)}" + fuse = self.system.get_component(component_type=MatrixImpedanceFuseEquipment, name=fuse_id) + return fuse diff --git a/src/ditto/readers/cyme/components/matrix_impedance_recloser.py b/src/ditto/readers/cyme/components/matrix_impedance_recloser.py index 1a52193..405b5cd 100644 --- a/src/ditto/readers/cyme/components/matrix_impedance_recloser.py +++ b/src/ditto/readers/cyme/components/matrix_impedance_recloser.py @@ -1,5 +1,5 @@ from ditto.readers.cyme.cyme_mapper import CymeMapper -from ditto.readers.cyme.equipment.matrix_impedance_recloser_equipment import MatrixImpedanceRecloserEquipmentMapper +from ditto.readers.cyme.equipment.matrix_impedance_recloser_equipment import MatrixImpedanceRecloserEquipment from gdm.distribution.components.matrix_impedance_recloser import MatrixImpedanceRecloser from gdm.distribution.components.distribution_bus import DistributionBus from gdm.distribution.controllers.distribution_recloser_controller import DistributionRecloserController @@ -13,7 +13,7 @@ def __init__(self, system): cyme_file = 'Network' cyme_section = 'RECLOSER SETTING' - def parse(self, row, used_sections, section_id_sections, equipment_data): + def parse(self, row, used_sections, section_id_sections): name = self.map_name(row) buses = self.map_buses(row, section_id_sections) @@ -21,7 +21,7 @@ def parse(self, row, used_sections, section_id_sections, equipment_data): phases = self.map_phases(row, section_id_sections) is_closed = self.map_is_closed(row, phases) controller = self.map_controller(row) - equipment = self.map_equipment(row, phases, equipment_data) + equipment = self.map_equipment(row, phases) used_sections.add(name) @@ -79,15 +79,10 @@ def map_controller(self, row): return DistributionRecloserController.example() - def map_equipment(self, row, phases,equipment_data): - recloser_id = row['EqID'] - mapper = MatrixImpedanceRecloserEquipmentMapper(self.system) - equipment_row = equipment_data.loc[recloser_id] - if equipment_row is not None: - equipment = mapper.parse(equipment_row, phases) - if equipment is not None: - return equipment - return None + def map_equipment(self, row, phases): + recloser_id = f"{row['EqID']}_{len(phases)}" + recloser = self.system.get_component(component_type=MatrixImpedanceRecloserEquipment, name=recloser_id) + return recloser diff --git a/src/ditto/readers/cyme/components/matrix_impedance_switch.py b/src/ditto/readers/cyme/components/matrix_impedance_switch.py index 8bcb866..c2d9909 100644 --- a/src/ditto/readers/cyme/components/matrix_impedance_switch.py +++ b/src/ditto/readers/cyme/components/matrix_impedance_switch.py @@ -1,5 +1,5 @@ from ditto.readers.cyme.cyme_mapper import CymeMapper -from ditto.readers.cyme.equipment.matrix_impedance_switch_equipment import MatrixImpedanceSwitchEquipmentMapper +from ditto.readers.cyme.equipment.matrix_impedance_switch_equipment import MatrixImpedanceSwitchEquipment from gdm.distribution.components.matrix_impedance_switch import MatrixImpedanceSwitch from gdm.distribution.components.distribution_bus import DistributionBus from gdm.quantities import Distance @@ -12,14 +12,14 @@ def __init__(self, system): cyme_file = 'Network' cyme_section = 'SWITCH SETTING' - def parse(self, row, used_sections, section_id_sections, equipment_data): + def parse(self, row, used_sections, section_id_sections): name = self.map_name(row) buses = self.map_buses(row, section_id_sections) length = self.map_length(row) phases = self.map_phases(row, section_id_sections) is_closed = self.map_is_closed(row, phases) - equipment = self.map_equipment(row, phases, equipment_data) + equipment = self.map_equipment(row, phases) used_sections.add(name) return MatrixImpedanceSwitch( name=name, @@ -71,15 +71,10 @@ def map_is_closed(self, row, phases): return is_closed - def map_equipment(self, row, phases,equipment_data): - switch_id = row['EqID'] - mapper = MatrixImpedanceSwitchEquipmentMapper(self.system) - equipment_row = equipment_data.loc[switch_id] - if equipment_row is not None: - equipment = mapper.parse(equipment_row, phases) - if equipment is not None: - return equipment - return None + def map_equipment(self, row, phases): + switch_id = f"{row['EqID']}_{len(phases)}" + switch = self.system.get_component(component_type=MatrixImpedanceSwitchEquipment, name=switch_id) + return switch diff --git a/src/ditto/readers/cyme/equipment/matrix_impedance_branch_equipment.py b/src/ditto/readers/cyme/equipment/matrix_impedance_branch_equipment.py index 1db6335..94eb6b0 100644 --- a/src/ditto/readers/cyme/equipment/matrix_impedance_branch_equipment.py +++ b/src/ditto/readers/cyme/equipment/matrix_impedance_branch_equipment.py @@ -34,10 +34,11 @@ def _sequence_impedance_to_phase_impedance_matrix(self, r1, r0, phases=3): return R def parse(self, row, phases): - name = self.map_name(row, phases) - r_matrix = self.map_r_matrix(row, phases) - x_matrix = self.map_x_matrix(row, phases) - c_matrix = self.map_c_matrix(row, phases) + num_phases = len(phases) + name = self.map_name(row, num_phases) + r_matrix = self.map_r_matrix(row, num_phases) + x_matrix = self.map_x_matrix(row, num_phases) + c_matrix = self.map_c_matrix(row, num_phases) ampacity = self.map_ampacity(row) try: return MatrixImpedanceBranchEquipment(name=name, diff --git a/src/ditto/readers/cyme/equipment/matrix_impedance_fuse_equipment.py b/src/ditto/readers/cyme/equipment/matrix_impedance_fuse_equipment.py index 7030710..69d80cc 100644 --- a/src/ditto/readers/cyme/equipment/matrix_impedance_fuse_equipment.py +++ b/src/ditto/readers/cyme/equipment/matrix_impedance_fuse_equipment.py @@ -14,7 +14,7 @@ def __init__(self, system): cyme_section = 'FUSE' def parse(self, row, phases): - name = self.map_name(row) + name = self.map_name(row, phases) delay = self.map_delay(row) tcc_curve = self.map_tcc_curve(row) r_matrix = self.map_r_matrix(phases) @@ -33,8 +33,8 @@ def parse(self, row, phases): ampacity=ampacity ) - def map_name(self, row): - return row['ID'] + def map_name(self, row, phases): + return f"{row['ID']}_{len(phases)}" def map_r_matrix(self, phases): default_matrix = [ diff --git a/src/ditto/readers/cyme/equipment/matrix_impedance_recloser_equipment.py b/src/ditto/readers/cyme/equipment/matrix_impedance_recloser_equipment.py index 2ef2180..f2ad7c9 100644 --- a/src/ditto/readers/cyme/equipment/matrix_impedance_recloser_equipment.py +++ b/src/ditto/readers/cyme/equipment/matrix_impedance_recloser_equipment.py @@ -11,7 +11,7 @@ def __init__(self, system): cyme_section = 'RECLOSER' def parse(self, row, phases): - name = self.map_name(row) + name = self.map_name(row, phases) r_matrix = self.map_r_matrix(phases) x_matrix = self.map_x_matrix(phases) c_matrix = self.map_c_matrix(phases) @@ -26,8 +26,8 @@ def parse(self, row, phases): ampacity=ampacity ) - def map_name(self, row): - return row['ID'] + def map_name(self, row, phases): + return f"{row['ID']}_{len(phases)}" def map_r_matrix(self, phases): default_matrix = [ diff --git a/src/ditto/readers/cyme/equipment/matrix_impedance_switch_equipment.py b/src/ditto/readers/cyme/equipment/matrix_impedance_switch_equipment.py index c55de43..965e6f7 100644 --- a/src/ditto/readers/cyme/equipment/matrix_impedance_switch_equipment.py +++ b/src/ditto/readers/cyme/equipment/matrix_impedance_switch_equipment.py @@ -11,7 +11,7 @@ def __init__(self, system): cyme_section = 'SWITCH' def parse(self, row, phases): - name = self.map_name(row) + name = self.map_name(row, phases) r_matrix = self.map_r_matrix(phases) x_matrix = self.map_x_matrix(phases) c_matrix = self.map_c_matrix(phases) @@ -26,8 +26,8 @@ def parse(self, row, phases): ampacity=ampacity ) - def map_name(self, row): - return row['ID'] + def map_name(self, row, phases): + return f"{row['ID']}_{len(phases)}" def map_r_matrix(self, phases): default_matrix = [ From c97b84eedeb903567a0617d4e6fc0444294fa2f3 Mon Sep 17 00:00:00 2001 From: tarekelgindy Date: Tue, 4 Nov 2025 19:39:20 -0700 Subject: [PATCH 33/50] fixes for missing loads --- src/ditto/readers/cyme/components/distribution_bus.py | 5 ++++- src/ditto/readers/cyme/components/distribution_load.py | 3 +++ src/ditto/readers/cyme/equipment/load_equipment.py | 4 ++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/ditto/readers/cyme/components/distribution_bus.py b/src/ditto/readers/cyme/components/distribution_bus.py index 2ddad59..cc7322d 100644 --- a/src/ditto/readers/cyme/components/distribution_bus.py +++ b/src/ditto/readers/cyme/components/distribution_bus.py @@ -36,7 +36,10 @@ def map_name(self, row): return name def map_coordinate(self, row): - X, Y = float(row["CoordX"]), float(row["CoordY"]) + try: + X, Y = float(row["CoordX"]), float(row["CoordY"]) + except: + X, Y = float(row["CoordX1"]), float(row["CoordY1"]) crs = None return Location(x=X, y=Y, crs=crs) diff --git a/src/ditto/readers/cyme/components/distribution_load.py b/src/ditto/readers/cyme/components/distribution_load.py index 5adff2a..2f579ef 100644 --- a/src/ditto/readers/cyme/components/distribution_load.py +++ b/src/ditto/readers/cyme/components/distribution_load.py @@ -16,9 +16,12 @@ def __init__(self, system): def parse(self, row, section_id_sections, equipment_file, load_record): name = self.map_name(row) + bus = self.map_bus(row, section_id_sections) phases = self.map_phases(row) equipment = self.map_equipment(row, equipment_file) + if equipment is None: + return None if load_record.get(name) is not None: existing_load = load_record.get(name) diff --git a/src/ditto/readers/cyme/equipment/load_equipment.py b/src/ditto/readers/cyme/equipment/load_equipment.py index d35b1fb..cb19d4d 100644 --- a/src/ditto/readers/cyme/equipment/load_equipment.py +++ b/src/ditto/readers/cyme/equipment/load_equipment.py @@ -17,6 +17,8 @@ def parse(self, row, network_row): # Connection is not included in LOOADS but in CONSUMER LOADS connection_type = self.map_connection_type(row) phase_loads = self.map_phase_loads(network_row) + if any(pl is None for pl in phase_loads): + return None return LoadEquipment.model_construct(name=name, phase_loads=phase_loads, connection_type=connection_type) @@ -56,6 +58,8 @@ def parse(self, row): name = self.map_name(row) real_power = self.map_real_power(row) reactive_power = self.map_reactive_power(row) + if real_power ==0 and reactive_power ==0: + return None z_real = self.map_z_real(row) z_imag = self.map_z_imag(row) i_real = self.map_i_real(row) From 3095da2a7c8a04e7f4c57f65853ce6e7873df9f4 Mon Sep 17 00:00:00 2001 From: tarekelgindy Date: Tue, 4 Nov 2025 19:42:40 -0700 Subject: [PATCH 34/50] updating reader for unique linecodes --- src/ditto/readers/cyme/reader.py | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/src/ditto/readers/cyme/reader.py b/src/ditto/readers/cyme/reader.py index 88c1f65..1a68e74 100644 --- a/src/ditto/readers/cyme/reader.py +++ b/src/ditto/readers/cyme/reader.py @@ -29,8 +29,11 @@ class Reader(AbstractReader): component_types = [ "DistributionBus", # First as other components connect to buses "DistributionVoltageSource", + "MatrixImpedanceRecloserEquipment", "MatrixImpedanceRecloser", + "MatrixImpedanceSwitchEquipment", "MatrixImpedanceSwitch", + "MatrixImpedanceFuseEquipment", "MatrixImpedanceFuse", "DistributionCapacitor", "DistributionLoad", @@ -58,6 +61,12 @@ def read(self, network_file, equipment_file, load_file, load_model_id = None): section_id_sections = {} from_node_sections = {} to_node_sections = {} + phase_elements = set([ + "MatrixImpedanceBranchEquipmentMapper", + "MatrixImpedanceRecloserEquipmentMapper", + "MatrixImpedanceSwitchEquipmentMapper", + "MatrixImpedanceFuseEquipmentMapper", + ]) default_conductor = BareConductorEquipment( name="Default", conductor_diameter=Distance(0.368000,'inch').to('mm'), @@ -123,15 +132,18 @@ def read(self, network_file, equipment_file, load_file, load_model_id = None): "GeometryBranchByPhaseMapper": lambda: [used_sections, section_id_sections], "BareConductorEquipmentMapper": lambda: [], "GeometryBranchEquipmentMapper": lambda: [read_cyme_data(equipment_file,"SPACING TABLE FOR LINE", index_col='ID')], - "MatrixImpedanceSwitchMapper": lambda: [used_sections, section_id_sections, read_cyme_data(equipment_file, "SWITCH", index_col='ID')], - "MatrixImpedanceFuseMapper": lambda: [used_sections, section_id_sections, read_cyme_data(equipment_file, "FUSE", index_col='ID')], - "MatrixImpedanceRecloserMapper": lambda: [used_sections, section_id_sections, read_cyme_data(equipment_file, "RECLOSER", index_col='ID')], + "MatrixImpedanceSwitchEquipmentMapper": lambda: [], + "MatrixImpedanceSwitchMapper": lambda: [used_sections, section_id_sections], + "MatrixImpedanceFuseEquipmentMapper": lambda: [], + "MatrixImpedanceFuseMapper": lambda: [used_sections, section_id_sections], + "MatrixImpedanceRecloserEquipmentMapper": lambda: [], + "MatrixImpedanceRecloserMapper": lambda: [used_sections, section_id_sections], "GeometryBranchByPhaseEquipmentMapper": lambda: [read_cyme_data(equipment_file,"SPACING TABLE FOR LINE", index_col='ID')], "DistributionTransformerThreeWindingMapper": lambda: [used_sections, section_id_sections, read_cyme_data(equipment_file, "THREE WINDING TRANSFORMER", index_col='ID').to_dict("index")], "DistributionTransformerMapper": lambda: [used_sections, section_id_sections, read_cyme_data(equipment_file, "TRANSFORMER", index_col='ID').to_dict("index")], "DistributionTransformerByPhaseMapper": lambda: [used_sections, section_id_sections, read_cyme_data(equipment_file, "TRANSFORMER", index_col='ID').to_dict("index")], + "MatrixImpedanceBranchEquipmentMapper": lambda: [], "MatrixImpedanceBranchMapper": lambda: [used_sections, section_id_sections, cyme_section], - "MatrixImpedanceBranchEquipmenthMapper": lambda: [], } args = argument_handler.get(mapper_name, lambda: [])() @@ -140,8 +152,10 @@ def parse_row(row): model_entry = mapper.parse(row, *args) return model_entry - if mapper_name == 'MatrixImpedanceBranchEquipmentMapper': - for phases in range(1,4): + if mapper_name in phase_elements: + phases = [] + for phase in ['A','B','C']: + phases.append(phase) args = [phases] components = data.apply(parse_row, axis=1) components = [c for c in components if c is not None] @@ -153,7 +167,6 @@ def parse_row(row): components = [item for c in components for item in (c if isinstance(c, list) else [c])] self.system.add_components(*components) - self.system = self.assign_bus_voltages(network_dist_sys=self.system) From 576b7db84d7f1c9960397a39e1eeffb954239da7 Mon Sep 17 00:00:00 2001 From: tarekelgindy Date: Tue, 4 Nov 2025 19:47:14 -0700 Subject: [PATCH 35/50] removing . values from opendss outputs --- .../writers/opendss/components/distribution_branch.py | 6 +++--- src/ditto/writers/opendss/components/distribution_bus.py | 2 +- .../writers/opendss/components/distribution_capacitor.py | 4 ++-- src/ditto/writers/opendss/components/distribution_load.py | 4 ++-- .../opendss/components/distribution_transformer.py | 8 ++++---- .../writers/opendss/components/distribution_vsource.py | 4 ++-- src/ditto/writers/opendss/components/geometry_branch.py | 2 +- .../writers/opendss/components/matrix_impedance_branch.py | 2 +- .../writers/opendss/components/matrix_impedance_fuse.py | 2 +- .../opendss/components/matrix_impedance_recloser.py | 2 +- .../writers/opendss/components/matrix_impedance_switch.py | 2 +- .../opendss/components/sequence_impedance_branch.py | 2 +- .../writers/opendss/equipment/bare_conductor_equipment.py | 2 +- .../opendss/equipment/concentric_cable_equipment.py | 2 +- .../equipment/distribution_transformer_equipment.py | 2 +- .../opendss/equipment/geometry_branch_equipment.py | 4 ++-- .../equipment/matrix_impedance_branch_equipment.py | 2 +- .../equipment/sequence_impedance_branch_equipment.py | 2 +- 18 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/ditto/writers/opendss/components/distribution_branch.py b/src/ditto/writers/opendss/components/distribution_branch.py index 3825721..8f2ce8c 100644 --- a/src/ditto/writers/opendss/components/distribution_branch.py +++ b/src/ditto/writers/opendss/components/distribution_branch.py @@ -13,11 +13,11 @@ def __init__(self, model): opendss_file = OpenDSSFileTypes.LINES_FILE.value def map_name(self): - self.opendss_dict["Name"] = self.model.name.replace(" ","_") + self.opendss_dict["Name"] = self.model.name.replace(" ","_").replace(".","_") def map_buses(self): - self.opendss_dict["Bus1"] = self.model.buses[0].name.replace(" ","_") - self.opendss_dict["Bus2"] = self.model.buses[1].name.replace(" ","_") + self.opendss_dict["Bus1"] = self.model.buses[0].name.replace(" ","_").replace(".","_") + self.opendss_dict["Bus2"] = self.model.buses[1].name.replace(" ","_").replace(".","_") for phase in self.model.phases: if phase != Phase.N: self.opendss_dict["Bus1"] += self.phase_map[phase] diff --git a/src/ditto/writers/opendss/components/distribution_bus.py b/src/ditto/writers/opendss/components/distribution_bus.py index 023cf09..24b99bb 100644 --- a/src/ditto/writers/opendss/components/distribution_bus.py +++ b/src/ditto/writers/opendss/components/distribution_bus.py @@ -11,7 +11,7 @@ def __init__(self, model): opendss_file = OpenDSSFileTypes.COORDINATE_FILE.value def map_name(self): - self.opendss_dict["Name"] = self.model.name.replace(" ", "_") + self.opendss_dict["Name"] = self.model.name.replace(" ", "_").replace('.', '_') def map_coordinate(self): if hasattr(self.model.coordinate, "x"): diff --git a/src/ditto/writers/opendss/components/distribution_capacitor.py b/src/ditto/writers/opendss/components/distribution_capacitor.py index 121dd49..0a196b3 100644 --- a/src/ditto/writers/opendss/components/distribution_capacitor.py +++ b/src/ditto/writers/opendss/components/distribution_capacitor.py @@ -13,10 +13,10 @@ def __init__(self, model): opendss_file = OpenDSSFileTypes.CAPACITORS_FILE.value def map_name(self): - self.opendss_dict["Name"] = self.model.name.replace(" ", "_") + self.opendss_dict["Name"] = self.model.name.replace(" ", "_").replace(".", "_") def map_bus(self): - self.opendss_dict["Bus1"] = self.model.bus.name.replace(" ","_") + self.opendss_dict["Bus1"] = self.model.bus.name.replace(" ","_").replace(".","_") num_phases = len(self.model.phases) for phase in self.model.phases: self.opendss_dict["Bus1"] += self.phase_map[phase] diff --git a/src/ditto/writers/opendss/components/distribution_load.py b/src/ditto/writers/opendss/components/distribution_load.py index 1a6608f..a954b08 100644 --- a/src/ditto/writers/opendss/components/distribution_load.py +++ b/src/ditto/writers/opendss/components/distribution_load.py @@ -15,12 +15,12 @@ def __init__(self, model): opendss_file = OpenDSSFileTypes.LOADS_FILE.value def map_name(self): - self.opendss_dict["Name"] = self.model.name.replace(" ","_") + self.opendss_dict["Name"] = self.model.name.replace(" ","_").replace(".","_") # TODO: Want to set the Yearly attribute here, but need to access the system. Is that possible? def map_bus(self): num_phases = len(self.model.phases) - self.opendss_dict["Bus1"] = self.model.bus.name.replace(" ","_") + self.opendss_dict["Bus1"] = self.model.bus.name.replace(" ","_").replace(".","_") for phase in self.model.phases: self.opendss_dict["Bus1"] += self.phase_map[phase] # TODO: Should we include the phases its connected to here? diff --git a/src/ditto/writers/opendss/components/distribution_transformer.py b/src/ditto/writers/opendss/components/distribution_transformer.py index 42cd213..fb47418 100644 --- a/src/ditto/writers/opendss/components/distribution_transformer.py +++ b/src/ditto/writers/opendss/components/distribution_transformer.py @@ -11,7 +11,7 @@ def __init__(self, model): opendss_file = OpenDSSFileTypes.TRANSFORMERS_FILE.value def map_name(self): - self.opendss_dict["Name"] = self.model.name.replace(" ","_") + self.opendss_dict["Name"] = self.model.name.replace(" ","_").replace(".","_") def map_buses(self): buses = [] @@ -21,7 +21,7 @@ def map_buses(self): if is_center_tapped: for i in range(len(self.model.buses)): bus = self.model.buses[i] - buses.append(bus.name.replace(" ","_")) + buses.append(bus.name.replace(" ","_").replace(".","_")) dss_phases = "" for phase in self.model.winding_phases[0]: dss_phases += self.phase_map[phase] @@ -31,7 +31,7 @@ def map_buses(self): else: for bus in self.model.buses: - buses.append(bus.name.replace(" ","_")) + buses.append(bus.name.replace(" ","_").replace(".","_")) for winding_phases in self.model.winding_phases: dss_phases = "" for phase in winding_phases: @@ -48,4 +48,4 @@ def map_winding_phases(self): def map_equipment(self): equipment = self.model.equipment - self.opendss_dict["XfmrCode"] = equipment.name.replace(" ","_") + self.opendss_dict["XfmrCode"] = equipment.name.replace(" ","_").replace(".","_") diff --git a/src/ditto/writers/opendss/components/distribution_vsource.py b/src/ditto/writers/opendss/components/distribution_vsource.py index ca7d499..c9059dd 100644 --- a/src/ditto/writers/opendss/components/distribution_vsource.py +++ b/src/ditto/writers/opendss/components/distribution_vsource.py @@ -13,10 +13,10 @@ def __init__(self, model): opendss_file = OpenDSSFileTypes.MASTER_FILE.value def map_name(self): - self.opendss_dict["Name"] = self.model.name.replace(" ", "_") + self.opendss_dict["Name"] = self.model.name.replace(" ", "_").replace(".", "_") def map_bus(self): - self.opendss_dict["Bus1"] = self.model.bus.name.replace(" ","_") + self.opendss_dict["Bus1"] = self.model.bus.name.replace(" ","_").replace(".", "_") for phase in self.model.phases: self.opendss_dict["Bus1"] += self.phase_map[phase] diff --git a/src/ditto/writers/opendss/components/geometry_branch.py b/src/ditto/writers/opendss/components/geometry_branch.py index e341ee3..82a1f78 100644 --- a/src/ditto/writers/opendss/components/geometry_branch.py +++ b/src/ditto/writers/opendss/components/geometry_branch.py @@ -11,4 +11,4 @@ def __init__(self, model): opendss_file = OpenDSSFileTypes.LINES_FILE.value def map_equipment(self): - self.opendss_dict["Geometry"] = self.model.equipment.name.replace(" ", "_") + self.opendss_dict["Geometry"] = self.model.equipment.name.replace(" ", "_").replace(".", "_") diff --git a/src/ditto/writers/opendss/components/matrix_impedance_branch.py b/src/ditto/writers/opendss/components/matrix_impedance_branch.py index dea0b11..3cec475 100644 --- a/src/ditto/writers/opendss/components/matrix_impedance_branch.py +++ b/src/ditto/writers/opendss/components/matrix_impedance_branch.py @@ -11,4 +11,4 @@ def __init__(self, model): opendss_file = OpenDSSFileTypes.LINES_FILE.value def map_equipment(self): - self.opendss_dict["LineCode"] = self.model.equipment.name.replace(" ", "_") + self.opendss_dict["LineCode"] = self.model.equipment.name.replace(" ", "_").replace(".", "_") diff --git a/src/ditto/writers/opendss/components/matrix_impedance_fuse.py b/src/ditto/writers/opendss/components/matrix_impedance_fuse.py index b923a7f..4d671ca 100644 --- a/src/ditto/writers/opendss/components/matrix_impedance_fuse.py +++ b/src/ditto/writers/opendss/components/matrix_impedance_fuse.py @@ -11,7 +11,7 @@ def __init__(self, model): opendss_file = OpenDSSFileTypes.FUSE_FILE.value def map_equipment(self): - self.opendss_dict["LineCode"] = self.model.equipment.name.replace(" ", "_") + self.opendss_dict["LineCode"] = self.model.equipment.name.replace(" ", "_").replace(".", "_") def map_is_closed(self): # Require every phase to be enabled for the OpenDSS line to be enabled. diff --git a/src/ditto/writers/opendss/components/matrix_impedance_recloser.py b/src/ditto/writers/opendss/components/matrix_impedance_recloser.py index 39ac2ac..22125a0 100644 --- a/src/ditto/writers/opendss/components/matrix_impedance_recloser.py +++ b/src/ditto/writers/opendss/components/matrix_impedance_recloser.py @@ -11,7 +11,7 @@ def __init__(self, model): opendss_file = OpenDSSFileTypes.RECLOSER_FILE.value def map_equipment(self): - self.opendss_dict["LineCode"] = self.model.equipment.name.replace(" ", "_") + self.opendss_dict["LineCode"] = self.model.equipment.name.replace(" ", "_").replace(".", "_") def map_is_closed(self): # Require every phase to be enabled for the OpenDSS line to be enabled. diff --git a/src/ditto/writers/opendss/components/matrix_impedance_switch.py b/src/ditto/writers/opendss/components/matrix_impedance_switch.py index fa2cfe9..837e62f 100644 --- a/src/ditto/writers/opendss/components/matrix_impedance_switch.py +++ b/src/ditto/writers/opendss/components/matrix_impedance_switch.py @@ -11,7 +11,7 @@ def __init__(self, model): opendss_file = OpenDSSFileTypes.SWITCH_FILE.value def map_equipment(self): - self.opendss_dict["LineCode"] = self.model.equipment.name.replace(" ", "_") + self.opendss_dict["LineCode"] = self.model.equipment.name.replace(" ", "_").replace(".", "_") def map_is_closed(self): # Require every phase to be enabled for the OpenDSS line to be enabled. diff --git a/src/ditto/writers/opendss/components/sequence_impedance_branch.py b/src/ditto/writers/opendss/components/sequence_impedance_branch.py index f6556f3..98c12dd 100644 --- a/src/ditto/writers/opendss/components/sequence_impedance_branch.py +++ b/src/ditto/writers/opendss/components/sequence_impedance_branch.py @@ -11,4 +11,4 @@ def __init__(self, model): opendss_file = OpenDSSFileTypes.LINES_FILE.value def map_equipment(self): - self.opendss_dict["LineCode"] = self.model.equipment.name.replace(" ", "_") + self.opendss_dict["LineCode"] = self.model.equipment.name.replace(" ", "_").replace(".", "_") diff --git a/src/ditto/writers/opendss/equipment/bare_conductor_equipment.py b/src/ditto/writers/opendss/equipment/bare_conductor_equipment.py index f8ec14e..7a7cf06 100644 --- a/src/ditto/writers/opendss/equipment/bare_conductor_equipment.py +++ b/src/ditto/writers/opendss/equipment/bare_conductor_equipment.py @@ -11,7 +11,7 @@ def __init__(self, model): opendss_file = OpenDSSFileTypes.WIRES_FILE.value def map_name(self): - self.opendss_dict["Name"] = self.model.name.replace(" ","_") + self.opendss_dict["Name"] = self.model.name.replace(" ","_").replace(".","_") def map_conductor_diameter(self): self.opendss_dict["Radius"] = self.model.conductor_diameter.magnitude / 2 diff --git a/src/ditto/writers/opendss/equipment/concentric_cable_equipment.py b/src/ditto/writers/opendss/equipment/concentric_cable_equipment.py index 3a5a605..f6b8b59 100644 --- a/src/ditto/writers/opendss/equipment/concentric_cable_equipment.py +++ b/src/ditto/writers/opendss/equipment/concentric_cable_equipment.py @@ -11,7 +11,7 @@ def __init__(self, model): opendss_file = OpenDSSFileTypes.WIRES_FILE.value def map_name(self): - self.opendss_dict["Name"] = self.model.name.replace(" ","_") + self.opendss_dict["Name"] = self.model.name.replace(" ","_").replace(".","_") def map_strand_diameter(self): self.opendss_dict["DiaStrand"] = self.model.strand_diameter.magnitude diff --git a/src/ditto/writers/opendss/equipment/distribution_transformer_equipment.py b/src/ditto/writers/opendss/equipment/distribution_transformer_equipment.py index 3171b8f..5023535 100644 --- a/src/ditto/writers/opendss/equipment/distribution_transformer_equipment.py +++ b/src/ditto/writers/opendss/equipment/distribution_transformer_equipment.py @@ -11,7 +11,7 @@ def __init__(self, model): opendss_file = OpenDSSFileTypes.TRANSFORMERS_FILE.value def map_name(self): - self.opendss_dict["Name"] = self.model.name.replace(" ","_") + self.opendss_dict["Name"] = self.model.name.replace(" ","_").replace(".","_") def map_pct_no_load_loss(self): self.opendss_dict["pctNoLoadLoss"] = self.model.pct_no_load_loss diff --git a/src/ditto/writers/opendss/equipment/geometry_branch_equipment.py b/src/ditto/writers/opendss/equipment/geometry_branch_equipment.py index 0047669..f9200b1 100644 --- a/src/ditto/writers/opendss/equipment/geometry_branch_equipment.py +++ b/src/ditto/writers/opendss/equipment/geometry_branch_equipment.py @@ -14,7 +14,7 @@ def __init__(self, model): opendss_file = OpenDSSFileTypes.LINECODES_FILE.value def map_name(self): - self.opendss_dict["Name"] = self.model.name.replace(" ","_") + self.opendss_dict["Name"] = self.model.name.replace(" ","_").replace(".","_") def map_common(self): units = [] @@ -55,5 +55,5 @@ def map_conductors(self): # conductor_type = 'tsdata' else: raise ValueError(f"Unknown conductor type {conductor}") - all_conductors.append(f"{conductor_type}.{conductor.name.replace(' ','_')}") + all_conductors.append(f"{conductor_type}.{conductor.name.replace(' ','_').replace('.','_')}") self.opendss_dict["Conductors"] = all_conductors diff --git a/src/ditto/writers/opendss/equipment/matrix_impedance_branch_equipment.py b/src/ditto/writers/opendss/equipment/matrix_impedance_branch_equipment.py index caaae65..c230568 100644 --- a/src/ditto/writers/opendss/equipment/matrix_impedance_branch_equipment.py +++ b/src/ditto/writers/opendss/equipment/matrix_impedance_branch_equipment.py @@ -19,7 +19,7 @@ def map_common(self): self.opendss_dict["Units"] = "km" def map_name(self): - self.opendss_dict["Name"] = self.model.name.replace(" ", "_") + self.opendss_dict["Name"] = self.model.name.replace(" ", "_").replace(".", "_") def map_r_matrix(self): r_matrix_ohms = self.model.r_matrix.to("ohm/km") diff --git a/src/ditto/writers/opendss/equipment/sequence_impedance_branch_equipment.py b/src/ditto/writers/opendss/equipment/sequence_impedance_branch_equipment.py index 1ca57a1..e5d1e4d 100644 --- a/src/ditto/writers/opendss/equipment/sequence_impedance_branch_equipment.py +++ b/src/ditto/writers/opendss/equipment/sequence_impedance_branch_equipment.py @@ -11,7 +11,7 @@ def __init__(self, model): opendss_file = OpenDSSFileTypes.LINECODES_FILE.value def map_name(self): - self.opendss_dict["Name"] = self.model.name.replace(" ", "_") + self.opendss_dict["Name"] = self.model.name.replace(" ", "_").replace(".", "_") def map_common(self): self.opendss_dict["Units"] = "km" From eaf6a7d05d960d49cb6d577c6b9dacc6cbb2a648 Mon Sep 17 00:00:00 2001 From: tarekelgindy Date: Tue, 4 Nov 2025 19:49:23 -0700 Subject: [PATCH 36/50] removing extra addition of equipment. This is done when parsing the equipment itself --- src/ditto/writers/opendss/write.py | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/ditto/writers/opendss/write.py b/src/ditto/writers/opendss/write.py index f5a3e63..55c235d 100644 --- a/src/ditto/writers/opendss/write.py +++ b/src/ditto/writers/opendss/write.py @@ -24,6 +24,7 @@ def _get_dss_string(self, model_map: Any) -> str: altdss_class = getattr(altdss_models, model_map.altdss_name) # Example altdss_class is Bus altdss_object = altdss_class.model_validate(model_map.opendss_dict) + if model_map.altdss_composition_name is not None: altdss_composition_class = getattr(altdss_models, model_map.altdss_composition_name) altdss_composition_object = altdss_composition_class(altdss_object) @@ -86,18 +87,6 @@ def write( # noqa equipment_dss_string = None equipment_map: list[Path] = None - if hasattr(model, "equipment"): - equipment_mapper_name = model.equipment.__class__.__name__ + "Mapper" - if not hasattr(opendss_mapper, equipment_mapper_name): - logger.warning( - f"Equipment Mapper {equipment_mapper_name} not found. Skipping" - ) - else: - equipment_mapper = getattr(opendss_mapper, equipment_mapper_name) - equipment_map = equipment_mapper(model.equipment) - equipment_map.populate_opendss_dictionary() - equipment_dss_string = self._get_dss_string(equipment_map) - output_folder = output_path output_redirect = Path("") From 68eeea2eead4b1eb8a46fee42296c4846139eaaf Mon Sep 17 00:00:00 2001 From: tarekelgindy Date: Tue, 4 Nov 2025 20:28:34 -0700 Subject: [PATCH 37/50] updating names due to issues with non-uniqueness in opendss. Also setting zero length lines to be small instead --- src/ditto/writers/opendss/components/distribution_branch.py | 5 ++++- .../writers/opendss/components/matrix_impedance_fuse.py | 5 +++++ .../writers/opendss/components/matrix_impedance_recloser.py | 5 +++++ .../writers/opendss/components/matrix_impedance_switch.py | 5 +++++ .../writers/opendss/components/sequence_impedance_branch.py | 5 +++++ 5 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/ditto/writers/opendss/components/distribution_branch.py b/src/ditto/writers/opendss/components/distribution_branch.py index 8f2ce8c..311abc7 100644 --- a/src/ditto/writers/opendss/components/distribution_branch.py +++ b/src/ditto/writers/opendss/components/distribution_branch.py @@ -24,7 +24,10 @@ def map_buses(self): self.opendss_dict["Bus2"] += self.phase_map[phase] def map_length(self): - self.opendss_dict["Length"] = self.model.length.magnitude + length = self.model.length.magnitude + if length ==0: + length = 0.0001 # OpenDSS does not accept 0 length lines + self.opendss_dict["Length"] = length model_unit = str(self.model.length.units) if model_unit not in self.length_units_map: raise ValueError(f"{model_unit} not mapped for OpenDSS") diff --git a/src/ditto/writers/opendss/components/matrix_impedance_fuse.py b/src/ditto/writers/opendss/components/matrix_impedance_fuse.py index 4d671ca..bb2d03d 100644 --- a/src/ditto/writers/opendss/components/matrix_impedance_fuse.py +++ b/src/ditto/writers/opendss/components/matrix_impedance_fuse.py @@ -10,6 +10,11 @@ def __init__(self, model): altdss_composition_name = "Line" opendss_file = OpenDSSFileTypes.FUSE_FILE.value + def map_name(self): + name = self.model.name.replace(" ","_").replace(".","_") + name = name + "_fuse" + self.opendss_dict["Name"] = name + def map_equipment(self): self.opendss_dict["LineCode"] = self.model.equipment.name.replace(" ", "_").replace(".", "_") diff --git a/src/ditto/writers/opendss/components/matrix_impedance_recloser.py b/src/ditto/writers/opendss/components/matrix_impedance_recloser.py index 22125a0..0e211c8 100644 --- a/src/ditto/writers/opendss/components/matrix_impedance_recloser.py +++ b/src/ditto/writers/opendss/components/matrix_impedance_recloser.py @@ -10,6 +10,11 @@ def __init__(self, model): altdss_composition_name = "Line" opendss_file = OpenDSSFileTypes.RECLOSER_FILE.value + def map_name(self): + name = self.model.name.replace(" ","_").replace(".","_") + name = name + "_recloser" + self.opendss_dict["Name"] = name + def map_equipment(self): self.opendss_dict["LineCode"] = self.model.equipment.name.replace(" ", "_").replace(".", "_") diff --git a/src/ditto/writers/opendss/components/matrix_impedance_switch.py b/src/ditto/writers/opendss/components/matrix_impedance_switch.py index 837e62f..4714502 100644 --- a/src/ditto/writers/opendss/components/matrix_impedance_switch.py +++ b/src/ditto/writers/opendss/components/matrix_impedance_switch.py @@ -10,6 +10,11 @@ def __init__(self, model): altdss_composition_name = "Line" opendss_file = OpenDSSFileTypes.SWITCH_FILE.value + def map_name(self): + name = self.model.name.replace(" ","_").replace(".","_") + name = name + "_switch" + self.opendss_dict["Name"] = name + def map_equipment(self): self.opendss_dict["LineCode"] = self.model.equipment.name.replace(" ", "_").replace(".", "_") diff --git a/src/ditto/writers/opendss/components/sequence_impedance_branch.py b/src/ditto/writers/opendss/components/sequence_impedance_branch.py index 98c12dd..32aff32 100644 --- a/src/ditto/writers/opendss/components/sequence_impedance_branch.py +++ b/src/ditto/writers/opendss/components/sequence_impedance_branch.py @@ -10,5 +10,10 @@ def __init__(self, model): altdss_composition_name = "Line" opendss_file = OpenDSSFileTypes.LINES_FILE.value + def map_name(self): + name = self.model.name.replace(" ","_").replace(".","_") + name = name + "_seqimpedance" + self.opendss_dict["Name"] = name + def map_equipment(self): self.opendss_dict["LineCode"] = self.model.equipment.name.replace(" ", "_").replace(".", "_") From f40f19264355307ccc5f242c9eac927af2f9f136 Mon Sep 17 00:00:00 2001 From: tarekelgindy Date: Mon, 24 Nov 2025 17:27:46 -0700 Subject: [PATCH 38/50] updates to components to fix bugs with capacitors and voltages sources in different versions of cyme --- .../cyme/components/distribution_capacitor.py | 20 +++++++++++++------ .../components/distribution_voltage_source.py | 7 ++++++- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/ditto/readers/cyme/components/distribution_capacitor.py b/src/ditto/readers/cyme/components/distribution_capacitor.py index e5e0dd9..4127cfa 100644 --- a/src/ditto/readers/cyme/components/distribution_capacitor.py +++ b/src/ditto/readers/cyme/components/distribution_capacitor.py @@ -16,7 +16,7 @@ def __init__(self, system): def parse(self, row, section_id_sections, equipment_data): name = self.map_name(row) bus = self.map_bus(row, section_id_sections) - phases = self.map_phases(row) + phases = self.map_phases(row, section_id_sections) controllers = self.map_controllers(row) equipment = self.map_equipment(row, equipment_data) in_service = self.map_in_service(row) @@ -30,14 +30,19 @@ def parse(self, row, section_id_sections, equipment_data): def map_name(self, row): return row["DeviceNumber"] - def map_phases(self, row): + def map_phases(self, row, section_id_sections): phases = [] - if row["FixedKVARA"]: + section_id = row['SectionID'] + section = section_id_sections[section_id] + section_phases = section['Phase'] + if 'FixedKVARA' in row and row["FixedKVARA"] or 'A' in section_phases: phases.append(Phase.A) - if row["FixedKVARB"]: + if 'FixedKVARB' in row and row["FixedKVARB"] or 'B' in section_phases: phases.append(Phase.B) - if row["FixedKVARC"]: + if 'FixedKVARC' in row and row["FixedKVARC"] or 'C' in section_phases: phases.append(Phase.C) + if phases == []: + raise ValueError(f"Could not determine phases for capacitor {row['DeviceNumber']} on section {section_id} with section phases {section_phases}") return phases def map_bus(self, row, section_id_sections): @@ -69,7 +74,10 @@ def map_controllers(self, row): def map_equipment(self, row, equipment_data): mapper = CapacitorEquipmentMapper(self.system) - equipment_row = equipment_data.loc[row['ShuntCapacitorID']] + capacitor_id = row['ShuntCapacitorID'] + if capacitor_id not in equipment_data.index: + capacitor_id = 'DEFAULT' + equipment_row = equipment_data.loc[capacitor_id] if not equipment_row.empty: equipment = mapper.parse(equipment_row, connection=row['Connection']) return equipment diff --git a/src/ditto/readers/cyme/components/distribution_voltage_source.py b/src/ditto/readers/cyme/components/distribution_voltage_source.py index 682190f..8755a7d 100644 --- a/src/ditto/readers/cyme/components/distribution_voltage_source.py +++ b/src/ditto/readers/cyme/components/distribution_voltage_source.py @@ -18,7 +18,12 @@ def parse(self, row): bus = self.map_bus(row) feeder = bus.feeder substation = bus.substation - voltage = float(row['OperatingVoltageA']) + if 'OperatingVoltageA' in row: + voltage = float(row['OperatingVoltageA']) + elif 'DesiredVoltage' in row: + voltage = float(row['DesiredVoltage']) + else: + raise ValueError(f"Operating voltage not found in row: {row}") if voltage is None or voltage == '': return None From 5b4334f58f3afc5f442f0140c940b786aff9161e Mon Sep 17 00:00:00 2001 From: tarekelgindy Date: Mon, 24 Nov 2025 17:33:16 -0700 Subject: [PATCH 39/50] updates to equipment to fix bugs with transformers and branches in different versions of cyme --- .../distribution_transformer_equipment.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/ditto/readers/cyme/equipment/distribution_transformer_equipment.py b/src/ditto/readers/cyme/equipment/distribution_transformer_equipment.py index 1268d26..a5a8bb0 100644 --- a/src/ditto/readers/cyme/equipment/distribution_transformer_equipment.py +++ b/src/ditto/readers/cyme/equipment/distribution_transformer_equipment.py @@ -203,7 +203,12 @@ def map_is_grounded(self, row, winding_number): winding = 1 elif winding_number == 3: winding = 1 - winding_type = self.connection_map.get(int(connection_type), 'Y_Y').split('_')[winding] + if isinstance(connection_type, int) or (isinstance(connection_type, str) and connection_type.isdigit()): + conn_type_int = int(connection_type) + winding_type = self.connection_map.get(conn_type_int, 'Y_Y').split('_')[winding] + else: + winding_type = str(connection_type) + if 'YNG' in winding_type: grounded = False elif 'D' in winding_type: @@ -228,7 +233,7 @@ def map_rated_voltage(self, row, winding_number): def map_voltage_type(self, row, rated_voltage): # This is from the CYME documentation but appears to not be entirely correct # Clearly L-L voltages still appear with a voltage type of 1 - if row["VoltageUnit"] == '1' or row["VoltageUnit"] == "3": + if 'VoltageUnit' in row and (row["VoltageUnit"] == '1' or row["VoltageUnit"] == "3"): return VoltageTypes.LINE_TO_GROUND return VoltageTypes.LINE_TO_LINE @@ -254,7 +259,11 @@ def map_connection_type(self, row, winding_number): winding = 1 elif winding_number == 3: winding = 1 - winding_type = self.connection_map.get(int(connection_type), 'Y_Y').split('_')[winding] + if isinstance(connection_type, int) or (isinstance(connection_type, str) and connection_type.isdigit()): + conn_type_int = int(connection_type) + winding_type = self.connection_map.get(conn_type_int, 'Y_Y').split('_')[winding] + else: + winding_type = str(connection_type) if winding_type == 'YO': connection_type = 'OPEN_STAR' elif winding_type == 'DO': From 005a6c190d60c975dde0c1409dba23bcfa4ac0c0 Mon Sep 17 00:00:00 2001 From: tarekelgindy Date: Mon, 24 Nov 2025 17:33:54 -0700 Subject: [PATCH 40/50] setting units for matrix impedance branch. TODO: check unit types --- .../cyme/equipment/matrix_impedance_branch_equipment.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/ditto/readers/cyme/equipment/matrix_impedance_branch_equipment.py b/src/ditto/readers/cyme/equipment/matrix_impedance_branch_equipment.py index 94eb6b0..a41933f 100644 --- a/src/ditto/readers/cyme/equipment/matrix_impedance_branch_equipment.py +++ b/src/ditto/readers/cyme/equipment/matrix_impedance_branch_equipment.py @@ -1,4 +1,5 @@ -from gdm.quantities import Distance + +from gdm.quantities import Current, Distance, ResistancePULength, Distance, Voltage from ditto.readers.cyme.cyme_mapper import CymeMapper from gdm.distribution.equipment.matrix_impedance_branch_equipment import MatrixImpedanceBranchEquipment from gdm.distribution.enums import Phase @@ -58,12 +59,14 @@ def map_r_matrix(self, row, phases): r1 = float(row['R1']) r0 = float(row['R0']) matrix = self._sequence_impedance_to_phase_impedance_matrix(r1, r0, phases) + matrix = ResistancePULength(np.array(matrix), "ohm/mile") return matrix def map_x_matrix(self, row, phases): x1 = float(row['X1']) x0 = float(row['X0']) matrix = self._sequence_impedance_to_phase_impedance_matrix(x1, x0, phases) + matrix = ResistancePULength(np.array(matrix), "ohm/mile") return matrix def map_c_matrix(self, row, phases): @@ -72,7 +75,8 @@ def map_c_matrix(self, row, phases): susceptance_matrix = self._sequence_impedance_to_phase_impedance_matrix(b1, b0, phases) # Convert susceptance to capacitance: C = B / (2 * pi * f) frequency = 60 # Hz - capacitance_matrix = susceptance_matrix / (2 * np.pi * frequency) + capacitance_matrix = susceptance_matrix / (2 * np.pi * frequency) + capacitance_matrix = ResistancePULength(np.array(capacitance_matrix), "farad/mile") return capacitance_matrix def map_ampacity(self, row): From 31cf0f658fbdb06ce4b6d698399ef98c6f01e4aa Mon Sep 17 00:00:00 2001 From: tarekelgindy Date: Mon, 24 Nov 2025 17:36:37 -0700 Subject: [PATCH 41/50] updates to equipment to fix bugs with branches in different versions of cyme --- .../cyme/equipment/geometry_branch_equipment.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/ditto/readers/cyme/equipment/geometry_branch_equipment.py b/src/ditto/readers/cyme/equipment/geometry_branch_equipment.py index 0b6ce8e..646ddb8 100644 --- a/src/ditto/readers/cyme/equipment/geometry_branch_equipment.py +++ b/src/ditto/readers/cyme/equipment/geometry_branch_equipment.py @@ -298,8 +298,10 @@ def map_vertical_positions(self, row, spacing_ids): phase_A_conductor_name = row['CondID_A'] phase_B_conductor_name = row['CondID_B'] phase_C_conductor_name = row['CondID_C'] - neutral_conductor_name = row['CondID_N1'] - + if 'CondID_N1' in row: + neutral_conductor_name = row['CondID_N1'] + else: + neutral_conductor_name = row['CondID_N'] spacing_id = row['SpacingID'] spacing = spacing_ids.loc[spacing_id] @@ -328,7 +330,10 @@ def map_horizontal_positions(self, row, spacing_ids): phase_A_conductor_name = row['CondID_A'] phase_B_conductor_name = row['CondID_B'] phase_C_conductor_name = row['CondID_C'] - neutral_conductor_name = row['CondID_N1'] + if 'CondID_N1' in row: + neutral_conductor_name = row['CondID_N1'] + else: + neutral_conductor_name = row['CondID_N'] spacing_id = row['SpacingID'] spacing = spacing_ids.loc[spacing_id] if not spacing.empty: @@ -356,7 +361,10 @@ def map_conductors(self,row, spacing_ids): phase_A_conductor_name = row['CondID_A'] phase_B_conductor_name = row['CondID_B'] phase_C_conductor_name = row['CondID_C'] - neutral_conductor_name = row['CondID_N1'] + if 'CondID_N1' in row: + neutral_conductor_name = row['CondID_N1'] + else: + neutral_conductor_name = row['CondID_N'] phase_A_conductor = None phase_B_conductor = None From 78170a00206fb4662a78122c6d08df77249f5d23 Mon Sep 17 00:00:00 2001 From: tarekelgindy Date: Mon, 24 Nov 2025 17:46:44 -0700 Subject: [PATCH 42/50] adding considerations for sizes which are zero. Should be caught by validation in gdm first... --- .../opendss/equipment/bare_conductor_equipment.py | 10 ++++++++-- .../opendss/equipment/concentric_cable_equipment.py | 10 ++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/ditto/writers/opendss/equipment/bare_conductor_equipment.py b/src/ditto/writers/opendss/equipment/bare_conductor_equipment.py index 7a7cf06..ce87a66 100644 --- a/src/ditto/writers/opendss/equipment/bare_conductor_equipment.py +++ b/src/ditto/writers/opendss/equipment/bare_conductor_equipment.py @@ -14,14 +14,20 @@ def map_name(self): self.opendss_dict["Name"] = self.model.name.replace(" ","_").replace(".","_") def map_conductor_diameter(self): - self.opendss_dict["Radius"] = self.model.conductor_diameter.magnitude / 2 + radius = self.model.conductor_diameter.magnitude / 2 + if radius <=0: + radius = 0.0001 + self.opendss_dict["Radius"] = radius rad_units = str(self.model.conductor_diameter.units) if rad_units not in self.length_units_map: raise ValueError(f"{rad_units} not mapped for OpenDSS") self.opendss_dict["RadUnits"] = self.length_units_map[rad_units] def map_conductor_gmr(self): - self.opendss_dict["GMRAC"] = self.model.conductor_gmr.magnitude + gmr = self.model.conductor_gmr.magnitude + if gmr <=0: + gmr = 0.0001 + self.opendss_dict["GMRAC"] = gmr gmr_units = str(self.model.conductor_gmr.units) if gmr_units not in self.length_units_map: raise ValueError(f"{gmr_units} not mapped for OpenDSS") diff --git a/src/ditto/writers/opendss/equipment/concentric_cable_equipment.py b/src/ditto/writers/opendss/equipment/concentric_cable_equipment.py index f6b8b59..3e70e6d 100644 --- a/src/ditto/writers/opendss/equipment/concentric_cable_equipment.py +++ b/src/ditto/writers/opendss/equipment/concentric_cable_equipment.py @@ -17,7 +17,10 @@ def map_strand_diameter(self): self.opendss_dict["DiaStrand"] = self.model.strand_diameter.magnitude def map_conductor_diameter(self): - self.opendss_dict["Radius"] = self.model.conductor_diameter.magnitude / 2 + radius = self.model.conductor_diameter.magnitude / 2 + if radius <=0: + radius = 0.0001 + self.opendss_dict["Radius"] = radius rad_units = str(self.model.conductor_diameter.units) if rad_units not in self.length_units_map: raise ValueError(f"{rad_units} not mapped for OpenDSS") @@ -37,7 +40,10 @@ def map_ampacity(self): self.opendss_dict["NormAmps"] = ampacity_amps.magnitude def map_conductor_gmr(self): - self.opendss_dict["GMRAC"] = self.model.conductor_gmr.magnitude + gmr = self.model.conductor_gmr.magnitude + if gmr <=0: + gmr = 0.0001 + self.opendss_dict["GMRAC"] = gmr gmr_units = str(self.model.conductor_gmr.units) if gmr_units not in self.length_units_map: raise ValueError(f"{gmr_units} not mapped for OpenDSS") From 06f9302b47960554b20619e3eb50db8b4cc430d1 Mon Sep 17 00:00:00 2001 From: tarekelgindy Date: Mon, 24 Nov 2025 18:01:17 -0700 Subject: [PATCH 43/50] adding a default load number --- tests/test_cyme/test_cyme_reader.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_cyme/test_cyme_reader.py b/tests/test_cyme/test_cyme_reader.py index db55cbb..ba78454 100644 --- a/tests/test_cyme/test_cyme_reader.py +++ b/tests/test_cyme/test_cyme_reader.py @@ -34,7 +34,7 @@ def test_cyme_reader(cyme_folder: Path, tmp_path): if not export_path.exists(): export_path.mkdir(parents=True, exist_ok=True) - reader = Reader(cyme_folder / cyme_network_name, cyme_folder / cyme_equipment_name, cyme_folder / cyme_load_name) + reader = Reader(cyme_folder / cyme_network_name, cyme_folder / cyme_equipment_name, cyme_folder / cyme_load_name, '1') writer = Writer(reader.get_system()) writer.write(export_path / "opendss", separate_substations=False, separate_feeders=False) system = reader.get_system() @@ -42,4 +42,6 @@ def test_cyme_reader(cyme_folder: Path, tmp_path): system.to_json(json_path, overwrite=True, indent=4) system.to_geojson(export_path / (cyme_folder.stem.lower() + ".geojson")) + + assert json_path.exists(), "Failed to export the json file" From f83605a0fdae9ef945f7d9003e604cd37443ef78 Mon Sep 17 00:00:00 2001 From: tarekelgindy Date: Mon, 24 Nov 2025 18:03:30 -0700 Subject: [PATCH 44/50] organizing opendss data to be in a folder --- tests/test_opendss/test_opendss_reader.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_opendss/test_opendss_reader.py b/tests/test_opendss/test_opendss_reader.py index 72fcd08..2490f16 100644 --- a/tests/test_opendss/test_opendss_reader.py +++ b/tests/test_opendss/test_opendss_reader.py @@ -29,10 +29,10 @@ def test_serialize_opendss_model(opendss_file: Path, tmp_path): example_name = opendss_file.parent.name # export_path = Path(tmp_path) / example_name - export_path = base_path / "dump_from_tests" / example_name + export_path = base_path / "dump_from_tests" / "opendss" / example_name if not export_path.exists(): - os.mkdir(export_path) + os.makedirs(export_path) parser = Reader(opendss_file) system = parser.get_system() json_path = export_path / (opendss_file.stem.lower() + ".json") From ffd117e96a01238ad9dc033a3204c13c097202e7 Mon Sep 17 00:00:00 2001 From: "Zink, Zephyr" Date: Wed, 7 Jan 2026 14:15:23 -0700 Subject: [PATCH 45/50] ability to truncate model to certain subs,feeders --- src/ditto/readers/cyme/reader.py | 20 +++++------- src/ditto/readers/cyme/utils.py | 55 ++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 12 deletions(-) diff --git a/src/ditto/readers/cyme/reader.py b/src/ditto/readers/cyme/reader.py index 1a68e74..6460689 100644 --- a/src/ditto/readers/cyme/reader.py +++ b/src/ditto/readers/cyme/reader.py @@ -1,28 +1,21 @@ from gdm.quantities import Distance, Current, ResistancePULength from gdm.distribution.equipment.bare_conductor_equipment import BareConductorEquipment -from gdm.distribution.components.base.distribution_component_base import DistributionComponentBase from gdm.distribution.distribution_system import DistributionSystem -from gdm.distribution.components.matrix_impedance_branch import MatrixImpedanceBranch -from gdm.distribution.equipment.matrix_impedance_branch_equipment import MatrixImpedanceBranchEquipment from ditto.readers.reader import AbstractReader -from ditto.readers.cyme.utils import read_cyme_data +from ditto.readers.cyme.utils import read_cyme_data, network_truncation import ditto.readers.cyme as cyme_mapper from loguru import logger from pydantic import ValidationError from rich.console import Console from infrasys import Component from rich.table import Table -from functools import partial from collections import defaultdict from gdm.distribution.components.distribution_bus import DistributionBus from gdm.distribution.components import DistributionVoltageSource -from gdm.distribution.components.distribution_transformer import DistributionTransformer from gdm.quantities import Voltage -from gdm.distribution.enums import VoltageTypes, ConnectionType - -from infrasys.exceptions import ISAlreadyAttached +from gdm.distribution.enums import VoltageTypes class Reader(AbstractReader): # Order of components is important @@ -51,11 +44,11 @@ class Reader(AbstractReader): validation_errors = [] - def __init__(self, network_file, equipment_file, load_file, load_model_id = None): + def __init__(self, network_file, equipment_file, load_file, load_model_id = None, substation_names=None, feeder_names=None): self.system = DistributionSystem(auto_add_composed_components=True) - self.read(network_file, equipment_file, load_file, load_model_id) + self.read(network_file, equipment_file, load_file, load_model_id, substation_names=substation_names, feeder_names=feeder_names) - def read(self, network_file, equipment_file, load_file, load_model_id = None): + def read(self, network_file, equipment_file, load_file, load_model_id = None, substation_names=None, feeder_names=None): # Section data read separately as it links to other tables section_id_sections = {} @@ -169,6 +162,9 @@ def parse_row(row): self.system = self.assign_bus_voltages(network_dist_sys=self.system) + if substation_names is not None or feeder_names is not None: + self.system = network_truncation(self.system, substation_names=substation_names, feeder_names=feeder_names) + print("Finished truncation") for component_type in self.system.get_component_types(): components = self.system.get_components(component_type) diff --git a/src/ditto/readers/cyme/utils.py b/src/ditto/readers/cyme/utils.py index 4b17fdf..2c6873f 100644 --- a/src/ditto/readers/cyme/utils.py +++ b/src/ditto/readers/cyme/utils.py @@ -1,6 +1,11 @@ import pandas as pd from gdm.distribution.components.distribution_feeder import DistributionFeeder from gdm.distribution.components.distribution_substation import DistributionSubstation +from gdm.distribution.distribution_system import DistributionSystem +from gdm.distribution.components.distribution_bus import DistributionBus +from infrasys.exceptions import ISAlreadyAttached + +from functools import partial def read_cyme_data(cyme_file, cyme_section, index_col=None, node_feeder_map = None, network_voltage_map = None, node_substation_map = None, parse_feeders=False, parse_substation=False): @@ -64,3 +69,53 @@ def read_cyme_data(cyme_file, cyme_section, index_col=None, node_feeder_map = No if index_col is not None: data.set_index(index_col, inplace=True, drop=False) return data + + +def network_truncation(system ,substation_names=None, feeder_names=None): + trunc_dist_sys = DistributionSystem(auto_add_composed_components=True) + buses = list(system.get_components(DistributionBus, filter_func=partial(filter_substation, substation_names=substation_names))) + buses.extend(list(system.get_components(DistributionBus, filter_func=partial(filter_feeder, feeder_names=feeder_names)))) + bus_set = set() + for bus in buses: + bus_set.add(bus.name) + print(f"Truncating to {len(bus_set)} buses") + types = list(system.get_component_types()) + for component_type in types: + components = list(system.get_components(component_type)) + length = len(components) + print(f"Truncating components of type {component_type.__name__}, total: {length}") + for i, comp in enumerate(components): + print(f"Truncating component {i+1} of {length}", end='\r', flush=True) + if hasattr(comp, "bus"): + if comp.bus.name in bus_set: + try: + trunc_dist_sys.add_component(comp) + except ISAlreadyAttached: + pass + elif hasattr(comp, "buses"): + for bus in comp.buses: + if bus.name in bus_set: + try: + trunc_dist_sys.add_component(comp) + except ISAlreadyAttached: + pass + break + else: + break + + return trunc_dist_sys + + +def filter_feeder(object, feeder_names=None): + if not hasattr(object.feeder, "name"): + return False + if object.feeder.name in feeder_names: + return True + return False + +def filter_substation(object, substation_names=None): + if not hasattr(object.substation, "name"): + return False + if object.substation.name in substation_names: + return True + return False \ No newline at end of file From b9702ba8bb846bb029d49dc5ff9c48200fc9e99d Mon Sep 17 00:00:00 2001 From: "Zink, Zephyr" Date: Thu, 15 Jan 2026 17:28:03 -0700 Subject: [PATCH 46/50] linting, removing split phase for now --- .../components/distribution_transformer.py | 177 +++++---- .../cyme/components/geometry_branch.py | 88 +++-- .../distribution_transformer_equipment.py | 344 +++++++++--------- src/ditto/readers/cyme/reader.py | 195 +++++++--- 4 files changed, 451 insertions(+), 353 deletions(-) diff --git a/src/ditto/readers/cyme/components/distribution_transformer.py b/src/ditto/readers/cyme/components/distribution_transformer.py index e88efdc..fce2112 100644 --- a/src/ditto/readers/cyme/components/distribution_transformer.py +++ b/src/ditto/readers/cyme/components/distribution_transformer.py @@ -1,77 +1,88 @@ from loguru import logger from ditto.readers.cyme.cyme_mapper import CymeMapper -from ditto.readers.cyme.equipment.distribution_transformer_equipment import DistributionTransformerEquipmentMapper -from ditto.readers.cyme.equipment.distribution_transformer_three_winding_equipment import DistributionTransformerThreeWindingEquipmentMapper +from ditto.readers.cyme.equipment.distribution_transformer_equipment import ( + DistributionTransformerEquipmentMapper, +) +from ditto.readers.cyme.equipment.distribution_transformer_three_winding_equipment import ( + DistributionTransformerThreeWindingEquipmentMapper, +) from gdm.distribution.components.distribution_bus import DistributionBus from gdm.distribution.components.distribution_transformer import DistributionTransformer -from gdm.distribution.equipment.distribution_transformer_equipment import DistributionTransformerEquipment from gdm.distribution.enums import Phase + class DistributionTransformerMapper(CymeMapper): def __init__(self, system): super().__init__(system) - cyme_file = 'Network' - cyme_section = 'TRANSFORMER SETTING' + cyme_file = "Network" + cyme_section = "TRANSFORMER SETTING" def parse(self, row, used_sections, section_id_sections, equipment_data): - equipment_row = equipment_data.get(row['EqID'], None) + section_id = str(row["SectionID"]) + section = section_id_sections[section_id] + phase = section["Phase"] + + equipment_row = equipment_data.get(row["EqID"], None) name = self.map_name(row) buses = self.map_buses(row, section_id_sections) - winding_phases = self.map_winding_phases(row, section_id_sections, equipment_row) - equipment = self.map_equipment(row, equipment_row) + winding_phases = self.map_winding_phases(row, section_id_sections, equipment_row, phase) + equipment = self.map_equipment(row, equipment_row, phase) try: used_sections.add(name) - return DistributionTransformer.model_construct(name=name, - buses=buses, - winding_phases=winding_phases, - equipment=equipment) + return DistributionTransformer.model_construct( + name=name, buses=buses, winding_phases=winding_phases, equipment=equipment + ) except Exception as e: logger.warning(f"Failed to create DistributionTransformer {name}: {e}") return None def map_name(self, row): - name = row['SectionID'] + name = row["SectionID"] return name def map_buses(self, row, section_id_sections): - section_id = str(row['SectionID']) + section_id = str(row["SectionID"]) section = section_id_sections[section_id] - from_bus_name = section['FromNodeID'] - to_bus_name = section['ToNodeID'] + from_bus_name = section["FromNodeID"] + to_bus_name = section["ToNodeID"] from_bus = self.system.get_component(component_type=DistributionBus, name=from_bus_name) to_bus = self.system.get_component(component_type=DistributionBus, name=to_bus_name) return [from_bus, to_bus] - def map_winding_phases(self, row, section_id_sections, equipment_row): - section_id = str(row['SectionID']) + def map_winding_phases(self, row, section_id_sections, equipment_row, phase): + section_id = str(row["SectionID"]) section = section_id_sections[section_id] - phase = section['Phase'] + if equipment_row is None: - print(f"Equipment row not found for transformer {row['EqID']}. Assuming 2 windings. {section}") - equipment_row = {'Type': "2"} + print( + f"Equipment row not found for transformer {row['EqID']}. Assuming 2 windings. {section}" + ) + equipment_row = {"Type": "2"} windings_list = [] - if equipment_row['Type'] == "4": - num_windings = 3 - else: - num_windings = 2 - + # TODO Center tapped/Split phase not supported + # This will assign it properly but handling of buses needs to be developed + # if equipment_row['Type'] == "4": + # num_windings = 3 + # else: + # num_windings = 2 + num_windings = 2 for i in range(num_windings): winding_phases = [] - if 'A' in phase: + if "A" in phase: winding_phases.append(Phase.A) - if 'B' in phase: + if "B" in phase: winding_phases.append(Phase.B) - if 'C' in phase: + if "C" in phase: winding_phases.append(Phase.C) windings_list.append(winding_phases) return windings_list - def map_equipment(self, row, equipment_row): + def map_equipment(self, row, equipment_row, phase): mapper = DistributionTransformerEquipmentMapper(self.system) if equipment_row is not None: - equipment = mapper.parse(equipment_row, row) + equipment = mapper.parse(equipment_row, row, phase) if equipment is not None: return equipment return None @@ -81,44 +92,48 @@ class DistributionTransformerByPhaseMapper(CymeMapper): def __init__(self, system): super().__init__(system) - cyme_file = 'Network' - cyme_section = 'TRANSFORMER BYPHASE SETTING' + cyme_file = "Network" + cyme_section = "TRANSFORMER BYPHASE SETTING" def parse(self, row, used_sections, section_id_sections, equipment_data): additional_transformers = [] - - for phase in ['1', '2', '3']: - if row['PhaseTransformerID' + phase] is None or row['PhaseTransformerID' + phase] == '': + + for phase in ["1", "2", "3"]: + if ( + row["PhaseTransformerID" + phase] is None + or row["PhaseTransformerID" + phase] == "" + ): continue - equipment_row = equipment_data.get(row['PhaseTransformerID' + phase], None) - + equipment_row = equipment_data.get(row["PhaseTransformerID" + phase], None) + name = self.map_name(row, phase) equipment = self.map_equipment(row, phase, equipment_row) buses = self.map_buses(row, section_id_sections, equipment.is_center_tapped) winding_phases = self.map_winding_phases(row, phase, equipment_row) - try: - used_sections.add(row['SectionID']) - additional_transformers.append(DistributionTransformer.model_construct(name=name, - buses=buses, - winding_phases=winding_phases, - equipment=equipment)) + used_sections.add(row["SectionID"]) + additional_transformers.append( + DistributionTransformer.model_construct( + name=name, buses=buses, winding_phases=winding_phases, equipment=equipment + ) + ) except Exception as e: - logger.warning(f"Failed to add additional transformer {name} for phase {phase} on {row['SectionID']}: {e}") + logger.warning( + f"Failed to add additional transformer {name} for phase {phase} on {row['SectionID']}: {e}" + ) continue return additional_transformers - def map_name(self, row, phase): - name = row['SectionID'] + f"_{phase}" + name = row["SectionID"] + f"_{phase}" return name def map_buses(self, row, section_id_sections, is_center_tapped=False): - section_id = str(row['SectionID']) + section_id = str(row["SectionID"]) section = section_id_sections[section_id] - from_bus_name = section['FromNodeID'] - to_bus_name = section['ToNodeID'] + from_bus_name = section["FromNodeID"] + to_bus_name = section["ToNodeID"] from_bus = self.system.get_component(component_type=DistributionBus, name=from_bus_name) to_bus = self.system.get_component(component_type=DistributionBus, name=to_bus_name) @@ -128,22 +143,26 @@ def map_buses(self, row, section_id_sections, is_center_tapped=False): def map_winding_phases(self, row, phase, equipment_row): if equipment_row is None: - print(f"Equipment row not found for transformer {row['PhaseTransformerID' + phase]}. Assuming 2 windings.") - equipment_row = {'Type': 2} + print( + f"Equipment row not found for transformer {row['PhaseTransformerID' + phase]}. Assuming 2 windings." + ) + equipment_row = {"Type": 2} windings_list = [] num_windings = 3 - if equipment_row['Type'] == "4": - num_windings = 3 - else: - num_windings = 2 - + # TODO Center tapped/Split phase not supported + # This will assign it properly but handling of buses needs to be developed + # if equipment_row['Type'] == "4": + # num_windings = 3 + # else: + # num_windings = 2 + # num_windings = 2 for i in range(num_windings): winding_phases = [] - if '1' == phase: + if "1" == phase: winding_phases.append(Phase.A) - if '2' == phase: + if "2" == phase: winding_phases.append(Phase.B) - if '3' == phase: + if "3" == phase: winding_phases.append(Phase.C) windings_list.append(winding_phases) assert len(windings_list) == num_windings @@ -152,46 +171,44 @@ def map_winding_phases(self, row, phase, equipment_row): def map_equipment(self, row, phase, equipment_row): mapper = DistributionTransformerEquipmentMapper(self.system) if equipment_row is not None: - equipment = mapper.parse(equipment_row, row) + equipment = mapper.parse(equipment_row, row, phase) if equipment is not None: return equipment return None - class DistributionTransformerThreeWindingMapper(CymeMapper): def __init__(self, system): super().__init__(system) - cyme_file = 'Network' - cyme_section = 'THREE WINDING TRANSFORMER SETTING' + cyme_file = "Network" + cyme_section = "THREE WINDING TRANSFORMER SETTING" def parse(self, row, used_sections, section_id_sections, equipment_data): - equipment_row = equipment_data.get(row['EqID'], None) + equipment_row = equipment_data.get(row["EqID"], None) name = self.map_name(row) buses = self.map_buses(row, section_id_sections) winding_phases = self.map_winding_phases(row, section_id_sections) equipment = self.map_equipment(row, equipment_row) try: used_sections.add(name) - return DistributionTransformer.model_construct(name=name, - buses=buses, - winding_phases=winding_phases, - equipment=equipment) + return DistributionTransformer.model_construct( + name=name, buses=buses, winding_phases=winding_phases, equipment=equipment + ) except Exception as e: logger.warning(f"Failed to create DistributionTransformer {name}: {e}") return None def map_name(self, row): - name = row['SectionID'] + name = row["SectionID"] return name def map_buses(self, row, section_id_sections): - section_id = str(row['SectionID']) + section_id = str(row["SectionID"]) section = section_id_sections[section_id] - from_bus_name = section['FromNodeID'] - to_bus_name = section['ToNodeID'] - tertiary_bus = row['TertiaryNodeID'] + from_bus_name = section["FromNodeID"] + to_bus_name = section["ToNodeID"] + tertiary_bus = row["TertiaryNodeID"] from_bus = self.system.get_component(component_type=DistributionBus, name=from_bus_name) to_bus = self.system.get_component(component_type=DistributionBus, name=to_bus_name) @@ -203,19 +220,19 @@ def map_buses(self, row, section_id_sections): return [from_bus, to_bus, tertiary_bus] def map_winding_phases(self, row, section_id_sections): - section_id = str(row['SectionID']) + section_id = str(row["SectionID"]) section = section_id_sections[section_id] - phase = section['Phase'] + phase = section["Phase"] windings_list = [] num_windings = 3 for i in range(num_windings): winding_phases = [] - if 'A' in phase: + if "A" in phase: winding_phases.append(Phase.A) - if 'B' in phase: + if "B" in phase: winding_phases.append(Phase.B) - if 'C' in phase: + if "C" in phase: winding_phases.append(Phase.C) windings_list.append(winding_phases) return windings_list @@ -226,4 +243,4 @@ def map_equipment(self, row, equipment_row): equipment = mapper.parse(equipment_row, row) if equipment is not None: return equipment - return None \ No newline at end of file + return None diff --git a/src/ditto/readers/cyme/components/geometry_branch.py b/src/ditto/readers/cyme/components/geometry_branch.py index 150dec5..ca70a92 100644 --- a/src/ditto/readers/cyme/components/geometry_branch.py +++ b/src/ditto/readers/cyme/components/geometry_branch.py @@ -6,56 +6,55 @@ from gdm.distribution.enums import Phase - class GeometryBranchMapper(CymeMapper): def __init__(self, system): super().__init__(system) - cyme_file = 'Network' - cyme_section = 'OVERHEADLINE SETTING' + cyme_file = "Network" + cyme_section = "OVERHEADLINE SETTING" def parse(self, row, used_sections, section_id_sections): name = self.map_name(row) - buses = self.map_buses(row,section_id_sections) + buses = self.map_buses(row, section_id_sections) length = self.map_length(row) equipment = self.map_equipment(row) phases = self.map_phases(row, section_id_sections, equipment, buses) used_sections.add(name) - return GeometryBranch.model_construct(name=name, - buses=buses, - length=length, - phases=phases, - equipment=equipment) + return GeometryBranch.model_construct( + name=name, buses=buses, length=length, phases=phases, equipment=equipment + ) def map_name(self, row): - name = row['SectionID'] + name = row["SectionID"] return name def map_buses(self, row, section_id_sections): - section_id = str(row['SectionID']) + section_id = str(row["SectionID"]) section = section_id_sections[section_id] - from_bus_name = section['FromNodeID'] - to_bus_name = section['ToNodeID'] - + from_bus_name = section["FromNodeID"] + to_bus_name = section["ToNodeID"] + from_bus = self.system.get_component(component_type=DistributionBus, name=from_bus_name) to_bus = self.system.get_component(component_type=DistributionBus, name=to_bus_name) return [from_bus, to_bus] def map_length(self, row): - length = Distance(float(row['Length']),'foot').to('km') + length = Distance(float(row["Length"]), "foot").to("km") + if length <= 0: + length = Distance(0.001, "km") return length def map_phases(self, row, section_id_sections, equipment, buses): - section_id = str(row['SectionID']) + section_id = str(row["SectionID"]) section = section_id_sections[section_id] - phase = section['Phase'] + phase = section["Phase"] phases = [] - if 'A' in phase: + if "A" in phase: phases.append(Phase.A) - if 'B' in phase: + if "B" in phase: phases.append(Phase.B) - if 'C' in phase: + if "C" in phase: phases.append(Phase.C) if len(phases) == len(equipment.conductors): @@ -68,65 +67,63 @@ def map_phases(self, row, section_id_sections, equipment, buses): return phases else: return phases - #raise ValueError(f"Number of phases {len(phases)} does not match number of conductors {len(equipment.conductors)} for line {row['SectionID']}") + # raise ValueError(f"Number of phases {len(phases)} does not match number of conductors {len(equipment.conductors)} for line {row['SectionID']}") def map_equipment(self, row): - line_id = row['LineCableID'] + line_id = row["LineCableID"] line = self.system.get_component(component_type=GeometryBranchEquipment, name=line_id) return line - + + class GeometryBranchByPhaseMapper(CymeMapper): def __init__(self, system): super().__init__(system) - cyme_file = 'Network' - cyme_section = 'OVERHEAD BYPHASE SETTING' + cyme_file = "Network" + cyme_section = "OVERHEAD BYPHASE SETTING" def parse(self, row, used_sections, section_id_sections): name = self.map_name(row) - buses = self.map_buses(row,section_id_sections) + buses = self.map_buses(row, section_id_sections) length = self.map_length(row) equipment = self.map_equipment(row) phases = self.map_phases(row, section_id_sections, equipment, buses) used_sections.add(name) - return GeometryBranch.model_construct(name=name, - buses=buses, - length=length, - phases=phases, - equipment=equipment) - + return GeometryBranch.model_construct( + name=name, buses=buses, length=length, phases=phases, equipment=equipment + ) def map_name(self, row): - name = row['SectionID'] + name = row["SectionID"] return name def map_buses(self, row, section_id_sections): - section_id = str(row['SectionID']) + section_id = str(row["SectionID"]) section = section_id_sections[section_id] - from_bus_name = section['FromNodeID'] - to_bus_name = section['ToNodeID'] - + from_bus_name = section["FromNodeID"] + to_bus_name = section["ToNodeID"] + from_bus = self.system.get_component(component_type=DistributionBus, name=from_bus_name) to_bus = self.system.get_component(component_type=DistributionBus, name=to_bus_name) return [from_bus, to_bus] def map_length(self, row): - length = Distance(float(row['Length']),'foot').to('km') + length = Distance(float(row["Length"]), "foot").to("km") return length def map_phases(self, row, section_id_sections, equipment, buses): - section_id = str(row['SectionID']) + section_id = str(row["SectionID"]) section = section_id_sections[section_id] - phase = section['Phase'] + phase = section["Phase"] phases = [] - if 'A' in phase: + if "A" in phase: phases.append(Phase.A) - if 'B' in phase: + if "B" in phase: phases.append(Phase.B) - if 'C' in phase: + if "C" in phase: phases.append(Phase.C) - + if len(phases) == len(equipment.conductors): return phases elif len(phase) == len(equipment.conductors) - 1: @@ -138,7 +135,6 @@ def map_phases(self, row, section_id_sections, equipment, buses): return phases def map_equipment(self, row): - line_id = row['SectionID'] + line_id = row["SectionID"] line = self.system.get_component(component_type=GeometryBranchEquipment, name=line_id) return line - diff --git a/src/ditto/readers/cyme/equipment/distribution_transformer_equipment.py b/src/ditto/readers/cyme/equipment/distribution_transformer_equipment.py index a5a8bb0..93a9d35 100644 --- a/src/ditto/readers/cyme/equipment/distribution_transformer_equipment.py +++ b/src/ditto/readers/cyme/equipment/distribution_transformer_equipment.py @@ -1,62 +1,65 @@ -from loguru import logger from ditto.readers.cyme.cyme_mapper import CymeMapper -from gdm.distribution.equipment.distribution_transformer_equipment import DistributionTransformerEquipment +from gdm.distribution.equipment.distribution_transformer_equipment import ( + DistributionTransformerEquipment, +) from gdm.distribution.equipment.distribution_transformer_equipment import WindingEquipment -from gdm.quantities import ActivePower, ReactivePower, Voltage +from gdm.quantities import ActivePower, Voltage from gdm.distribution.common.sequence_pair import SequencePair from gdm.distribution.enums import ConnectionType, VoltageTypes + class DistributionTransformerEquipmentMapper(CymeMapper): def __init__(self, system): super().__init__(system) - cyme_file = 'Equipment' - cyme_section = 'TRANSFORMER' + cyme_file = "Equipment" + cyme_section = "TRANSFORMER" - def parse(self, row, network_row): - + def parse(self, row, network_row, phase): name = self.map_name(row) pct_no_load_loss = self.map_pct_no_load_loss(row) pct_full_load_loss = self.map_pct_full_load_loss(row) is_center_tapped = self.map_is_center_tapped(row) - windings = self.map_windings(row, network_row, is_center_tapped) + windings = self.map_windings(row, network_row, is_center_tapped, phase) winding_reactances = self.map_winding_reactances(row, is_center_tapped) coupling_sequences = self.map_coupling(row, is_center_tapped) - return DistributionTransformerEquipment.model_construct(name=name, - pct_no_load_loss=pct_no_load_loss, - pct_full_load_loss=pct_full_load_loss, - windings=windings, - winding_reactances=winding_reactances, - is_center_tapped=is_center_tapped, - coupling_sequences=coupling_sequences) + return DistributionTransformerEquipment.model_construct( + name=name, + pct_no_load_loss=pct_no_load_loss, + pct_full_load_loss=pct_full_load_loss, + windings=windings, + winding_reactances=winding_reactances, + is_center_tapped=is_center_tapped, + coupling_sequences=coupling_sequences, + ) def map_name(self, row): - name = row['ID'] + name = row["ID"] return name def map_pct_no_load_loss(self, row): - no_load_loss = float(row['NoLoadLosses']) - kva = float(row['KVA']) - pct_no_load_loss = no_load_loss/kva*100 + no_load_loss = float(row["NoLoadLosses"]) + kva = float(row["KVA"]) + pct_no_load_loss = no_load_loss / kva * 100 return pct_no_load_loss def map_pct_full_load_loss(self, row): # Need to compute rated current and rated resistance to compute full load loss - rated_current_sec = float(row['KVA']) * 1000 / (float(row['KVLLsec']) * 1000) - resistance_pu = float(row['Z1']) / 100 / ((1 + float(row['XR'])**2)**0.5) - resistance_sec = resistance_pu * (float(row['KVLLsec'])**2 * 1000) / float(row['KVA']) - + rated_current_sec = float(row["KVA"]) * 1000 / (float(row["KVLLsec"]) * 1000) + resistance_pu = float(row["Z1"]) / 100 / ((1 + float(row["XR"]) ** 2) ** 0.5) + resistance_sec = resistance_pu * (float(row["KVLLsec"]) ** 2 * 1000) / float(row["KVA"]) + full_load_loss = rated_current_sec**2 * resistance_sec - pct_full_load_loss = 100 * full_load_loss / (float(row['KVA']) * 1000) + pct_full_load_loss = 100 * full_load_loss / (float(row["KVA"]) * 1000) return pct_full_load_loss def map_winding_reactances(self, row, is_center_tapped): - xr_ratio = float(row['XR']) + xr_ratio = float(row["XR"]) if xr_ratio == 0: xr_ratio = 0.01 - rx_ratio = 1/xr_ratio - reactance_pu = float(row['Z1']) / 100 / ((1+rx_ratio**2)**0.5) + rx_ratio = 1 / xr_ratio + reactance_pu = float(row["Z1"]) / 100 / ((1 + rx_ratio**2) ** 0.5) if is_center_tapped: winding_reactances = [reactance_pu, reactance_pu, reactance_pu] else: @@ -64,156 +67,161 @@ def map_winding_reactances(self, row, is_center_tapped): return winding_reactances def map_is_center_tapped(self, row): - transformer_type = row['Type'] - if transformer_type == '4': - return True + transformer_type = row["Type"] + # TODO Center tapped/Split phase not supported + # This will assign it properly but handling of buses needs to be developed + # if transformer_type == '4': + # return True + return False - def map_windings(self, row, network_row, is_center_tapped): + def map_windings(self, row, network_row, is_center_tapped, phase): windings = [] winding_mapper1 = WindingEquipmentMapper(self.system) - winding_1 = winding_mapper1.parse(row, network_row, winding_number=1) + winding_1 = winding_mapper1.parse(row, network_row, winding_number=1, phase=phase) winding_mapper2 = WindingEquipmentMapper(self.system) - winding_2 = winding_mapper2.parse(row, network_row, winding_number=2) + winding_2 = winding_mapper2.parse(row, network_row, winding_number=2, phase=phase) windings.append(winding_1) windings.append(winding_2) if is_center_tapped: winding_mapper3 = WindingEquipmentMapper(self.system) - winding_3 = winding_mapper3.parse(row, network_row, winding_number=3) + winding_3 = winding_mapper3.parse(row, network_row, winding_number=3, phase=phase) windings.append(winding_3) return windings - + def map_coupling(self, row, is_center_tapped): if is_center_tapped: - coupling = [SequencePair(0,1), - SequencePair(0,2), - SequencePair(1,2)] + coupling = [SequencePair(0, 1), SequencePair(0, 2), SequencePair(1, 2)] else: - coupling = [SequencePair(0,1)] + coupling = [SequencePair(0, 1)] return coupling + class WindingEquipmentMapper(CymeMapper): def __init__(self, system): super().__init__(system) - cyme_file = 'Equipment' - cyme_section = 'TRANSFORMER' + cyme_file = "Equipment" + cyme_section = "TRANSFORMER" connection_map = { - 0: 'Y_Y', - 1: 'D_Y', - 2: 'D_Y', - 3: 'YNG_YNG', - 4: 'D_D', - 5: 'DO_DO', - 6: 'YO_DO', - 7: 'D_YNG', - 8: 'YNG_D', - 9: 'Y_YNG', - 10: 'YNG_Y', - 11: 'Yg_Zg', - 12: 'D_Zg', - } - + 0: "Y_Y", + 1: "D_Y", + 2: "D_Y", + 3: "YNG_YNG", + 4: "D_D", + 5: "DO_DO", + 6: "YO_DO", + 7: "D_YNG", + 8: "YNG_D", + 9: "Y_YNG", + 10: "YNG_Y", + 11: "Yg_Zg", + 12: "D_Zg", + } + # The documentation is confusing on where the below connection types are used # It appears they are used in the equipment file although it is repoted in the network file connection_map = { - 0: 'Yg_Yg', - 1: 'D_Yg', - 2: 'D_D', - 3: 'Y_Y', - 4: 'DO_DO', - 5: 'YO_D', - 6: 'Yg_D', - 7: 'D_Y', - 8: 'Y_D', - 9: 'Yg_Y', - 10: 'Y_Yg', - 11: 'Yg_Zg', - 12: 'D_Zg', - 13: 'Zg_Yg', - 14: 'Zg_D', - 15: 'Yg_CT', - 16: 'D_CT', - 17: 'Yg_DCT', - 18: 'D_DCT', - 19: 'Y_DCT', - 20: 'DO_DOCT', - 21: 'YO_DOCT', - 22: 'DO_YO', - 23: 'Yg_Dn', - 24: 'Y_Dn', - 25: 'D_Dn', - 26: 'Zg_Dn', - 27: 'Dn_Yg', - 28: 'Dn_Y', - 29: 'Dn_D', - 30: 'Dn_Dn', - 31: 'Dn_Zg', - 99: 'Equip_Connection', - } - - def parse(self, row, network_row, winding_number): + 0: "Yg_Yg", + 1: "D_Yg", + 2: "D_D", + 3: "Y_Y", + 4: "DO_DO", + 5: "YO_D", + 6: "Yg_D", + 7: "D_Y", + 8: "Y_D", + 9: "Yg_Y", + 10: "Y_Yg", + 11: "Yg_Zg", + 12: "D_Zg", + 13: "Zg_Yg", + 14: "Zg_D", + 15: "Yg_CT", + 16: "D_CT", + 17: "Yg_DCT", + 18: "D_DCT", + 19: "Y_DCT", + 20: "DO_DOCT", + 21: "YO_DOCT", + 22: "DO_YO", + 23: "Yg_Dn", + 24: "Y_Dn", + 25: "D_Dn", + 26: "Zg_Dn", + 27: "Dn_Yg", + 28: "Dn_Y", + 29: "Dn_D", + 30: "Dn_Dn", + 31: "Dn_Zg", + 99: "Equip_Connection", + } + + def parse(self, row, network_row, winding_number, phase): name = self.map_name(row) resistance = self.map_resistance(row, winding_number) is_grounded = self.map_is_grounded(row, winding_number) rated_voltage = self.map_rated_voltage(row, winding_number) voltage_type = self.map_voltage_type(row, rated_voltage) rated_power = self.map_rated_power(row) - num_phases = self.map_num_phases(row) + num_phases = self.map_num_phases(phase) connection_type = self.map_connection_type(row, winding_number) - tap_positions = self.map_tap_positions(row, winding_number, network_row) + tap_positions = self.map_tap_positions(row, winding_number, network_row, phase) total_taps = self.map_total_taps(row) min_tap_pu = self.min_tap_pu(row) max_tap_pu = self.max_tap_pu(row) - return WindingEquipment.model_construct(name=name, - resistance=resistance, - is_grounded=is_grounded, - rated_voltage=rated_voltage, - voltage_type=voltage_type, - rated_power=rated_power, - num_phases=num_phases, - connection_type=connection_type, - tap_positions=tap_positions, - total_taps=total_taps, - min_tap_pu=min_tap_pu, - max_tap_pu=max_tap_pu) + return WindingEquipment.model_construct( + name=name, + resistance=resistance, + is_grounded=is_grounded, + rated_voltage=rated_voltage, + voltage_type=voltage_type, + rated_power=rated_power, + num_phases=num_phases, + connection_type=connection_type, + tap_positions=tap_positions, + total_taps=total_taps, + min_tap_pu=min_tap_pu, + max_tap_pu=max_tap_pu, + ) def map_name(self, row): - name = row['ID'] + name = row["ID"] return name def map_resistance(self, row, winding_number): - xr_ratio = float(row['XR']) - resistance_pu = float(row['Z1']) / 100 / ((1 + xr_ratio**2)**0.5) + xr_ratio = float(row["XR"]) + resistance_pu = float(row["Z1"]) / 100 / ((1 + xr_ratio**2) ** 0.5) if winding_number == 1: - resistance = resistance_pu * float(row['KVLLprim'])**2 / float(row['KVA']) + resistance = resistance_pu * float(row["KVLLprim"]) ** 2 / float(row["KVA"]) elif winding_number == 2: - resistance = resistance_pu * float(row['KVLLsec'])**2 / float(row['KVA']) + resistance = resistance_pu * float(row["KVLLsec"]) ** 2 / float(row["KVA"]) elif winding_number == 3: - resistance = resistance_pu * float(row['KVLLsec'])**2 / float(row['KVA']) + resistance = resistance_pu * float(row["KVLLsec"]) ** 2 / float(row["KVA"]) return resistance def map_is_grounded(self, row, winding_number): - - connection_type = row['Conn'] + connection_type = row["Conn"] if winding_number == 1: winding = 0 elif winding_number == 2: winding = 1 elif winding_number == 3: winding = 1 - if isinstance(connection_type, int) or (isinstance(connection_type, str) and connection_type.isdigit()): + if isinstance(connection_type, int) or ( + isinstance(connection_type, str) and connection_type.isdigit() + ): conn_type_int = int(connection_type) - winding_type = self.connection_map.get(conn_type_int, 'Y_Y').split('_')[winding] + winding_type = self.connection_map.get(conn_type_int, "Y_Y").split("_")[winding] else: winding_type = str(connection_type) - if 'YNG' in winding_type: + if "YNG" in winding_type: grounded = False - elif 'D' in winding_type: + elif "D" in winding_type: grounded = False - elif 'YO' in winding_type: + elif "YO" in winding_type: grounded = False else: grounded = True @@ -221,125 +229,117 @@ def map_is_grounded(self, row, winding_number): def map_rated_voltage(self, row, winding_number): if winding_number == 1: - voltage = row['KVLLprim'] + voltage = row["KVLLprim"] elif winding_number == 2: - voltage = row['KVLLsec'] + voltage = row["KVLLsec"] elif winding_number == 3: - voltage = row['KVLLsec'] - + voltage = row["KVLLsec"] + voltage = Voltage(float(voltage), "kilovolt") return voltage def map_voltage_type(self, row, rated_voltage): # This is from the CYME documentation but appears to not be entirely correct # Clearly L-L voltages still appear with a voltage type of 1 - if 'VoltageUnit' in row and (row["VoltageUnit"] == '1' or row["VoltageUnit"] == "3"): + if "VoltageUnit" in row and (row["VoltageUnit"] == "1" or row["VoltageUnit"] == "3"): return VoltageTypes.LINE_TO_GROUND return VoltageTypes.LINE_TO_LINE def map_rated_power(self, row): - power = row['KVA'] + power = row["KVA"] power = ActivePower(float(power), "kilowatt") return power - def map_num_phases(self, row): - phase_type = row['Type'] - if phase_type == 1 or phase_type == 4: - num_phases = 1 - else: - num_phases = 3 + def map_num_phases(self, phase): + num_phases = len(phase) return num_phases def map_connection_type(self, row, winding_number): - - connection_type = row['Conn'] + connection_type = row["Conn"] if winding_number == 1: winding = 0 elif winding_number == 2: winding = 1 elif winding_number == 3: winding = 1 - if isinstance(connection_type, int) or (isinstance(connection_type, str) and connection_type.isdigit()): + if isinstance(connection_type, int) or ( + isinstance(connection_type, str) and connection_type.isdigit() + ): conn_type_int = int(connection_type) - winding_type = self.connection_map.get(conn_type_int, 'Y_Y').split('_')[winding] + winding_type = self.connection_map.get(conn_type_int, "Y_Y").split("_")[winding] else: winding_type = str(connection_type) - if winding_type == 'YO': - connection_type = 'OPEN_STAR' - elif winding_type == 'DO': - connection_type = 'OPEN_DELTA' - elif 'Z' in winding_type: - connection_type = 'ZIG_ZAG' - elif 'Y' in winding_type: - connection_type = 'STAR' - elif 'D' in winding_type: - connection_type = 'DELTA' - elif 'DCT' == winding_type: - connection_type = 'DELTA' - elif 'CT' == winding_type: - connection_type = 'STAR' + if winding_type == "YO": + connection_type = "OPEN_STAR" + elif winding_type == "DO": + connection_type = "OPEN_DELTA" + elif "Z" in winding_type: + connection_type = "ZIG_ZAG" + elif "Y" in winding_type: + connection_type = "STAR" + elif "D" in winding_type: + connection_type = "DELTA" + elif "DCT" == winding_type: + connection_type = "DELTA" + elif "CT" == winding_type: + connection_type = "STAR" else: - connection_type = 'STAR' + connection_type = "STAR" return ConnectionType(connection_type) - def map_tap_positions(self, row, winding_number, network_row): - phase_type = row['Type'] - if phase_type == 1 or phase_type == 4: - num_phases = 1 - else: - num_phases = 3 + def map_tap_positions(self, row, winding_number, network_row, phase): + num_phases = len(phase) tap_positions = [] if winding_number == 1: if network_row is None: tap = 0.0 else: - tap = network_row.get('PrimTap', None) + tap = network_row.get("PrimTap", None) if tap is None: - tap = network_row.get('PrimaryTapSettingA', 0) + tap = network_row.get("PrimaryTapSettingA", 0) tap = float(tap) / 100 elif winding_number == 2: if network_row is None: tap = 0.0 else: - tap = network_row.get('SecTap', None) + tap = network_row.get("SecTap", None) if tap is None: - tap = network_row.get('SecondaryTapSettingA', 0) + tap = network_row.get("SecondaryTapSettingA", 0) tap = float(tap) / 100 elif winding_number == 3: if network_row is None: tap = 0.0 else: - tap = network_row.get('SecTap', None) + tap = network_row.get("SecTap", None) if tap is None: - tap = network_row.get('SecondaryTapSettingA', 0) + tap = network_row.get("SecondaryTapSettingA", 0) tap = float(tap) / 100 - if row['Taps'] == '' or row['Taps'] is None: + if row["Taps"] == "" or row["Taps"] is None: tap = 0.0 for phase in range(1, num_phases + 1): tap_positions.append(tap) return tap_positions def map_total_taps(self, row): - taps = row['Taps'] - if taps == '' or taps is None: + taps = row["Taps"] + if taps == "" or taps is None: taps = 0 total_taps = int(taps) return total_taps def min_tap_pu(self, row): - min_tap_pu = row['MinReg_Range'] - if min_tap_pu == '' or min_tap_pu is None: + min_tap_pu = row["MinReg_Range"] + if min_tap_pu == "" or min_tap_pu is None: return 0.9 - min_tap_pu = 1 - float(min_tap_pu)/100 + min_tap_pu = 1 - float(min_tap_pu) / 100 return float(min_tap_pu) def max_tap_pu(self, row): - max_tap_pu = row['MaxReg_Range'] - if max_tap_pu == '' or max_tap_pu is None: + max_tap_pu = row["MaxReg_Range"] + if max_tap_pu == "" or max_tap_pu is None: return 1.1 - max_tap_pu = 1 + float(max_tap_pu)/100 + max_tap_pu = 1 + float(max_tap_pu) / 100 return float(max_tap_pu) - diff --git a/src/ditto/readers/cyme/reader.py b/src/ditto/readers/cyme/reader.py index 6460689..acb8ded 100644 --- a/src/ditto/readers/cyme/reader.py +++ b/src/ditto/readers/cyme/reader.py @@ -17,10 +17,11 @@ from gdm.quantities import Voltage from gdm.distribution.enums import VoltageTypes + class Reader(AbstractReader): # Order of components is important component_types = [ - "DistributionBus", # First as other components connect to buses + "DistributionBus", # First as other components connect to buses "DistributionVoltageSource", "MatrixImpedanceRecloserEquipment", "MatrixImpedanceRecloser", @@ -39,35 +40,59 @@ class Reader(AbstractReader): "DistributionTransformerByPhase", "DistributionTransformer", "DistributionTransformerThreeWinding", - "MatrixImpedanceBranch", # This must be last as it includes a catch-all for unrecognized branches + "MatrixImpedanceBranch", # This must be last as it includes a catch-all for unrecognized branches ] validation_errors = [] - def __init__(self, network_file, equipment_file, load_file, load_model_id = None, substation_names=None, feeder_names=None): + def __init__( + self, + network_file, + equipment_file, + load_file, + load_model_id=None, + substation_names=None, + feeder_names=None, + ): self.system = DistributionSystem(auto_add_composed_components=True) - self.read(network_file, equipment_file, load_file, load_model_id, substation_names=substation_names, feeder_names=feeder_names) - - def read(self, network_file, equipment_file, load_file, load_model_id = None, substation_names=None, feeder_names=None): + self.read( + network_file, + equipment_file, + load_file, + load_model_id, + substation_names=substation_names, + feeder_names=feeder_names, + ) + def read( + self, + network_file, + equipment_file, + load_file, + load_model_id=None, + substation_names=None, + feeder_names=None, + ): # Section data read separately as it links to other tables section_id_sections = {} from_node_sections = {} to_node_sections = {} - phase_elements = set([ - "MatrixImpedanceBranchEquipmentMapper", - "MatrixImpedanceRecloserEquipmentMapper", - "MatrixImpedanceSwitchEquipmentMapper", - "MatrixImpedanceFuseEquipmentMapper", - ]) + phase_elements = set( + [ + "MatrixImpedanceBranchEquipmentMapper", + "MatrixImpedanceRecloserEquipmentMapper", + "MatrixImpedanceSwitchEquipmentMapper", + "MatrixImpedanceFuseEquipmentMapper", + ] + ) default_conductor = BareConductorEquipment( name="Default", - conductor_diameter=Distance(0.368000,'inch').to('mm'), - conductor_gmr=Distance(0.133200,'inch').to('mm'), - ampacity=Current(600.0,'amp'), - emergency_ampacity=Current(600.0,'amp'), - ac_resistance=ResistancePULength(0.555000,'ohm/mile').to('ohm/km'), - dc_resistance=ResistancePULength(0.555000,'ohm/mile').to('ohm/km'), + conductor_diameter=Distance(0.368000, "inch").to("mm"), + conductor_gmr=Distance(0.133200, "inch").to("mm"), + ampacity=Current(600.0, "amp"), + emergency_ampacity=Current(600.0, "amp"), + ac_resistance=ResistancePULength(0.555000, "ohm/mile").to("ohm/km"), + dc_resistance=ResistancePULength(0.555000, "ohm/mile").to("ohm/km"), ) self.system.add_component(default_conductor) @@ -77,11 +102,27 @@ def read(self, network_file, equipment_file, load_file, load_model_id = None, su load_record = {} used_sections = set() - section_data = read_cyme_data(network_file,"SECTION", node_feeder_map=node_feeder_map, network_voltage_map=network_voltage_map, node_substation_map=node_substation_map, parse_feeders=True, parse_substation=True) + section_data = read_cyme_data( + network_file, + "SECTION", + node_feeder_map=node_feeder_map, + network_voltage_map=network_voltage_map, + node_substation_map=node_substation_map, + parse_feeders=True, + parse_substation=True, + ) section_id_sections = section_data.set_index("SectionID").to_dict(orient="index") - from_node_sections = section_data.groupby("FromNodeID").apply(lambda df: df.to_dict(orient="records")).to_dict() - to_node_sections = section_data.groupby("ToNodeID").apply(lambda df: df.to_dict(orient="records")).to_dict() + from_node_sections = ( + section_data.groupby("FromNodeID") + .apply(lambda df: df.to_dict(orient="records")) + .to_dict() + ) + to_node_sections = ( + section_data.groupby("ToNodeID") + .apply(lambda df: df.to_dict(orient="records")) + .to_dict() + ) for component_type in self.component_types: logger.info(f"Parsing Type: {component_type}") @@ -97,9 +138,10 @@ def read(self, network_file, equipment_file, load_file, load_model_id = None, su if isinstance(all_cyme_sections, str): all_cyme_sections = [all_cyme_sections] if not isinstance(all_cyme_sections, list): - raise ValueError(f"cyme_section must be a string or list of strings. Got {type(all_cyme_sections)}") + raise ValueError( + f"cyme_section must be a string or list of strings. Got {type(all_cyme_sections)}" + ) for cyme_section in all_cyme_sections: - data = None if cyme_file == "Network": data = read_cyme_data(network_file, cyme_section) @@ -108,62 +150,110 @@ def read(self, network_file, equipment_file, load_file, load_model_id = None, su elif cyme_file == "Load": data = read_cyme_data(load_file, cyme_section) if load_model_id is not None: - data = data[data['LoadModelID'] == load_model_id] + data = data[data["LoadModelID"] == load_model_id] logger.info(f"Filtered Load data by LoadModelID: {load_model_id}") else: - if len(data['LoadModelID'].unique()) > 1: - raise ValueError(f"Multiple LoadModelIDs found in load data: {data['LoadModelID'].unique()}. Please specify load_model_id") + if len(data["LoadModelID"].unique()) > 1: + raise ValueError( + f"Multiple LoadModelIDs found in load data: {data['LoadModelID'].unique()}. Please specify load_model_id" + ) else: raise ValueError(f"Unknown CYME file {cyme_file}") argument_handler = { - "DistributionCapacitorMapper": lambda: [section_id_sections, read_cyme_data(equipment_file, "SHUNT CAPACITOR", index_col='ID')], - "DistributionBusMapper": lambda: [from_node_sections, to_node_sections, node_feeder_map, node_substation_map], + "DistributionCapacitorMapper": lambda: [ + section_id_sections, + read_cyme_data(equipment_file, "SHUNT CAPACITOR", index_col="ID"), + ], + "DistributionBusMapper": lambda: [ + from_node_sections, + to_node_sections, + node_feeder_map, + node_substation_map, + ], "DistributionVoltageSourceMapper": lambda: [], - "DistributionLoadMapper": lambda: [section_id_sections, read_cyme_data(load_file, "LOADS", index_col='DeviceNumber'), load_record], + "DistributionLoadMapper": lambda: [ + section_id_sections, + read_cyme_data(load_file, "LOADS", index_col="DeviceNumber"), + load_record, + ], "GeometryBranchMapper": lambda: [used_sections, section_id_sections], "GeometryBranchByPhaseMapper": lambda: [used_sections, section_id_sections], "BareConductorEquipmentMapper": lambda: [], - "GeometryBranchEquipmentMapper": lambda: [read_cyme_data(equipment_file,"SPACING TABLE FOR LINE", index_col='ID')], + "GeometryBranchEquipmentMapper": lambda: [ + read_cyme_data(equipment_file, "SPACING TABLE FOR LINE", index_col="ID") + ], "MatrixImpedanceSwitchEquipmentMapper": lambda: [], "MatrixImpedanceSwitchMapper": lambda: [used_sections, section_id_sections], "MatrixImpedanceFuseEquipmentMapper": lambda: [], "MatrixImpedanceFuseMapper": lambda: [used_sections, section_id_sections], "MatrixImpedanceRecloserEquipmentMapper": lambda: [], "MatrixImpedanceRecloserMapper": lambda: [used_sections, section_id_sections], - "GeometryBranchByPhaseEquipmentMapper": lambda: [read_cyme_data(equipment_file,"SPACING TABLE FOR LINE", index_col='ID')], - "DistributionTransformerThreeWindingMapper": lambda: [used_sections, section_id_sections, read_cyme_data(equipment_file, "THREE WINDING TRANSFORMER", index_col='ID').to_dict("index")], - "DistributionTransformerMapper": lambda: [used_sections, section_id_sections, read_cyme_data(equipment_file, "TRANSFORMER", index_col='ID').to_dict("index")], - "DistributionTransformerByPhaseMapper": lambda: [used_sections, section_id_sections, read_cyme_data(equipment_file, "TRANSFORMER", index_col='ID').to_dict("index")], + "GeometryBranchByPhaseEquipmentMapper": lambda: [ + read_cyme_data(equipment_file, "SPACING TABLE FOR LINE", index_col="ID") + ], + "DistributionTransformerThreeWindingMapper": lambda: [ + used_sections, + section_id_sections, + read_cyme_data( + equipment_file, "THREE WINDING TRANSFORMER", index_col="ID" + ).to_dict("index"), + ], + "DistributionTransformerMapper": lambda: [ + used_sections, + section_id_sections, + read_cyme_data(equipment_file, "TRANSFORMER", index_col="ID").to_dict( + "index" + ), + ], + "DistributionTransformerByPhaseMapper": lambda: [ + used_sections, + section_id_sections, + read_cyme_data(equipment_file, "TRANSFORMER", index_col="ID").to_dict( + "index" + ), + ], "MatrixImpedanceBranchEquipmentMapper": lambda: [], - "MatrixImpedanceBranchMapper": lambda: [used_sections, section_id_sections, cyme_section], + "MatrixImpedanceBranchMapper": lambda: [ + used_sections, + section_id_sections, + cyme_section, + ], } args = argument_handler.get(mapper_name, lambda: [])() def parse_row(row): - model_entry = mapper.parse(row, *args) - return model_entry + model_entry = mapper.parse(row, *args) + return model_entry if mapper_name in phase_elements: phases = [] - for phase in ['A','B','C']: + for phase in ["A", "B", "C"]: phases.append(phase) args = [phases] components = data.apply(parse_row, axis=1) components = [c for c in components if c is not None] - components = [item for c in components for item in (c if isinstance(c, list) else [c])] + components = [ + item + for c in components + for item in (c if isinstance(c, list) else [c]) + ] self.system.add_components(*components) else: components = data.apply(parse_row, axis=1) components = [c for c in components if c is not None] - components = [item for c in components for item in (c if isinstance(c, list) else [c])] + components = [ + item for c in components for item in (c if isinstance(c, list) else [c]) + ] self.system.add_components(*components) self.system = self.assign_bus_voltages(network_dist_sys=self.system) if substation_names is not None or feeder_names is not None: - self.system = network_truncation(self.system, substation_names=substation_names, feeder_names=feeder_names) + self.system = network_truncation( + self.system, substation_names=substation_names, feeder_names=feeder_names + ) print("Finished truncation") for component_type in self.system.get_component_types(): @@ -191,7 +281,6 @@ def _add_components(self, components: list[Component]): ] ) - def _validate_model(self): if self.validation_errors: error_table = Table(title="Validation warning summary") @@ -213,11 +302,8 @@ def _validate_model(self): def get_system(self) -> DistributionSystem: return self.system - - def assign_bus_voltages(self, network_dist_sys=None): - bus_queue = set() observed_buses = set() observed_components = set() @@ -232,9 +318,12 @@ def assign_bus_voltages(self, network_dist_sys=None): voltage_sources = list(self.system.get_components(DistributionVoltageSource)) - for vsource in voltage_sources: - vsource.bus.rated_voltage = vsource.equipment.sources[0].voltage * 1.732 if len(vsource.phases) > 1 else vsource.equipment[0].voltage + vsource.bus.rated_voltage = ( + vsource.equipment.sources[0].voltage * 1.732 + if len(vsource.phases) > 1 + else vsource.equipment[0].voltage + ) bus_queue.add(vsource.bus.name) while bus_queue: current_bus_name = bus_queue.pop() @@ -254,28 +343,24 @@ def assign_bus_voltages(self, network_dist_sys=None): component_type = obj.__class__.__name__ for j, bus in enumerate(obj.buses): - if (bus.name == current_bus.name): + if bus.name == current_bus.name: continue - if component_type == 'DistributionTransformer': + if component_type == "DistributionTransformer": for i, winding in enumerate(obj.equipment.windings): voltage = winding.rated_voltage voltage_type = winding.voltage_type - if (voltage_type == VoltageTypes.LINE_TO_GROUND) and ((voltage == Voltage(12.47, 'kilovolt')) or (voltage == Voltage(12.0, 'kilovolt')) or (voltage == Voltage(0.208, 'kilovolt'))): - # Hacked in but no better way found yet - winding.voltage_type = VoltageTypes.LINE_TO_LINE voltage_type = winding.voltage_type if i == j: if voltage != current_voltage: bus.voltage_type = voltage_type bus.rated_voltage = voltage - if (not bus.name in observed_buses): + if bus.name not in observed_buses: bus_queue.add(bus.name) else: bus.rated_voltage = current_voltage bus.voltage_type = current_voltage_type - if (not bus.name in observed_buses): + if bus.name not in observed_buses: bus_queue.add(bus.name) return network_dist_sys - From 9e5a8db28eccf38ce91b8d52bc01b6726bfbe5ac Mon Sep 17 00:00:00 2001 From: "Zink, Zephyr" Date: Fri, 16 Jan 2026 14:41:23 -0700 Subject: [PATCH 47/50] bug fix --- .../readers/cyme/components/distribution_transformer.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ditto/readers/cyme/components/distribution_transformer.py b/src/ditto/readers/cyme/components/distribution_transformer.py index fce2112..dc08016 100644 --- a/src/ditto/readers/cyme/components/distribution_transformer.py +++ b/src/ditto/readers/cyme/components/distribution_transformer.py @@ -148,14 +148,15 @@ def map_winding_phases(self, row, phase, equipment_row): ) equipment_row = {"Type": 2} windings_list = [] - num_windings = 3 + # TODO Center tapped/Split phase not supported # This will assign it properly but handling of buses needs to be developed + # num_windings = 3 # if equipment_row['Type'] == "4": # num_windings = 3 # else: # num_windings = 2 - # num_windings = 2 + num_windings = 2 for i in range(num_windings): winding_phases = [] if "1" == phase: From 708a0c014cfff91432482e512a7ab7fa8dc7f616 Mon Sep 17 00:00:00 2001 From: "Zink, Zephyr" Date: Tue, 10 Feb 2026 12:47:01 -0700 Subject: [PATCH 48/50] aligning values with GDM definitions --- .../distribution_transformer_equipment.py | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/ditto/readers/cyme/equipment/distribution_transformer_equipment.py b/src/ditto/readers/cyme/equipment/distribution_transformer_equipment.py index 93a9d35..09b279b 100644 --- a/src/ditto/readers/cyme/equipment/distribution_transformer_equipment.py +++ b/src/ditto/readers/cyme/equipment/distribution_transformer_equipment.py @@ -61,9 +61,9 @@ def map_winding_reactances(self, row, is_center_tapped): rx_ratio = 1 / xr_ratio reactance_pu = float(row["Z1"]) / 100 / ((1 + rx_ratio**2) ** 0.5) if is_center_tapped: - winding_reactances = [reactance_pu, reactance_pu, reactance_pu] + winding_reactances = [reactance_pu * 100, reactance_pu * 100, reactance_pu * 100] else: - winding_reactances = [reactance_pu] + winding_reactances = [reactance_pu * 100] return winding_reactances def map_is_center_tapped(self, row): @@ -199,7 +199,7 @@ def map_resistance(self, row, winding_number): resistance = resistance_pu * float(row["KVLLsec"]) ** 2 / float(row["KVA"]) elif winding_number == 3: resistance = resistance_pu * float(row["KVLLsec"]) ** 2 / float(row["KVA"]) - return resistance + return resistance_pu * 100 def map_is_grounded(self, row, winding_number): connection_type = row["Conn"] @@ -294,31 +294,33 @@ def map_tap_positions(self, row, winding_number, network_row, phase): tap_positions = [] if winding_number == 1: if network_row is None: - tap = 0.0 + tap = 1.0 else: tap = network_row.get("PrimTap", None) if tap is None: - tap = network_row.get("PrimaryTapSettingA", 0) + tap = network_row.get("PrimaryTapSettingA", 100) tap = float(tap) / 100 elif winding_number == 2: if network_row is None: - tap = 0.0 + tap = 1.0 else: tap = network_row.get("SecTap", None) if tap is None: - tap = network_row.get("SecondaryTapSettingA", 0) + tap = network_row.get("SecondaryTapSettingA", 100) tap = float(tap) / 100 elif winding_number == 3: if network_row is None: - tap = 0.0 + tap = 1.0 else: tap = network_row.get("SecTap", None) if tap is None: - tap = network_row.get("SecondaryTapSettingA", 0) + tap = network_row.get( + "SecondaryTapSettingA", + ) tap = float(tap) / 100 if row["Taps"] == "" or row["Taps"] is None: - tap = 0.0 + tap = 1.0 for phase in range(1, num_phases + 1): tap_positions.append(tap) return tap_positions @@ -326,7 +328,7 @@ def map_tap_positions(self, row, winding_number, network_row, phase): def map_total_taps(self, row): taps = row["Taps"] if taps == "" or taps is None: - taps = 0 + taps = 32 total_taps = int(taps) return total_taps From 6a796849c358d97aee9e44e05dbb05237957c171 Mon Sep 17 00:00:00 2001 From: "Zink, Zephyr" Date: Fri, 20 Feb 2026 17:49:41 -0700 Subject: [PATCH 49/50] read and assign voltage complexity --- src/ditto/readers/cyme/reader.py | 117 +++++++++++++++++-------------- 1 file changed, 64 insertions(+), 53 deletions(-) diff --git a/src/ditto/readers/cyme/reader.py b/src/ditto/readers/cyme/reader.py index acb8ded..fe55aea 100644 --- a/src/ditto/readers/cyme/reader.py +++ b/src/ditto/readers/cyme/reader.py @@ -14,8 +14,6 @@ from gdm.distribution.components.distribution_bus import DistributionBus from gdm.distribution.components import DistributionVoltageSource -from gdm.quantities import Voltage -from gdm.distribution.enums import VoltageTypes class Reader(AbstractReader): @@ -129,6 +127,7 @@ def read( mapper_name = component_type + "Mapper" if not hasattr(cyme_mapper, mapper_name): logger.warning(f"Mapper {mapper_name} not found. Skipping.") + continue mapper = getattr(cyme_mapper, mapper_name)(self.system) @@ -137,28 +136,11 @@ def read( all_cyme_sections = mapper.cyme_section if isinstance(all_cyme_sections, str): all_cyme_sections = [all_cyme_sections] - if not isinstance(all_cyme_sections, list): - raise ValueError( - f"cyme_section must be a string or list of strings. Got {type(all_cyme_sections)}" - ) + for cyme_section in all_cyme_sections: - data = None - if cyme_file == "Network": - data = read_cyme_data(network_file, cyme_section) - elif cyme_file == "Equipment": - data = read_cyme_data(equipment_file, cyme_section) - elif cyme_file == "Load": - data = read_cyme_data(load_file, cyme_section) - if load_model_id is not None: - data = data[data["LoadModelID"] == load_model_id] - logger.info(f"Filtered Load data by LoadModelID: {load_model_id}") - else: - if len(data["LoadModelID"].unique()) > 1: - raise ValueError( - f"Multiple LoadModelIDs found in load data: {data['LoadModelID'].unique()}. Please specify load_model_id" - ) - else: - raise ValueError(f"Unknown CYME file {cyme_file}") + data = self._prepare_data( + cyme_file, cyme_section, load_model_id, network_file, equipment_file, load_file + ) argument_handler = { "DistributionCapacitorMapper": lambda: [ @@ -248,7 +230,7 @@ def parse_row(row): ] self.system.add_components(*components) - self.system = self.assign_bus_voltages(network_dist_sys=self.system) + self.assign_bus_voltages() if substation_names is not None or feeder_names is not None: self.system = network_truncation( @@ -300,35 +282,41 @@ def _validate_model(self): "Validations errors occured when running the script. See the table above" ) + def _prepare_data( + self, cyme_file, cyme_section, load_model_id, network_file, equipment_file, load_file + ): + if cyme_file == "Network": + data = read_cyme_data(network_file, cyme_section) + elif cyme_file == "Equipment": + data = read_cyme_data(equipment_file, cyme_section) + elif cyme_file == "Load": + data = read_cyme_data(load_file, cyme_section) + if load_model_id is not None: + data = data[data["LoadModelID"] == load_model_id] + logger.info(f"Filtered Load data by LoadModelID: {load_model_id}") + else: + if len(data["LoadModelID"].unique()) > 1: + raise ValueError( + f"Multiple LoadModelIDs found in load data: {data['LoadModelID'].unique()}. Please specify load_model_id" + ) + else: + raise ValueError(f"Unknown CYME file {cyme_file}") + + return data + def get_system(self) -> DistributionSystem: return self.system - def assign_bus_voltages(self, network_dist_sys=None): - bus_queue = set() + def assign_bus_voltages(self): observed_buses = set() observed_components = set() - bus_obj_map = defaultdict(list) - for component_type in self.system.get_component_types(): - component_list = list(self.system.get_components(component_type)) - for comp in component_list: - if hasattr(comp, "buses"): - for bus in comp.buses: - bus_obj_map[bus.name].append(comp) + bus_obj_map = self._create_bus_obj_map() - voltage_sources = list(self.system.get_components(DistributionVoltageSource)) + bus_queue = self._start_queue_w_voltage_sources() - for vsource in voltage_sources: - vsource.bus.rated_voltage = ( - vsource.equipment.sources[0].voltage * 1.732 - if len(vsource.phases) > 1 - else vsource.equipment[0].voltage - ) - bus_queue.add(vsource.bus.name) while bus_queue: current_bus_name = bus_queue.pop() - if current_bus_name in observed_buses: - continue current_bus = self.system.get_component(DistributionBus, name=current_bus_name) current_voltage = current_bus.rated_voltage @@ -345,22 +333,45 @@ def assign_bus_voltages(self, network_dist_sys=None): for j, bus in enumerate(obj.buses): if bus.name == current_bus.name: continue + if component_type == "DistributionTransformer": for i, winding in enumerate(obj.equipment.windings): voltage = winding.rated_voltage voltage_type = winding.voltage_type - voltage_type = winding.voltage_type - if i == j: - if voltage != current_voltage: - bus.voltage_type = voltage_type - bus.rated_voltage = voltage - if bus.name not in observed_buses: - bus_queue.add(bus.name) + if i == j and voltage != current_voltage: + bus.voltage_type = voltage_type + bus.rated_voltage = voltage + else: bus.rated_voltage = current_voltage bus.voltage_type = current_voltage_type - if bus.name not in observed_buses: - bus_queue.add(bus.name) - return network_dist_sys + if bus.name not in observed_buses: + bus_queue.add(bus.name) + + def _start_queue_w_voltage_sources(self): + bus_queue = set() + + voltage_sources = list(self.system.get_components(DistributionVoltageSource)) + + for vsource in voltage_sources: + vsource.bus.rated_voltage = ( + vsource.equipment.sources[0].voltage * 1.732 + if len(vsource.phases) > 1 + else vsource.equipment[0].voltage + ) + bus_queue.add(vsource.bus.name) + + return bus_queue + + def _create_bus_obj_map(self): + bus_obj_map = defaultdict(list) + for component_type in self.system.get_component_types(): + component_list = list(self.system.get_components(component_type)) + for comp in component_list: + if hasattr(comp, "buses"): + for bus in comp.buses: + bus_obj_map[bus.name].append(comp) + + return bus_obj_map From 8e3489ca41dc496be820442dfc93e8db6d539dc5 Mon Sep 17 00:00:00 2001 From: "Zink, Zephyr" Date: Tue, 24 Feb 2026 15:31:06 -0700 Subject: [PATCH 50/50] ruff and comment fixes for CYME Reader --- src/ditto/readers/cyme/__init__.py | 50 ++- .../cyme/components/distribution_bus.py | 66 ++-- .../cyme/components/distribution_capacitor.py | 64 ++-- .../cyme/components/distribution_load.py | 77 ++-- .../cyme/components/geometry_branch.py | 75 +--- .../components/matrix_impedance_branch.py | 108 +++--- .../cyme/components/matrix_impedance_fuse.py | 54 ++- .../components/matrix_impedance_recloser.py | 63 ++-- .../components/matrix_impedance_switch.py | 53 ++- src/ditto/readers/cyme/constants.py | 27 ++ .../cyme/equipment/capacitor_equipment.py | 38 +- .../distribution_transformer_equipment.py | 50 +-- ...ion_transformer_three_winding_equipment.py | 319 +++++++++------- .../readers/cyme/equipment/geometry_branch.py | 23 -- .../equipment/geometry_branch_equipment.py | 350 +++++++----------- .../readers/cyme/equipment/load_equipment.py | 88 ++--- .../matrix_impedance_branch_equipment.py | 66 ++-- .../matrix_impedance_fuse_equipment.py | 53 ++- .../matrix_impedance_recloser_equipment.py | 52 ++- .../matrix_impedance_switch_equipment.py | 55 ++- src/ditto/readers/cyme/reader.py | 26 +- src/ditto/readers/cyme/utils.py | 185 ++++++--- tests/test_cyme/test_cyme_reader.py | 13 +- 23 files changed, 939 insertions(+), 1016 deletions(-) create mode 100644 src/ditto/readers/cyme/constants.py delete mode 100644 src/ditto/readers/cyme/equipment/geometry_branch.py diff --git a/src/ditto/readers/cyme/__init__.py b/src/ditto/readers/cyme/__init__.py index b5c0560..0d4db49 100644 --- a/src/ditto/readers/cyme/__init__.py +++ b/src/ditto/readers/cyme/__init__.py @@ -2,25 +2,45 @@ from ditto.readers.cyme.components.distribution_capacitor import DistributionCapacitorMapper from ditto.readers.cyme.components.distribution_load import DistributionLoadMapper from ditto.readers.cyme.equipment.geometry_branch_equipment import BareConductorEquipmentMapper -from ditto.readers.cyme.equipment.geometry_branch_equipment import ConcentricCableEquipmentMapper from ditto.readers.cyme.equipment.geometry_branch_equipment import GeometryBranchEquipmentMapper -from ditto.readers.cyme.equipment.matrix_impedance_branch_equipment import MatrixImpedanceBranchEquipmentMapper -from ditto.readers.cyme.equipment.geometry_branch_equipment import GeometryBranchByPhaseEquipmentMapper -from ditto.readers.cyme.components.matrix_impedance_branch import MatrixImpedanceBranchMapper +from ditto.readers.cyme.equipment.matrix_impedance_branch_equipment import ( + MatrixImpedanceBranchEquipmentMapper, +) +from ditto.readers.cyme.equipment.geometry_branch_equipment import ( + GeometryBranchByPhaseEquipmentMapper, +) from ditto.readers.cyme.components.geometry_branch import GeometryBranchMapper -from ditto.readers.cyme.components.geometry_branch import GeometryBranchByPhaseMapper -from ditto.readers.cyme.equipment.distribution_transformer_equipment import DistributionTransformerEquipmentMapper +from ditto.readers.cyme.equipment.distribution_transformer_equipment import ( + DistributionTransformerEquipmentMapper, +) from ditto.readers.cyme.equipment.distribution_transformer_equipment import WindingEquipmentMapper -from ditto.readers.cyme.equipment.distribution_transformer_three_winding_equipment import DistributionTransformerThreeWindingEquipmentMapper -from ditto.readers.cyme.equipment.distribution_transformer_three_winding_equipment import ThreeWindingEquipmentMapper -from ditto.readers.cyme.components.distribution_transformer import DistributionTransformerByPhaseMapper, DistributionTransformerMapper, DistributionTransformerThreeWindingMapper +from ditto.readers.cyme.equipment.distribution_transformer_three_winding_equipment import ( + DistributionTransformerThreeWindingEquipmentMapper, +) +from ditto.readers.cyme.equipment.distribution_transformer_three_winding_equipment import ( + ThreeWindingEquipmentMapper, +) +from ditto.readers.cyme.components.distribution_transformer import ( + DistributionTransformerByPhaseMapper, + DistributionTransformerMapper, + DistributionTransformerThreeWindingMapper, +) from ditto.readers.cyme.components.matrix_impedance_switch import MatrixImpedanceSwitchMapper -from ditto.readers.cyme.equipment.matrix_impedance_switch_equipment import MatrixImpedanceSwitchEquipmentMapper +from ditto.readers.cyme.equipment.matrix_impedance_switch_equipment import ( + MatrixImpedanceSwitchEquipmentMapper, +) from ditto.readers.cyme.components.matrix_impedance_fuse import MatrixImpedanceFuseMapper -from ditto.readers.cyme.equipment.matrix_impedance_fuse_equipment import MatrixImpedanceFuseEquipmentMapper +from ditto.readers.cyme.equipment.matrix_impedance_fuse_equipment import ( + MatrixImpedanceFuseEquipmentMapper, +) from ditto.readers.cyme.components.matrix_impedance_recloser import MatrixImpedanceRecloserMapper -from ditto.readers.cyme.equipment.matrix_impedance_recloser_equipment import MatrixImpedanceRecloserEquipmentMapper +from ditto.readers.cyme.equipment.matrix_impedance_recloser_equipment import ( + MatrixImpedanceRecloserEquipmentMapper, +) from ditto.readers.cyme.components.matrix_impedance_branch import MatrixImpedanceBranchMapper -from ditto.readers.cyme.components.distribution_voltage_source import DistributionVoltageSourceMapper -from ditto.readers.cyme.equipment.phase_voltagesource_equipment import PhaseVoltageSourceEquipmentMapper - +from ditto.readers.cyme.components.distribution_voltage_source import ( + DistributionVoltageSourceMapper, +) +from ditto.readers.cyme.equipment.phase_voltagesource_equipment import ( + PhaseVoltageSourceEquipmentMapper, +) diff --git a/src/ditto/readers/cyme/components/distribution_bus.py b/src/ditto/readers/cyme/components/distribution_bus.py index cc7322d..924de32 100644 --- a/src/ditto/readers/cyme/components/distribution_bus.py +++ b/src/ditto/readers/cyme/components/distribution_bus.py @@ -9,47 +9,48 @@ class DistributionBusMapper(CymeMapper): def __init__(self, cyme_model): super().__init__(cyme_model) - cyme_file = 'Network' - cyme_section = 'NODE' + cyme_file = "Network" + cyme_section = "NODE" - def parse(self, row, from_node_sections, to_node_sections, node_feeder_map, node_substation_map): + def parse( + self, row, from_node_sections, to_node_sections, node_feeder_map, node_substation_map + ): name = self.map_name(row) feeder = node_feeder_map.get(name, None) substation = node_substation_map.get(name, None) - + coordinate = self.map_coordinate(row) phases = self.map_phases(row, from_node_sections, to_node_sections) rated_voltage = self.map_rated_voltage(row) voltage_limits = self.map_voltagelimits(row) voltage_type = self.map_voltage_type(row) - return DistributionBus.model_construct(name=name, - coordinate=coordinate, - rated_voltage=rated_voltage, - feeder=feeder, - substation=substation, - phases=phases, - voltagelimits=voltage_limits, - voltage_type=voltage_type) + return DistributionBus.model_construct( + name=name, + coordinate=coordinate, + rated_voltage=rated_voltage, + feeder=feeder, + substation=substation, + phases=phases, + voltagelimits=voltage_limits, + voltage_type=voltage_type, + ) def map_name(self, row): - name = row['NodeID'] + name = row["NodeID"] return name def map_coordinate(self, row): - try: - X, Y = float(row["CoordX"]), float(row["CoordY"]) - except: - X, Y = float(row["CoordX1"]), float(row["CoordY1"]) - crs = None - return Location(x=X, y=Y, crs=crs) + x_key = "CoordX" if "CoordX" in row and row["CoordX"] != "" else "CoordX1" + y_key = "CoordY" if "CoordY" in row and row["CoordY"] != "" else "CoordY1" + # CRS is not provided in the Cyme data + return Location(x=float(row[x_key]), y=float(row[y_key]), crs=None) def map_rated_voltage(self, row): - #return PositiveVoltage(float(row['UserDefinedBaseVoltage']), "kilovolts") + # Placehoder voltage until assign_bus_voltages assigns voltages based on network traversal and transformer ratings return Voltage(float(12.47), "kilovolts") def map_phases(self, row, from_node_sections, to_node_sections): node_id = row["NodeID"] - section = None all_phases = set() if node_id in from_node_sections: for section in from_node_sections[node_id]: @@ -62,30 +63,21 @@ def map_phases(self, row, from_node_sections, to_node_sections): for phase in phases: all_phases.add(phase) - all_phases = sorted(list(all_phases)) - phases = [] - if "A" in all_phases: - phases.append(Phase.A) - if "B" in all_phases: - phases.append(Phase.B) - if "C" in all_phases: - phases.append(Phase.C) - if "N" in all_phases: - phases.append(Phase.N) - return phases - + phase_map = {"A": Phase.A, "B": Phase.B, "C": Phase.C, "N": Phase.N} + return [phase_map[p] for p in sorted(all_phases) if p in phase_map] def map_voltagelimits(self, row): low_voltage = None high_voltage = None - if row['LowVoltageLimit'] != '': - low_voltage = Voltage(row['LowVoltageLimit'], "kilovolts") - if row['HighVoltageLimit'] != '': - high_voltage = Voltage(row['HighVoltageLimit'], "kilovolts") + if row["LowVoltageLimit"] != "": + low_voltage = Voltage(row["LowVoltageLimit"], "kilovolts") + if row["HighVoltageLimit"] != "": + high_voltage = Voltage(row["HighVoltageLimit"], "kilovolts") if low_voltage is not None and high_voltage is not None: return [low_voltage, high_voltage] else: return [] def map_voltage_type(self, row): + # Defined later in assigne_bus_voltages based on network traversal and transformer ratings return VoltageTypes.LINE_TO_LINE diff --git a/src/ditto/readers/cyme/components/distribution_capacitor.py b/src/ditto/readers/cyme/components/distribution_capacitor.py index 4127cfa..92f5d7b 100644 --- a/src/ditto/readers/cyme/components/distribution_capacitor.py +++ b/src/ditto/readers/cyme/components/distribution_capacitor.py @@ -1,4 +1,3 @@ -from ditto.readers.cyme.utils import read_cyme_data from ditto.readers.cyme.cyme_mapper import CymeMapper from ditto.readers.cyme.equipment.capacitor_equipment import CapacitorEquipmentMapper from gdm.distribution.components.distribution_bus import DistributionBus @@ -6,12 +5,13 @@ from gdm.distribution.enums import Phase from loguru import logger + class DistributionCapacitorMapper(CymeMapper): def __init__(self, system): super().__init__(system) - cyme_file = 'Network' - cyme_section = 'SHUNT CAPACITOR SETTING' + cyme_file = "Network" + cyme_section = "SHUNT CAPACITOR SETTING" def parse(self, row, section_id_sections, equipment_data): name = self.map_name(row) @@ -20,29 +20,33 @@ def parse(self, row, section_id_sections, equipment_data): controllers = self.map_controllers(row) equipment = self.map_equipment(row, equipment_data) in_service = self.map_in_service(row) - return DistributionCapacitor.model_construct(name=name, - bus=bus, - phases=phases, - controllers=controllers, - equipment=equipment, - in_service=in_service) + return DistributionCapacitor.model_construct( + name=name, + bus=bus, + phases=phases, + controllers=controllers, + equipment=equipment, + in_service=in_service, + ) def map_name(self, row): return row["DeviceNumber"] def map_phases(self, row, section_id_sections): phases = [] - section_id = row['SectionID'] + section_id = row["SectionID"] section = section_id_sections[section_id] - section_phases = section['Phase'] - if 'FixedKVARA' in row and row["FixedKVARA"] or 'A' in section_phases: + section_phases = section["Phase"] + if "FixedKVARA" in row and row["FixedKVARA"] or "A" in section_phases: phases.append(Phase.A) - if 'FixedKVARB' in row and row["FixedKVARB"] or 'B' in section_phases: + if "FixedKVARB" in row and row["FixedKVARB"] or "B" in section_phases: phases.append(Phase.B) - if 'FixedKVARC' in row and row["FixedKVARC"] or 'C' in section_phases: + if "FixedKVARC" in row and row["FixedKVARC"] or "C" in section_phases: phases.append(Phase.C) if phases == []: - raise ValueError(f"Could not determine phases for capacitor {row['DeviceNumber']} on section {section_id} with section phases {section_phases}") + raise ValueError( + f"Could not determine phases for capacitor {row['DeviceNumber']} on section {section_id} with section phases {section_phases}" + ) return phases def map_bus(self, row, section_id_sections): @@ -52,36 +56,34 @@ def map_bus(self, row, section_id_sections): to_bus_name = section["ToNodeID"] to_bus = None from_bus = None - try: - from_bus = self.system.get_component(component_type=DistributionBus,name=from_bus_name) - except Exception as e: - pass - try: - to_bus = self.system.get_component(component_type=DistributionBus,name=to_bus_name) - except Exception as e: - pass + from_bus = self.system.get_component(component_type=DistributionBus, name=from_bus_name) + + to_bus = self.system.get_component(component_type=DistributionBus, name=to_bus_name) if from_bus is None: + if to_bus is None: + logger.warning(f"Capacitor {section_id} has no bus") + return None return to_bus - if from_bus is None: - logger.warning(f"Load {section_id} has no bus") return from_bus - def map_controllers(self, row): return [] def map_equipment(self, row, equipment_data): mapper = CapacitorEquipmentMapper(self.system) - capacitor_id = row['ShuntCapacitorID'] + capacitor_id = row["ShuntCapacitorID"] if capacitor_id not in equipment_data.index: - capacitor_id = 'DEFAULT' + logger.warning( + f"Capacitor {row['DeviceNumber']} references capacitor equipment {capacitor_id} which is not defined in the equipment data. Assigning default capacitor equipment." + ) + capacitor_id = "DEFAULT" equipment_row = equipment_data.loc[capacitor_id] if not equipment_row.empty: - equipment = mapper.parse(equipment_row, connection=row['Connection']) + equipment = mapper.parse(equipment_row, connection=row["Connection"]) return equipment return None - + def map_in_service(self, row): - return True if int(row['ConnectionStatus']) == 0 else False + return True if int(row["ConnectionStatus"]) == 0 else False diff --git a/src/ditto/readers/cyme/components/distribution_load.py b/src/ditto/readers/cyme/components/distribution_load.py index 2f579ef..250997f 100644 --- a/src/ditto/readers/cyme/components/distribution_load.py +++ b/src/ditto/readers/cyme/components/distribution_load.py @@ -1,4 +1,3 @@ -from ditto.readers.cyme.utils import read_cyme_data from ditto.readers.cyme.cyme_mapper import CymeMapper from ditto.readers.cyme.equipment.load_equipment import LoadEquipmentMapper from gdm.distribution.components.distribution_bus import DistributionBus @@ -6,13 +5,13 @@ from gdm.distribution.enums import Phase from loguru import logger + class DistributionLoadMapper(CymeMapper): def __init__(self, system): super().__init__(system) - - cyme_file = 'Load' - cyme_section = 'CUSTOMER LOADS' + cyme_file = "Load" + cyme_section = "CUSTOMER LOADS" def parse(self, row, section_id_sections, equipment_file, load_record): name = self.map_name(row) @@ -24,78 +23,68 @@ def parse(self, row, section_id_sections, equipment_file, load_record): return None if load_record.get(name) is not None: + # Combines powers from multiple customer IDs to their spot loads. + # Individual customer loads are not supported. + existing_load = load_record.get(name) - existing_load.equipment.phase_loads[0].real_power += equipment.phase_loads[0].real_power - existing_load.equipment.phase_loads[0].reactive_power += equipment.phase_loads[0].reactive_power + existing_load.equipment.phase_loads[0].real_power += equipment.phase_loads[ + 0 + ].real_power + existing_load.equipment.phase_loads[0].reactive_power += equipment.phase_loads[ + 0 + ].reactive_power return None if len(phases) == 0: - logger.warning(f"Load {name} has no kW values. Skipping...") + logger.warning(f"Load {name} has no phase values. Skipping...") return None - - load = DistributionLoad.model_construct(name=name, - bus=bus, - phases=phases, - equipment=equipment) + + load = DistributionLoad.model_construct( + name=name, bus=bus, phases=phases, equipment=equipment + ) load_record[name] = load return load - - + def map_name(self, row): load_phase = row["LoadPhase"] - return row["DeviceNumber"]+"_"+str(load_phase) + return row["DeviceNumber"] + "_" + str(load_phase) def map_bus(self, row, section_id_sections): - section_id = row['SectionID'] + section_id = row["SectionID"] section = section_id_sections[section_id] from_bus_name = section["FromNodeID"] to_bus_name = section["ToNodeID"] to_bus = None from_bus = None - try: - from_bus = self.system.get_component(component_type=DistributionBus,name=from_bus_name) - except Exception as e: - pass - try: - to_bus = self.system.get_component(component_type=DistributionBus,name=to_bus_name) - except Exception as e: - pass + from_bus = self.system.get_component(component_type=DistributionBus, name=from_bus_name) + + to_bus = self.system.get_component(component_type=DistributionBus, name=to_bus_name) if from_bus is None: + if to_bus is None: + logger.warning(f"Load {section_id} has no bus") + return None return to_bus - if from_bus is None: - logger.warning(f"Load {section_id} has no bus") return from_bus - bus_name = row["NodeID"] - bus = None - try: - bus = self.system.get_component(component_type=DistributionBus, name=bus_name) - except Exception as e: - pass - - if bus is None: - logger.warning(f"Load {row['NodeID']} has no bus") - return bus - def map_phases(self, row): phases = [] - if row['LoadPhase'] is not None: - phase = row['LoadPhase'] - if phase == 'A': + if row["LoadPhase"] is not None: + phase = row["LoadPhase"] + if phase == "A": phases.append(Phase.A) - elif phase == 'B': + elif phase == "B": phases.append(Phase.B) - elif phase == 'C': + elif phase == "C": phases.append(Phase.C) return phases def map_equipment(self, row, equipment_file): mapper = LoadEquipmentMapper(self.system) - equipment_row = equipment_file.loc[row['DeviceNumber']] + equipment_row = equipment_file.loc[row["DeviceNumber"]] if equipment_row is not None: equipment = mapper.parse(equipment_row, row) return equipment - + return None diff --git a/src/ditto/readers/cyme/components/geometry_branch.py b/src/ditto/readers/cyme/components/geometry_branch.py index ca70a92..b8a3b49 100644 --- a/src/ditto/readers/cyme/components/geometry_branch.py +++ b/src/ditto/readers/cyme/components/geometry_branch.py @@ -11,13 +11,13 @@ def __init__(self, system): super().__init__(system) cyme_file = "Network" - cyme_section = "OVERHEADLINE SETTING" + cyme_section = ["OVERHEADLINE SETTING", "OVERHEAD BYPHASE SETTING"] - def parse(self, row, used_sections, section_id_sections): + def parse(self, row, used_sections, section_id_sections, cyme_section): name = self.map_name(row) buses = self.map_buses(row, section_id_sections) length = self.map_length(row) - equipment = self.map_equipment(row) + equipment = self.map_equipment(row, cyme_section) phases = self.map_phases(row, section_id_sections, equipment, buses) used_sections.add(name) @@ -67,74 +67,11 @@ def map_phases(self, row, section_id_sections, equipment, buses): return phases else: return phases - # raise ValueError(f"Number of phases {len(phases)} does not match number of conductors {len(equipment.conductors)} for line {row['SectionID']}") - def map_equipment(self, row): - line_id = row["LineCableID"] - line = self.system.get_component(component_type=GeometryBranchEquipment, name=line_id) - return line - - -class GeometryBranchByPhaseMapper(CymeMapper): - def __init__(self, system): - super().__init__(system) - - cyme_file = "Network" - cyme_section = "OVERHEAD BYPHASE SETTING" - - def parse(self, row, used_sections, section_id_sections): - name = self.map_name(row) - buses = self.map_buses(row, section_id_sections) - length = self.map_length(row) - equipment = self.map_equipment(row) - phases = self.map_phases(row, section_id_sections, equipment, buses) - - used_sections.add(name) - return GeometryBranch.model_construct( - name=name, buses=buses, length=length, phases=phases, equipment=equipment + def map_equipment(self, row, cyme_section): + line_id = ( + row["LineCableID"] if cyme_section == "OVERHEADLINE SETTING" else row["DeviceNumber"] ) - def map_name(self, row): - name = row["SectionID"] - return name - - def map_buses(self, row, section_id_sections): - section_id = str(row["SectionID"]) - section = section_id_sections[section_id] - from_bus_name = section["FromNodeID"] - to_bus_name = section["ToNodeID"] - - from_bus = self.system.get_component(component_type=DistributionBus, name=from_bus_name) - to_bus = self.system.get_component(component_type=DistributionBus, name=to_bus_name) - return [from_bus, to_bus] - - def map_length(self, row): - length = Distance(float(row["Length"]), "foot").to("km") - return length - - def map_phases(self, row, section_id_sections, equipment, buses): - section_id = str(row["SectionID"]) - section = section_id_sections[section_id] - phase = section["Phase"] - phases = [] - if "A" in phase: - phases.append(Phase.A) - if "B" in phase: - phases.append(Phase.B) - if "C" in phase: - phases.append(Phase.C) - - if len(phases) == len(equipment.conductors): - return phases - elif len(phase) == len(equipment.conductors) - 1: - phases.append(Phase.N) - for bus in buses: - if bus.phases is not None and Phase.N not in bus.phases: - bus.phases.append(Phase.N) - return phases - return phases - - def map_equipment(self, row): - line_id = row["SectionID"] line = self.system.get_component(component_type=GeometryBranchEquipment, name=line_id) return line diff --git a/src/ditto/readers/cyme/components/matrix_impedance_branch.py b/src/ditto/readers/cyme/components/matrix_impedance_branch.py index a9469b2..7070945 100644 --- a/src/ditto/readers/cyme/components/matrix_impedance_branch.py +++ b/src/ditto/readers/cyme/components/matrix_impedance_branch.py @@ -1,113 +1,111 @@ - -from gdm.quantities import Distance, Current, ResistancePULength, ReactancePULength, CapacitancePULength +from gdm.quantities import Distance, ResistancePULength, ReactancePULength, CapacitancePULength from ditto.readers.cyme.cyme_mapper import CymeMapper -from gdm.distribution.equipment.matrix_impedance_branch_equipment import MatrixImpedanceBranchEquipment +from gdm.distribution.equipment.matrix_impedance_branch_equipment import ( + MatrixImpedanceBranchEquipment, +) from gdm.distribution.components.matrix_impedance_branch import MatrixImpedanceBranch from gdm.distribution.components.distribution_bus import DistributionBus from gdm.distribution.enums import Phase +from ditto.readers.cyme.constants import ( + DEFAULT_BRANCH_LENGTH, + DEFAULT_R_MATRIX, + DEFAULT_X_MATRIX, + DEFAULT_C_MATRIX, + DEFAULT_BRANCH_AMPACITY, +) class MatrixImpedanceBranchMapper(CymeMapper): def __init__(self, system): super().__init__(system) - cyme_file = 'Network' - cyme_section = ['UNDERGROUNDLINE SETTING', 'SECTION'] + cyme_file = "Network" + cyme_section = ["UNDERGROUNDLINE SETTING", "SECTION"] def parse(self, row, used_sections, section_id_sections, cyme_section): name = self.map_name(row) - if cyme_section == 'SECTION' and name in used_sections: + if cyme_section == "SECTION" and name in used_sections: return None - buses = self.map_buses(row,section_id_sections) + buses = self.map_buses(row, section_id_sections) length = self.map_length(row, cyme_section) phases = self.map_phases(row, section_id_sections) equipment = self.map_equipment(row, phases, cyme_section) used_sections.add(name) try: - return MatrixImpedanceBranch(name=name, - buses=buses, - length=length, - phases=phases, - equipment=equipment) + return MatrixImpedanceBranch( + name=name, buses=buses, length=length, phases=phases, equipment=equipment + ) except Exception as e: print(f"Error creating MatrixImpedanceBranch {name}: {e}") print(buses) return None def map_name(self, row): - name = row['SectionID'] + name = row["SectionID"] return name + def map_buses(self, row, section_id_sections): - section_id = str(row['SectionID']) + section_id = str(row["SectionID"]) section = section_id_sections[section_id] - from_bus_name = section['FromNodeID'] - to_bus_name = section['ToNodeID'] - + from_bus_name = section["FromNodeID"] + to_bus_name = section["ToNodeID"] + from_bus = self.system.get_component(component_type=DistributionBus, name=from_bus_name) to_bus = self.system.get_component(component_type=DistributionBus, name=to_bus_name) return [from_bus, to_bus] def map_length(self, row, cyme_section): - if cyme_section == 'UNDERGROUNDLINE SETTING': - length = Distance(float(row['Length']),'foot').to('km') + if cyme_section == "UNDERGROUNDLINE SETTING": + length = Distance(float(row["Length"]), "foot").to("km") else: - length = Distance(0.001,'kilometer') + length = DEFAULT_BRANCH_LENGTH return length def map_phases(self, row, section_id_sections): - section_id = str(row['SectionID']) + section_id = str(row["SectionID"]) section = section_id_sections[section_id] - phase = section['Phase'] + phase = section["Phase"] phases = [] - if 'A' in phase: + if "A" in phase: phases.append(Phase.A) - if 'B' in phase: + if "B" in phase: phases.append(Phase.B) - if 'C' in phase: + if "C" in phase: phases.append(Phase.C) return phases def map_equipment(self, row, phases, cyme_section): - if cyme_section == 'UNDERGROUNDLINE SETTING': - line_id = row['LineCableID'] + if cyme_section == "UNDERGROUNDLINE SETTING": + line_id = row["LineCableID"] equipment_name = f"{line_id}_{len(phases)}" - line = self.system.get_component(component_type=MatrixImpedanceBranchEquipment, name=equipment_name) - elif cyme_section == 'SECTION': - r = [ - [1e-6, 0.0, 0.0], - [0.0, 1e-6, 0.0], - [0.0, 0.0, 1e-6], - ] + line = self.system.get_component( + component_type=MatrixImpedanceBranchEquipment, name=equipment_name + ) + elif cyme_section == "SECTION": + r = DEFAULT_R_MATRIX r_matrix = ResistancePULength( - [row[:len(phases)] for row in r[:len(phases)]], + [row[: len(phases)] for row in r[: len(phases)]], "ohm/mi", ) - x = [ - [1e-4, 0.0, 0.0], - [0.0, 1e-4, 0.0], - [0.0, 0.0, 1e-4], - ] + x = DEFAULT_X_MATRIX x_matrix = ReactancePULength( - [row[:len(phases)] for row in x[:len(phases)]], + [row[: len(phases)] for row in x[: len(phases)]], "ohm/mi", ) - c = [ - [0.0, 0.0, 0.0], - [0.0, 0.0, 0.0], - [0.0, 0.0, 0.0], - ] + c = DEFAULT_C_MATRIX c_matrix = CapacitancePULength( - [row[:len(phases)] for row in c[:len(phases)]], + [row[: len(phases)] for row in c[: len(phases)]], "nanofarad/mi", ) - ampacity = Current(600.0, "A") - line = MatrixImpedanceBranchEquipment.model_construct(name=row['SectionID'], - r_matrix=r_matrix, - x_matrix=x_matrix, - c_matrix=c_matrix, - ampacity=ampacity - ) - return line \ No newline at end of file + ampacity = DEFAULT_BRANCH_AMPACITY + line = MatrixImpedanceBranchEquipment.model_construct( + name=row["SectionID"], + r_matrix=r_matrix, + x_matrix=x_matrix, + c_matrix=c_matrix, + ampacity=ampacity, + ) + return line diff --git a/src/ditto/readers/cyme/components/matrix_impedance_fuse.py b/src/ditto/readers/cyme/components/matrix_impedance_fuse.py index 73b9ab3..19d5b83 100644 --- a/src/ditto/readers/cyme/components/matrix_impedance_fuse.py +++ b/src/ditto/readers/cyme/components/matrix_impedance_fuse.py @@ -1,26 +1,28 @@ from ditto.readers.cyme.cyme_mapper import CymeMapper -from ditto.readers.cyme.equipment.matrix_impedance_fuse_equipment import MatrixImpedanceFuseEquipment +from ditto.readers.cyme.equipment.matrix_impedance_fuse_equipment import ( + MatrixImpedanceFuseEquipment, +) from gdm.distribution.components.matrix_impedance_fuse import MatrixImpedanceFuse from gdm.distribution.components.distribution_bus import DistributionBus -from gdm.quantities import Distance from gdm.distribution.enums import Phase +from ditto.readers.cyme.constants import DEFAULT_BRANCH_LENGTH + class MatrixImpedanceFuseMapper(CymeMapper): def __init__(self, system): super().__init__(system) - cyme_file = 'Network' - cyme_section = 'FUSE SETTING' + cyme_file = "Network" + cyme_section = "FUSE SETTING" def parse(self, row, used_sections, section_id_sections): - name = self.map_name(row) buses = self.map_buses(row, section_id_sections) - length = self.map_length(row) + length = self.map_length() phases = self.map_phases(row, section_id_sections) is_closed = self.map_is_closed(row, phases) equipment = self.map_equipment(row, phases) - + used_sections.add(name) return MatrixImpedanceFuse( name=name, @@ -28,54 +30,50 @@ def parse(self, row, used_sections, section_id_sections): length=length, phases=phases, is_closed=is_closed, - equipment=equipment + equipment=equipment, ) def map_name(self, row): - name = row['SectionID'] + name = row["SectionID"] return name - + def map_buses(self, row, section_id_sections): - section_id = str(row['SectionID']) + section_id = str(row["SectionID"]) section = section_id_sections[section_id] - from_bus_name = section['FromNodeID'] - to_bus_name = section['ToNodeID'] - + from_bus_name = section["FromNodeID"] + to_bus_name = section["ToNodeID"] + from_bus = self.system.get_component(component_type=DistributionBus, name=from_bus_name) to_bus = self.system.get_component(component_type=DistributionBus, name=to_bus_name) return [from_bus, to_bus] - def map_length(self, row): - length = Distance(0.001,'kilometer') + def map_length(self): + length = DEFAULT_BRANCH_LENGTH return length - + def map_phases(self, row, section_id_sections): - section_id = str(row['SectionID']) + section_id = str(row["SectionID"]) section = section_id_sections[section_id] - phase = section['Phase'] + phase = section["Phase"] phases = [] - if 'A' in phase: + if "A" in phase: phases.append(Phase.A) - if 'B' in phase: + if "B" in phase: phases.append(Phase.B) - if 'C' in phase: + if "C" in phase: phases.append(Phase.C) return phases - + def map_is_closed(self, row, phases): is_closed = [] for phase in phases: - if row['NStatus'] == '0': + if row["NStatus"] == "0": is_closed.append(True) else: is_closed.append(False) return is_closed - def map_equipment(self, row, phases): fuse_id = f"{row['EqID']}_{len(phases)}" fuse = self.system.get_component(component_type=MatrixImpedanceFuseEquipment, name=fuse_id) return fuse - - - diff --git a/src/ditto/readers/cyme/components/matrix_impedance_recloser.py b/src/ditto/readers/cyme/components/matrix_impedance_recloser.py index 405b5cd..586b274 100644 --- a/src/ditto/readers/cyme/components/matrix_impedance_recloser.py +++ b/src/ditto/readers/cyme/components/matrix_impedance_recloser.py @@ -1,23 +1,27 @@ from ditto.readers.cyme.cyme_mapper import CymeMapper -from ditto.readers.cyme.equipment.matrix_impedance_recloser_equipment import MatrixImpedanceRecloserEquipment +from ditto.readers.cyme.equipment.matrix_impedance_recloser_equipment import ( + MatrixImpedanceRecloserEquipment, +) from gdm.distribution.components.matrix_impedance_recloser import MatrixImpedanceRecloser from gdm.distribution.components.distribution_bus import DistributionBus -from gdm.distribution.controllers.distribution_recloser_controller import DistributionRecloserController -from gdm.quantities import Distance +from gdm.distribution.controllers.distribution_recloser_controller import ( + DistributionRecloserController, +) from gdm.distribution.enums import Phase +from ditto.readers.cyme.constants import DEFAULT_BRANCH_LENGTH + class MatrixImpedanceRecloserMapper(CymeMapper): def __init__(self, system): super().__init__(system) - cyme_file = 'Network' - cyme_section = 'RECLOSER SETTING' + cyme_file = "Network" + cyme_section = "RECLOSER SETTING" def parse(self, row, used_sections, section_id_sections): - name = self.map_name(row) buses = self.map_buses(row, section_id_sections) - length = self.map_length(row) + length = self.map_length() phases = self.map_phases(row, section_id_sections) is_closed = self.map_is_closed(row, phases) controller = self.map_controller(row) @@ -32,58 +36,55 @@ def parse(self, row, used_sections, section_id_sections): phases=phases, is_closed=is_closed, controller=controller, - equipment=equipment + equipment=equipment, ) def map_name(self, row): - name = row['SectionID'] + name = row["SectionID"] return name - + def map_buses(self, row, section_id_sections): - section_id = str(row['SectionID']) + section_id = str(row["SectionID"]) section = section_id_sections[section_id] - from_bus_name = section['FromNodeID'] - to_bus_name = section['ToNodeID'] - + from_bus_name = section["FromNodeID"] + to_bus_name = section["ToNodeID"] + from_bus = self.system.get_component(component_type=DistributionBus, name=from_bus_name) to_bus = self.system.get_component(component_type=DistributionBus, name=to_bus_name) return [from_bus, to_bus] - def map_length(self, row): - length = Distance(0.001,'km') + def map_length(self): + length = DEFAULT_BRANCH_LENGTH return length - + def map_phases(self, row, section_id_sections): - section_id = str(row['SectionID']) + section_id = str(row["SectionID"]) section = section_id_sections[section_id] - phase = section['Phase'] + phase = section["Phase"] phases = [] - if 'A' in phase: + if "A" in phase: phases.append(Phase.A) - if 'B' in phase: + if "B" in phase: phases.append(Phase.B) - if 'C' in phase: + if "C" in phase: phases.append(Phase.C) return phases - + def map_is_closed(self, row, phases): is_closed = [] for phase in phases: - if row['NStatus'] == '0': + if row["NStatus"] == "0": is_closed.append(True) else: is_closed.append(False) return is_closed - + def map_controller(self, row): return DistributionRecloserController.example() - def map_equipment(self, row, phases): recloser_id = f"{row['EqID']}_{len(phases)}" - recloser = self.system.get_component(component_type=MatrixImpedanceRecloserEquipment, name=recloser_id) + recloser = self.system.get_component( + component_type=MatrixImpedanceRecloserEquipment, name=recloser_id + ) return recloser - - - - diff --git a/src/ditto/readers/cyme/components/matrix_impedance_switch.py b/src/ditto/readers/cyme/components/matrix_impedance_switch.py index c2d9909..3e906aa 100644 --- a/src/ditto/readers/cyme/components/matrix_impedance_switch.py +++ b/src/ditto/readers/cyme/components/matrix_impedance_switch.py @@ -1,19 +1,21 @@ from ditto.readers.cyme.cyme_mapper import CymeMapper -from ditto.readers.cyme.equipment.matrix_impedance_switch_equipment import MatrixImpedanceSwitchEquipment +from ditto.readers.cyme.equipment.matrix_impedance_switch_equipment import ( + MatrixImpedanceSwitchEquipment, +) from gdm.distribution.components.matrix_impedance_switch import MatrixImpedanceSwitch from gdm.distribution.components.distribution_bus import DistributionBus -from gdm.quantities import Distance from gdm.distribution.enums import Phase +from ditto.readers.cyme.constants import DEFAULT_BRANCH_LENGTH + class MatrixImpedanceSwitchMapper(CymeMapper): def __init__(self, system): super().__init__(system) - cyme_file = 'Network' - cyme_section = 'SWITCH SETTING' + cyme_file = "Network" + cyme_section = "SWITCH SETTING" def parse(self, row, used_sections, section_id_sections): - name = self.map_name(row) buses = self.map_buses(row, section_id_sections) length = self.map_length(row) @@ -27,55 +29,52 @@ def parse(self, row, used_sections, section_id_sections): length=length, phases=phases, is_closed=is_closed, - equipment=equipment + equipment=equipment, ) def map_name(self, row): - name = row['SectionID'] + name = row["SectionID"] return name - + def map_buses(self, row, section_id_sections): - section_id = str(row['SectionID']) + section_id = str(row["SectionID"]) section = section_id_sections[section_id] - from_bus_name = section['FromNodeID'] - to_bus_name = section['ToNodeID'] - + from_bus_name = section["FromNodeID"] + to_bus_name = section["ToNodeID"] + from_bus = self.system.get_component(component_type=DistributionBus, name=from_bus_name) to_bus = self.system.get_component(component_type=DistributionBus, name=to_bus_name) return [from_bus, to_bus] def map_length(self, row): - length = Distance(0.001,'km') + length = DEFAULT_BRANCH_LENGTH return length - + def map_phases(self, row, section_id_sections): - section_id = str(row['SectionID']) + section_id = str(row["SectionID"]) section = section_id_sections[section_id] - phase = section['Phase'] + phase = section["Phase"] phases = [] - if 'A' in phase: + if "A" in phase: phases.append(Phase.A) - if 'B' in phase: + if "B" in phase: phases.append(Phase.B) - if 'C' in phase: + if "C" in phase: phases.append(Phase.C) return phases - + def map_is_closed(self, row, phases): is_closed = [] for phase in phases: - if row['NStatus'] == '0': + if row["NStatus"] == "0": is_closed.append(True) else: is_closed.append(False) return is_closed - def map_equipment(self, row, phases): switch_id = f"{row['EqID']}_{len(phases)}" - switch = self.system.get_component(component_type=MatrixImpedanceSwitchEquipment, name=switch_id) + switch = self.system.get_component( + component_type=MatrixImpedanceSwitchEquipment, name=switch_id + ) return switch - - - - diff --git a/src/ditto/readers/cyme/constants.py b/src/ditto/readers/cyme/constants.py new file mode 100644 index 0000000..6659d82 --- /dev/null +++ b/src/ditto/readers/cyme/constants.py @@ -0,0 +1,27 @@ +from gdm.quantities import Distance, Current, ResistancePULength + +DEFAULT_BRANCH_LENGTH = Distance(0.001, "km") + +DEFAULT_R_MATRIX = [ + [1e-6, 0.0, 0.0], + [0.0, 1e-6, 0.0], + [0.0, 0.0, 1e-6], +] + + +DEFAULT_X_MATRIX = [ + [1e-4, 0.0, 0.0], + [0.0, 1e-4, 0.0], + [0.0, 0.0, 1e-4], +] + + +DEFAULT_C_MATRIX = [ + [0.0, 0.0, 0.0], + [0.0, 0.0, 0.0], + [0.0, 0.0, 0.0], +] + +DEFAULT_BRANCH_AMPACITY = Current(600.0, "A") + +DEFAULT_BRANCH_RESISTANCE = ResistancePULength(0.555000, "ohm/mile").to("ohm/km") diff --git a/src/ditto/readers/cyme/equipment/capacitor_equipment.py b/src/ditto/readers/cyme/equipment/capacitor_equipment.py index c66ed35..7cb49f5 100644 --- a/src/ditto/readers/cyme/equipment/capacitor_equipment.py +++ b/src/ditto/readers/cyme/equipment/capacitor_equipment.py @@ -1,43 +1,46 @@ from ditto.readers.cyme.cyme_mapper import CymeMapper -from gdm.quantities import ReactivePower, Resistance, Reactance +from gdm.quantities import ReactivePower from gdm.distribution.equipment.phase_capacitor_equipment import PhaseCapacitorEquipment from gdm.distribution.equipment.capacitor_equipment import CapacitorEquipment -from gdm.distribution.enums import VoltageTypes, ConnectionType +from gdm.distribution.enums import VoltageTypes from gdm.quantities import Voltage + class CapacitorEquipmentMapper(CymeMapper): def __init__(self, system): super().__init__(system) - cyme_file = 'Equipment' - cyme_section = 'SHUNT CAPACITOR' + cyme_file = "Equipment" + cyme_section = "SHUNT CAPACITOR" def parse(self, row, connection): name = self.map_name(row) rated_voltage = self.map_rated_voltage(row) phase_capacitors = self.map_phase_capacitors(row) voltage_type = self.map_voltage_type(connection) - return CapacitorEquipment(name=name, - phase_capacitors=phase_capacitors, - rated_voltage=rated_voltage, - voltage_type=voltage_type) + return CapacitorEquipment( + name=name, + phase_capacitors=phase_capacitors, + rated_voltage=rated_voltage, + voltage_type=voltage_type, + ) def map_name(self, row): return row["ID"] - + def map_rated_voltage(self, row): return Voltage(float(row["KV"]), "kilovolt") def map_phase_capacitors(self, row): phase_capacitors = [] number_of_phases = 3 if int(row["Type"]) > 1 else 1 - + for phase in range(1, number_of_phases + 1): mapper = PhaseCapacitorEquipmentMapper(self.system, num_banks_on=number_of_phases) phase_capacitor = mapper.parse(row, phase) phase_capacitors.append(phase_capacitor) return phase_capacitors - + def map_voltage_type(self, connection): if connection in ("Y", "YNG"): return VoltageTypes.LINE_TO_GROUND @@ -49,15 +52,15 @@ def __init__(self, system, num_banks_on): super().__init__(system) self.num_banks_on = num_banks_on - cyme_file = 'Equipment' - cyme_section = 'SHUNT CAPACITOR' + cyme_file = "Equipment" + cyme_section = "SHUNT CAPACITOR" def parse(self, row, phase): name = self.map_name(row, phase) rated_reactive_power = self.map_rated_reactive_power(row) - return PhaseCapacitorEquipment(name = name, - rated_reactive_power=rated_reactive_power, - num_banks_on=self.num_banks_on) + return PhaseCapacitorEquipment( + name=name, rated_reactive_power=rated_reactive_power, num_banks_on=self.num_banks_on + ) def map_name(self, row, phase): if phase == 1: @@ -67,6 +70,5 @@ def map_name(self, row, phase): if phase == 3: return row["ID"] + "_C" - def map_rated_reactive_power(self, row): - return ReactivePower(float(row["KVAR"]),'kilovar') + return ReactivePower(float(row["KVAR"]), "kilovar") diff --git a/src/ditto/readers/cyme/equipment/distribution_transformer_equipment.py b/src/ditto/readers/cyme/equipment/distribution_transformer_equipment.py index 09b279b..5067e22 100644 --- a/src/ditto/readers/cyme/equipment/distribution_transformer_equipment.py +++ b/src/ditto/readers/cyme/equipment/distribution_transformer_equipment.py @@ -67,9 +67,9 @@ def map_winding_reactances(self, row, is_center_tapped): return winding_reactances def map_is_center_tapped(self, row): - transformer_type = row["Type"] # TODO Center tapped/Split phase not supported # This will assign it properly but handling of buses needs to be developed + # transformer_type = row["Type"] # if transformer_type == '4': # return True @@ -104,6 +104,8 @@ def __init__(self, system): cyme_file = "Equipment" cyme_section = "TRANSFORMER" + + """ connection_map = { 0: "Y_Y", 1: "D_Y", @@ -119,9 +121,11 @@ def __init__(self, system): 11: "Yg_Zg", 12: "D_Zg", } + """ # The documentation is confusing on where the below connection types are used - # It appears they are used in the equipment file although it is repoted in the network file + # It appears they are used in the equipment file although it is reported in the network file + # Including the above map incase this is incorrect but the below map appears to be correct based on testing with CYME data connection_map = { 0: "Yg_Yg", 1: "D_Yg", @@ -193,12 +197,6 @@ def map_name(self, row): def map_resistance(self, row, winding_number): xr_ratio = float(row["XR"]) resistance_pu = float(row["Z1"]) / 100 / ((1 + xr_ratio**2) ** 0.5) - if winding_number == 1: - resistance = resistance_pu * float(row["KVLLprim"]) ** 2 / float(row["KVA"]) - elif winding_number == 2: - resistance = resistance_pu * float(row["KVLLsec"]) ** 2 / float(row["KVA"]) - elif winding_number == 3: - resistance = resistance_pu * float(row["KVLLsec"]) ** 2 / float(row["KVA"]) return resistance_pu * 100 def map_is_grounded(self, row, winding_number): @@ -258,10 +256,9 @@ def map_connection_type(self, row, winding_number): connection_type = row["Conn"] if winding_number == 1: winding = 0 - elif winding_number == 2: - winding = 1 - elif winding_number == 3: + elif winding_number == 2 or winding_number == 3: winding = 1 + if isinstance(connection_type, int) or ( isinstance(connection_type, str) and connection_type.isdigit() ): @@ -269,20 +266,22 @@ def map_connection_type(self, row, winding_number): winding_type = self.connection_map.get(conn_type_int, "Y_Y").split("_")[winding] else: winding_type = str(connection_type) - if winding_type == "YO": - connection_type = "OPEN_STAR" - elif winding_type == "DO": - connection_type = "OPEN_DELTA" + + winding_connection_map = { + "YO": "OPEN_STAR", + "DO": "OPEN_DELTA", + "DCT": "DELTA", + "CT": "STAR", + } + + if winding_type in winding_connection_map: + connection_type = winding_connection_map[winding_type] elif "Z" in winding_type: connection_type = "ZIG_ZAG" elif "Y" in winding_type: connection_type = "STAR" elif "D" in winding_type: connection_type = "DELTA" - elif "DCT" == winding_type: - connection_type = "DELTA" - elif "CT" == winding_type: - connection_type = "STAR" else: connection_type = "STAR" @@ -301,7 +300,7 @@ def map_tap_positions(self, row, winding_number, network_row, phase): tap = network_row.get("PrimaryTapSettingA", 100) tap = float(tap) / 100 - elif winding_number == 2: + elif winding_number == 2 or winding_number == 3: if network_row is None: tap = 1.0 else: @@ -309,16 +308,7 @@ def map_tap_positions(self, row, winding_number, network_row, phase): if tap is None: tap = network_row.get("SecondaryTapSettingA", 100) tap = float(tap) / 100 - elif winding_number == 3: - if network_row is None: - tap = 1.0 - else: - tap = network_row.get("SecTap", None) - if tap is None: - tap = network_row.get( - "SecondaryTapSettingA", - ) - tap = float(tap) / 100 + if row["Taps"] == "" or row["Taps"] is None: tap = 1.0 for phase in range(1, num_phases + 1): diff --git a/src/ditto/readers/cyme/equipment/distribution_transformer_three_winding_equipment.py b/src/ditto/readers/cyme/equipment/distribution_transformer_three_winding_equipment.py index 776c4f5..3ef510d 100644 --- a/src/ditto/readers/cyme/equipment/distribution_transformer_three_winding_equipment.py +++ b/src/ditto/readers/cyme/equipment/distribution_transformer_three_winding_equipment.py @@ -1,20 +1,21 @@ -from loguru import logger from ditto.readers.cyme.cyme_mapper import CymeMapper -from gdm.distribution.equipment.distribution_transformer_equipment import DistributionTransformerEquipment +from gdm.distribution.equipment.distribution_transformer_equipment import ( + DistributionTransformerEquipment, +) from gdm.distribution.equipment.distribution_transformer_equipment import WindingEquipment -from gdm.quantities import ActivePower, ReactivePower, Voltage +from gdm.quantities import ActivePower, Voltage from gdm.distribution.common.sequence_pair import SequencePair from gdm.distribution.enums import ConnectionType, VoltageTypes + class DistributionTransformerThreeWindingEquipmentMapper(CymeMapper): def __init__(self, system): super().__init__(system) - cyme_file = 'Equipment' - cyme_section = 'THREE WINDING TRANSFORMER' + cyme_file = "Equipment" + cyme_section = "THREE WINDING TRANSFORMER" def parse(self, row, network_row): - name = self.map_name(row) pct_no_load_loss = self.map_pct_no_load_loss(row) pct_full_load_loss = self.map_pct_full_load_loss(row) @@ -23,70 +24,95 @@ def parse(self, row, network_row): winding_reactances = self.map_winding_reactances(row) coupling_sequences = self.map_coupling(row) - return DistributionTransformerEquipment.model_construct(name=name, - pct_no_load_loss=pct_no_load_loss, - pct_full_load_loss=pct_full_load_loss, - windings=windings, - winding_reactances=winding_reactances, - is_center_tapped=is_center_tapped, - coupling_sequences=coupling_sequences) + return DistributionTransformerEquipment.model_construct( + name=name, + pct_no_load_loss=pct_no_load_loss, + pct_full_load_loss=pct_full_load_loss, + windings=windings, + winding_reactances=winding_reactances, + is_center_tapped=is_center_tapped, + coupling_sequences=coupling_sequences, + ) def map_name(self, row): - name = row['ID'] + name = row["ID"] return name def map_pct_no_load_loss(self, row): - no_load_loss = float(row['NoLoadLosses']) - kva = float(row['PrimaryRatedCapacity']) - pct_no_load_loss = no_load_loss/kva*100 + no_load_loss = float(row["NoLoadLosses"]) + kva = float(row["PrimaryRatedCapacity"]) + pct_no_load_loss = no_load_loss / kva * 100 return pct_no_load_loss def map_pct_full_load_loss(self, row): - - I1 = float(row['PrimaryRatedCapacity']) * 1000 / (float(row['PrimaryVoltage']) * 1000) - I2 = float(row['SecondaryRatedCapacity']) * 1000 / (float(row['PrimaryVoltage']) * 1000) - I3 = float(row['TertiaryRatedCapacity']) * 1000 / (float(row['PrimaryVoltage']) * 1000) - - Rpu_12 = float(row['PrimaryToSecondaryZ1']) / 100 / ((1 + float(row['PrimaryToSecondaryXR1'])**2)**0.5) - Rpu_13 = float(row['PrimaryToTertiaryZ1']) / 100 / ((1 + float(row['PrimaryToTertiaryXR1'])**2)**0.5) - Rpu_23 = float(row['SecondaryToTertiaryZ1']) / 100 / ((1 + float(row['SecondaryToTertiaryXR1'])**2)**0.5) - - R12 = Rpu_12 * (float(row['PrimaryVoltage'])**2 * 1000) / float(row['PrimaryRatedCapacity']) - R13 = Rpu_13 * (float(row['PrimaryVoltage'])**2 * 1000) / float(row['PrimaryRatedCapacity']) - R23 = Rpu_23 * (float(row['PrimaryVoltage'])**2 * 1000) / float(row['PrimaryRatedCapacity']) - - R1 = (R12 + R13 - R23)/2 - R2 = (R12 + R23 - R13)/2 - R3 = (R13 + R23 - R12)/2 - - full_load_loss = (I1**2 * R1 + I2**2 * R2 + I3**2 * R3) - - va = float(row['PrimaryRatedCapacity']) * 1000 + I1 = float(row["PrimaryRatedCapacity"]) * 1000 / (float(row["PrimaryVoltage"]) * 1000) + I2 = float(row["SecondaryRatedCapacity"]) * 1000 / (float(row["PrimaryVoltage"]) * 1000) + I3 = float(row["TertiaryRatedCapacity"]) * 1000 / (float(row["PrimaryVoltage"]) * 1000) + + Rpu_12 = ( + float(row["PrimaryToSecondaryZ1"]) + / 100 + / ((1 + float(row["PrimaryToSecondaryXR1"]) ** 2) ** 0.5) + ) + Rpu_13 = ( + float(row["PrimaryToTertiaryZ1"]) + / 100 + / ((1 + float(row["PrimaryToTertiaryXR1"]) ** 2) ** 0.5) + ) + Rpu_23 = ( + float(row["SecondaryToTertiaryZ1"]) + / 100 + / ((1 + float(row["SecondaryToTertiaryXR1"]) ** 2) ** 0.5) + ) + + R12 = ( + Rpu_12 + * (float(row["PrimaryVoltage"]) ** 2 * 1000) + / float(row["PrimaryRatedCapacity"]) + ) + R13 = ( + Rpu_13 + * (float(row["PrimaryVoltage"]) ** 2 * 1000) + / float(row["PrimaryRatedCapacity"]) + ) + R23 = ( + Rpu_23 + * (float(row["PrimaryVoltage"]) ** 2 * 1000) + / float(row["PrimaryRatedCapacity"]) + ) + + R1 = (R12 + R13 - R23) / 2 + R2 = (R12 + R23 - R13) / 2 + R3 = (R13 + R23 - R12) / 2 + + full_load_loss = I1**2 * R1 + I2**2 * R2 + I3**2 * R3 + + va = float(row["PrimaryRatedCapacity"]) * 1000 pct_full_load_loss = 100 * full_load_loss / va return pct_full_load_loss def map_winding_reactances(self, row): winding_reactances = [] - - xr_ratio12 = float(row['PrimaryToSecondaryXR1']) + + xr_ratio12 = float(row["PrimaryToSecondaryXR1"]) if xr_ratio12 == 0: xr_ratio12 = 0.01 - rx_ratio12 = 1/xr_ratio12 - reactance_pu12 = float(row['PrimaryToSecondaryZ1']) / 100 /((1+rx_ratio12**2)**0.5) + rx_ratio12 = 1 / xr_ratio12 + reactance_pu12 = float(row["PrimaryToSecondaryZ1"]) / 100 / ((1 + rx_ratio12**2) ** 0.5) winding_reactances.append(reactance_pu12) - xr_ratio13 = float(row['PrimaryToTertiaryXR1']) + xr_ratio13 = float(row["PrimaryToTertiaryXR1"]) if xr_ratio13 == 0: xr_ratio13 = 0.01 - rx_ratio13 = 1/xr_ratio13 - reactance_pu13 = float(row['PrimaryToTertiaryZ1']) / 100 /((1+rx_ratio13**2)**0.5) + rx_ratio13 = 1 / xr_ratio13 + reactance_pu13 = float(row["PrimaryToTertiaryZ1"]) / 100 / ((1 + rx_ratio13**2) ** 0.5) winding_reactances.append(reactance_pu13) - xr_ratio23 = float(row['SecondaryToTertiaryXR1']) + xr_ratio23 = float(row["SecondaryToTertiaryXR1"]) if xr_ratio23 == 0: xr_ratio23 = 0.01 - rx_ratio23 = 1/xr_ratio23 - reactance_pu23 = float(row['SecondaryToTertiaryZ1']) / 100 /((1+rx_ratio23**2)**0.5) + rx_ratio23 = 1 / xr_ratio23 + reactance_pu23 = float(row["SecondaryToTertiaryZ1"]) / 100 / ((1 + rx_ratio23**2) ** 0.5) winding_reactances.append(reactance_pu23) return winding_reactances @@ -110,30 +136,28 @@ def map_windings(self, row, network_row): windings.append(winding_3) return windings - - def map_coupling(self, row): - coupling = [SequencePair(0,1), - SequencePair(0,2), - SequencePair(1,2)] + def map_coupling(self, row): + coupling = [SequencePair(0, 1), SequencePair(0, 2), SequencePair(1, 2)] return coupling + class ThreeWindingEquipmentMapper(CymeMapper): def __init__(self, system): super().__init__(system) - cyme_file = 'Equipment' - cyme_section = 'THREE WINDING TRANSFORMER' + cyme_file = "Equipment" + cyme_section = "THREE WINDING TRANSFORMER" connection_map = { - 0: 'Yg', - 1: 'Y', - 2: 'Delta', - 3: 'Open Delta', - 4: 'Closed Delta', - 5: 'Zg', - 6: 'CT', - 7: 'Dg', - } + 0: "Yg", + 1: "Y", + 2: "Delta", + 3: "Open Delta", + 4: "Closed Delta", + 5: "Zg", + 6: "CT", + 7: "Dg", + } def parse(self, row, network_row, winding_number): name = self.map_name(row) @@ -148,33 +172,57 @@ def parse(self, row, network_row, winding_number): total_taps = self.map_total_taps(row) min_tap_pu = self.min_tap_pu(row) max_tap_pu = self.max_tap_pu(row) - return WindingEquipment.model_construct(name=name, - resistance=resistance, - is_grounded=is_grounded, - rated_voltage=rated_voltage, - voltage_type=voltage_type, - rated_power=rated_power, - num_phases=num_phases, - connection_type=connection_type, - tap_positions=tap_positions, - total_taps=total_taps, - min_tap_pu=min_tap_pu, - max_tap_pu=max_tap_pu) + return WindingEquipment.model_construct( + name=name, + resistance=resistance, + is_grounded=is_grounded, + rated_voltage=rated_voltage, + voltage_type=voltage_type, + rated_power=rated_power, + num_phases=num_phases, + connection_type=connection_type, + tap_positions=tap_positions, + total_taps=total_taps, + min_tap_pu=min_tap_pu, + max_tap_pu=max_tap_pu, + ) def map_name(self, row): - name = row['ID'] + name = row["ID"] return name def map_resistance(self, row, winding_number): - - Rpu_12 = float(row['PrimaryToSecondaryZ1']) / 100 / ((1 + float(row['PrimaryToSecondaryXR1'])**2)**0.5) - Rpu_13 = float(row['PrimaryToTertiaryZ1']) / 100 / ((1 + float(row['PrimaryToTertiaryXR1'])**2)**0.5) - Rpu_23 = float(row['SecondaryToTertiaryZ1']) / 100 / ((1 + float(row['SecondaryToTertiaryXR1'])**2)**0.5) - - R12 = Rpu_12 * (float(row['SecondaryVoltage'])**2 * 1000) / float(row['SecondaryRatedCapacity']) - R13 = Rpu_13 * (float(row['TertiaryVoltage'])**2 * 1000) / float(row['TertiaryRatedCapacity']) - R23 = Rpu_23 * (float(row['TertiaryVoltage'])**2 * 1000) / float(row['TertiaryRatedCapacity']) - + Rpu_12 = ( + float(row["PrimaryToSecondaryZ1"]) + / 100 + / ((1 + float(row["PrimaryToSecondaryXR1"]) ** 2) ** 0.5) + ) + Rpu_13 = ( + float(row["PrimaryToTertiaryZ1"]) + / 100 + / ((1 + float(row["PrimaryToTertiaryXR1"]) ** 2) ** 0.5) + ) + Rpu_23 = ( + float(row["SecondaryToTertiaryZ1"]) + / 100 + / ((1 + float(row["SecondaryToTertiaryXR1"]) ** 2) ** 0.5) + ) + + R12 = ( + Rpu_12 + * (float(row["SecondaryVoltage"]) ** 2 * 1000) + / float(row["SecondaryRatedCapacity"]) + ) + R13 = ( + Rpu_13 + * (float(row["TertiaryVoltage"]) ** 2 * 1000) + / float(row["TertiaryRatedCapacity"]) + ) + R23 = ( + Rpu_23 + * (float(row["TertiaryVoltage"]) ** 2 * 1000) + / float(row["TertiaryRatedCapacity"]) + ) R1 = max(0.5 * (R12 + R13 - R23), 1e-6) R2 = max(0.5 * (R12 + R23 - R13), 1e-6) @@ -187,37 +235,34 @@ def map_resistance(self, row, winding_number): elif winding_number == 3: return R3 - def map_is_grounded(self, row, winding_number): - connection_type = None if winding_number == 1: - connection_type = row['PrimaryConnection'] + connection_type = row["PrimaryConnection"] elif winding_number == 2: - connection_type = row['SecondaryConnection'] + connection_type = row["SecondaryConnection"] elif winding_number == 3: - connection_type = row['TertiaryConnection'] + connection_type = row["TertiaryConnection"] - winding_type = self.connection_map.get(int(connection_type), 'Y') - if 'Yg' in winding_type: + winding_type = self.connection_map.get(int(connection_type), "Y") + if "Yg" in winding_type: grounded = True - elif 'Dg' in winding_type: + elif "Dg" in winding_type: grounded = True - elif 'Zg' in winding_type: + elif "Zg" in winding_type: grounded = True else: grounded = False return grounded def map_rated_voltage(self, row, winding_number): - if winding_number == 1: - voltage = Voltage(float(row['PrimaryVoltage']), "kilovolt") + voltage = Voltage(float(row["PrimaryVoltage"]), "kilovolt") elif winding_number == 2: - voltage = Voltage(float(row['SecondaryVoltage']), "kilovolt") + voltage = Voltage(float(row["SecondaryVoltage"]), "kilovolt") elif winding_number == 3: - voltage = Voltage(float(row['TertiaryVoltage']), "kilovolt") - + voltage = Voltage(float(row["TertiaryVoltage"]), "kilovolt") + return voltage def map_voltage_type(self, row): @@ -225,11 +270,11 @@ def map_voltage_type(self, row): def map_rated_power(self, row, winding_number): if winding_number == 1: - power = ActivePower(float(row['PrimaryRatedCapacity']), "kilowatt") + power = ActivePower(float(row["PrimaryRatedCapacity"]), "kilowatt") elif winding_number == 2: - power = ActivePower(float(row['SecondaryRatedCapacity']), "kilowatt") + power = ActivePower(float(row["SecondaryRatedCapacity"]), "kilowatt") elif winding_number == 3: - power = ActivePower(float(row['TertiaryRatedCapacity']), "kilowatt") + power = ActivePower(float(row["TertiaryRatedCapacity"]), "kilowatt") return power def map_num_phases(self, row): @@ -237,47 +282,44 @@ def map_num_phases(self, row): return num_phases def map_connection_type(self, row, winding_number): - connection_type = None if winding_number == 1: - connection_type = row['PrimaryConnection'] + connection_type = row["PrimaryConnection"] elif winding_number == 2: - connection_type = row['SecondaryConnection'] + connection_type = row["SecondaryConnection"] elif winding_number == 3: - connection_type = row['TertiaryConnection'] - - winding_type = self.connection_map.get(int(connection_type), 'Y') - if winding_type == 'Open Delta': - connection_type = 'OPEN_DELTA' - elif 'Delta' in winding_type: - connection_type = 'DELTA' - elif 'Z' in winding_type: - connection_type = 'ZIG_ZAG' - elif 'Y' in winding_type: - connection_type = 'STAR' - elif 'D' in winding_type: - connection_type = 'DELTA' - elif 'CT' == winding_type: - connection_type = 'STAR' + connection_type = row["TertiaryConnection"] + + winding_type = self.connection_map.get(int(connection_type), "Y") + if winding_type == "Open Delta": + connection_type = "OPEN_DELTA" + elif "Delta" in winding_type: + connection_type = "DELTA" + elif "Z" in winding_type: + connection_type = "ZIG_ZAG" + elif "Y" in winding_type: + connection_type = "STAR" + elif "D" in winding_type: + connection_type = "DELTA" + elif "CT" == winding_type: + connection_type = "STAR" else: - connection_type = 'STAR' + connection_type = "STAR" return ConnectionType(connection_type) def map_tap_positions(self, row, winding_number, network_row): - num_phases = 3 - tap_location = network_row['LTC1_TapLocation'] - if tap_location == '': - return [0.0 for _ in range(num_phases)] + tap_location = network_row["LTC1_TapLocation"] + if tap_location == "": + return [1.0 for _ in range(num_phases)] if int(tap_location) != winding_number: - return [0.0 for _ in range(num_phases)] - + return [1.0 for _ in range(num_phases)] if network_row is None: - tap = 0.0 + tap = 1.0 else: - tap = network_row['LTC1_InitialTapPosition'] + tap = network_row["LTC1_InitialTapPosition"] tap = float(tap) / 100 tap_positions = [] for _ in range(1, num_phases + 1): @@ -285,23 +327,22 @@ def map_tap_positions(self, row, winding_number, network_row): return tap_positions def map_total_taps(self, row): - taps = row['LTC1_NumberOfTaps'] - if taps == '' or taps is None: - taps = 0 + taps = row["LTC1_NumberOfTaps"] + if taps == "" or taps is None: + taps = 32 total_taps = int(taps) return total_taps def min_tap_pu(self, row): - min_tap_pu = row['LTC1_MinimumRegulationRange'] - if min_tap_pu == '' or min_tap_pu is None: + min_tap_pu = row["LTC1_MinimumRegulationRange"] + if min_tap_pu == "" or min_tap_pu is None: return 0.9 - min_tap_pu = 1 - float(min_tap_pu)/100 + min_tap_pu = 1 - float(min_tap_pu) / 100 return float(min_tap_pu) def max_tap_pu(self, row): - max_tap_pu = row['LTC1_MaximumRegulationRange'] - if max_tap_pu == '' or max_tap_pu is None: + max_tap_pu = row["LTC1_MaximumRegulationRange"] + if max_tap_pu == "" or max_tap_pu is None: return 1.1 - max_tap_pu = 1 + float(max_tap_pu)/100 + max_tap_pu = 1 + float(max_tap_pu) / 100 return float(max_tap_pu) - diff --git a/src/ditto/readers/cyme/equipment/geometry_branch.py b/src/ditto/readers/cyme/equipment/geometry_branch.py deleted file mode 100644 index f82b100..0000000 --- a/src/ditto/readers/cyme/equipment/geometry_branch.py +++ /dev/null @@ -1,23 +0,0 @@ -from ditto.readers.cyme.cyme_mapper import CymeMapper - -class BareConductorElement(CymeMapper): - def __init__(self, system): - super().__init__(system) - cyme_file = 'Equipment' - cyme_section = 'CABLE' - - def parse(self, row): - name = self.map_name(row) - - return - -class GeometryBranch(CymeMapper): - def __init__(self, system): - super().__init__(system) - cyme_file = 'Equipment' - cyme_section = 'CABLE' - - def parse(self, row): - name = self.map_name(row) - - diff --git a/src/ditto/readers/cyme/equipment/geometry_branch_equipment.py b/src/ditto/readers/cyme/equipment/geometry_branch_equipment.py index 646ddb8..1cbf774 100644 --- a/src/ditto/readers/cyme/equipment/geometry_branch_equipment.py +++ b/src/ditto/readers/cyme/equipment/geometry_branch_equipment.py @@ -1,17 +1,17 @@ -from gdm.quantities import Current, Distance, ResistancePULength, Distance, Voltage -from ditto.readers.cyme.utils import read_cyme_data +from ditto.readers.cyme.constants import DEFAULT_BRANCH_AMPACITY, DEFAULT_BRANCH_RESISTANCE +from loguru import logger +from gdm.quantities import Current, Distance, ResistancePULength from ditto.readers.cyme.cyme_mapper import CymeMapper from gdm.distribution.equipment.geometry_branch_equipment import GeometryBranchEquipment from gdm.distribution.equipment.bare_conductor_equipment import BareConductorEquipment -from gdm.distribution.equipment.concentric_cable_equipment import ConcentricCableEquipment -from loguru import logger + class GeometryBranchEquipmentMapper(CymeMapper): def __init__(self, system): super().__init__(system) - cyme_file = 'Equipment' - cyme_section = 'LINE' + cyme_file = "Equipment" + cyme_section = "LINE" def parse(self, row, spacing_ids): name = self.map_name(row) @@ -19,25 +19,27 @@ def parse(self, row, spacing_ids): horizontal_positions = self.map_horizontal_positions(row, spacing_ids) vertical_positions = self.map_vertical_positions(row, spacing_ids) - return GeometryBranchEquipment.model_construct(name=name, - conductors=conductors, - horizontal_positions=horizontal_positions, - vertical_positions=vertical_positions) + return GeometryBranchEquipment.model_construct( + name=name, + conductors=conductors, + horizontal_positions=horizontal_positions, + vertical_positions=vertical_positions, + ) def map_name(self, row): - name = row['ID'] + name = row["ID"] return name def map_vertical_positions(self, row, spacing_ids): - spacing_id = row['SpacingID'] + spacing_id = row["SpacingID"] spacing = spacing_ids.loc[spacing_id] if not spacing.empty: vertical_positions = [] - cond1_y = spacing['PosOfCond1_Y'] - cond2_y = spacing['PosOfCond2_Y'] - cond3_y = spacing['PosOfCond3_Y'] - neutral_y = spacing['PosOfNeutralCond_Y'] + cond1_y = spacing["PosOfCond1_Y"] + cond2_y = spacing["PosOfCond2_Y"] + cond3_y = spacing["PosOfCond3_Y"] + neutral_y = spacing["PosOfNeutralCond_Y"] if cond1_y != "": y1 = float(cond1_y) vertical_positions.append(y1) @@ -50,18 +52,18 @@ def map_vertical_positions(self, row, spacing_ids): if neutral_y != "": y_n = float(neutral_y) vertical_positions.append(y_n) - return Distance(vertical_positions, 'feet').to('m') + return Distance(vertical_positions, "feet").to("m") return None def map_horizontal_positions(self, row, spacing_ids): - spacing_id = row['SpacingID'] + spacing_id = row["SpacingID"] spacing = spacing_ids.loc[spacing_id] if not spacing.empty: horizontal_positions = [] - cond1_x = spacing['PosOfCond1_X'] - cond2_x = spacing['PosOfCond2_X'] - cond3_x = spacing['PosOfCond3_X'] - neutral_x = spacing['PosOfNeutralCond_X'] + cond1_x = spacing["PosOfCond1_X"] + cond2_x = spacing["PosOfCond2_X"] + cond3_x = spacing["PosOfCond3_X"] + neutral_x = spacing["PosOfNeutralCond_X"] if cond1_x != "": x1 = float(cond1_x) horizontal_positions.append(x1) @@ -74,29 +76,43 @@ def map_horizontal_positions(self, row, spacing_ids): if neutral_x != "": x_n = float(neutral_x) horizontal_positions.append(x_n) - return Distance(horizontal_positions, 'feet').to('m') + return Distance(horizontal_positions, "feet").to("m") return None - def map_conductors(self,row, spacing_ids): - phase_conductor_name = row['PhaseCondID'] - neutral_conductor_name = row['NeutralCondID'] + def map_conductors(self, row, spacing_ids): + phase_conductor_name = row["PhaseCondID"] + neutral_conductor_name = row["NeutralCondID"] try: - phase_conductor = self.system.get_component(component_type=BareConductorEquipment, name=phase_conductor_name) + phase_conductor = self.system.get_component( + component_type=BareConductorEquipment, name=phase_conductor_name + ) except Exception as e: - phase_conductor = self.system.get_component(component_type=BareConductorEquipment, name="Default") + logger.warning( + f"Phase conductor {phase_conductor_name} not found in system. Using default conductor. Error: {e}" + ) + phase_conductor = self.system.get_component( + component_type=BareConductorEquipment, name="Default" + ) try: - neutral_conductor = self.system.get_component(component_type=BareConductorEquipment, name=neutral_conductor_name) - except: - neutral_conductor = self.system.get_component(component_type=BareConductorEquipment, name="Default") - - spacing_id = row['SpacingID'] + neutral_conductor = self.system.get_component( + component_type=BareConductorEquipment, name=neutral_conductor_name + ) + except Exception as e: + logger.warning( + f"Neutral conductor {neutral_conductor_name} not found in system. Using default conductor. Error: {e}" + ) + neutral_conductor = self.system.get_component( + component_type=BareConductorEquipment, name="Default" + ) + + spacing_id = row["SpacingID"] spacing = spacing_ids.loc[spacing_id] if not spacing.empty: conductors = [] - cond1 = spacing['PosOfCond1_X'] - cond2 = spacing['PosOfCond2_X'] - cond3 = spacing['PosOfCond3_X'] - neutral = spacing['PosOfNeutralCond_X'] + cond1 = spacing["PosOfCond1_X"] + cond2 = spacing["PosOfCond2_X"] + cond3 = spacing["PosOfCond3_X"] + neutral = spacing["PosOfNeutralCond_X"] if cond1 != "": conductors.append(phase_conductor) if cond2 != "": @@ -108,176 +124,75 @@ def map_conductors(self,row, spacing_ids): return conductors return None -class ConcentricCableEquipmentMapper(CymeMapper): - def __init__(self, system): - super().__init__(system) - - cyme_file = 'Equipment' - cyme_section = 'CABLE' - - def parse(self, row, equipment_file): - name = self.map_name(row) - strand_diameter = self.map_strand_diameter(row, equipment_file) - conductor_diameter = self.map_conductor_diameter(row, equipment_file) - cable_diameter = self.map_cable_diameter(row, equipment_file) - insulation_thickness = self.map_insulation_thickness(row, equipment_file) - insulation_diameter = conductor_diameter+2*insulation_thickness + Distance(0.001,'mm') - cable_diameter = insulation_diameter + 2* strand_diameter - amapcity = self.map_ampacity(row) - conductor_gmr = conductor_diameter/2 *0.7788 - strand_gmr = strand_diameter/2 * 0.7788 - phase_ac_resistance = self.map_phase_ac_resistance(row, equipment_file) - strand_ac_resistance = self.map_strand_ac_resistance(row, equipment_file) - num_neutral_strands = self.map_num_neutral_strands(row, equipment_file) - rated_voltage = self.map_rated_voltage(row) - return ConcentricCableEquipment.model_construct(name=name, - strand_diameter=strand_diameter, - conductor_diameter=conductor_diameter, - cable_diameter=cable_diameter, - insulation_thickness=insulation_thickness, - insulation_diameter=insulation_diameter, - ampacity=amapcity, - conductor_gmr=conductor_gmr, - strand_gmr=strand_gmr, - phase_ac_resistance=phase_ac_resistance, - strand_ac_resistance=strand_ac_resistance, - num_neutral_strands=num_neutral_strands, - rated_voltage=rated_voltage) - - def map_name(self, row): - name = row['ID'] - return name - - def map_conductor_diameter(self, row, equipment_file): - cable_name = row['ID'] - conductor_info = read_cyme_data(equipment_file,"CABLE CONDUCTOR") - for idx, row in conductor_info.iterrows(): - if row['ID'] == cable_name: - conductor_diameter = float(row['Diameter']) - return Distance(conductor_diameter,'inch').to('mm') - return None - - - def map_strand_diameter(self, row, equipment_file): - cable_name = row['ID'] - strand_info = read_cyme_data(equipment_file,"CABLE CONCENTRIC NEUTRAL") - for idx, row in strand_info.iterrows(): - if row['ID'] == cable_name: - strand_diameter = float(row['Thickness']) - return Distance(strand_diameter,'inch').to('mm') - return Distance(0.01,'mm') - - def map_insulation_thickness(self, row, equipment_file): - # TODO: Understand how this is actually represented - return Distance(0.001,'mm') - - def map_insulation_diameter(self, row, equipment_file): - # TODO: Understand how this is actually represented - pass - - def map_conductor_gmr(self, row, equipment_file): - pass - - def map_strand_gmr(self, row, equipment_file): - pass - - def map_cable_diameter(self, row, equipment_file): - # TODO: should this be changed? - pass - - def map_phase_ac_resistance(self, row, equipment_file): - resistance = ResistancePULength(float(row['R1']), 'ohm/mile').to('ohm/km') - return resistance - - def map_strand_ac_resistance(self, row, equipment_file): - # Using the same as for Cable. Need to check this... - resistance = ResistancePULength(float(row['R1']), 'ohm/mile').to('ohm/km') - return resistance - - def map_num_neutral_strands(self, row, equipment_file): - cable_name = row['ID'] - strand_info = read_cyme_data(equipment_file,"CABLE CONCENTRIC NEUTRAL") - for idx, row in strand_info.iterrows(): - if row['ID'] == cable_name: - strand_number = float(row['NumberOfWires']) - return strand_number - - return 1 - - def map_rated_voltage(self, row): - # No voltage included. Need to remove this as a required field - return Voltage(23.0, "kilovolts") - - - def map_ampacity(self, row): - ampacity = Current(float(row['Amps']),'amp') - return ampacity class BareConductorEquipmentMapper(CymeMapper): def __init__(self, system): super().__init__(system) - cyme_file = 'Equipment' - cyme_section = 'CONDUCTOR' + cyme_file = "Equipment" + cyme_section = "CONDUCTOR" def parse(self, row): name = self.map_name(row) - conductor_diameter = self.map_conductor_diameter(row) + conductor_diameter = self.map_conductor_diameter(row) conductor_gmr = self.map_conductor_gmr(row) ampacity = self.map_ampacity(row) emergency_ampacity = self.map_emergency_ampacity(row) ac_resistance = self.map_ac_resistance(row) dc_resistance = self.map_dc_resistance(row) - return BareConductorEquipment.model_construct(name=name, - conductor_diameter=conductor_diameter, - conductor_gmr=conductor_gmr, - ampacity=ampacity, - emergency_ampacity=emergency_ampacity, - ac_resistance=ac_resistance, - dc_resistance=dc_resistance) + return BareConductorEquipment.model_construct( + name=name, + conductor_diameter=conductor_diameter, + conductor_gmr=conductor_gmr, + ampacity=ampacity, + emergency_ampacity=emergency_ampacity, + ac_resistance=ac_resistance, + dc_resistance=dc_resistance, + ) def map_name(self, row): - name = row['ID'] + name = row["ID"] return name def map_conductor_diameter(self, row): - conductor_diameter = float(row['Diameter']) - return Distance(conductor_diameter,'inch').to('mm') + conductor_diameter = float(row["Diameter"]) + return Distance(conductor_diameter, "inch").to("mm") def map_conductor_gmr(self, row): - conductor_gmr = Distance(float(row['GMR']),'inch').to('mm') + conductor_gmr = Distance(float(row["GMR"]), "inch").to("mm") return conductor_gmr def map_ampacity(self, row): - ampacity = Current(float(row['Amps']),'amp') + ampacity = Current(float(row["Amps"]), "amp") if ampacity == 0.0: - ampacity = Current(600.0,'amp') + ampacity = DEFAULT_BRANCH_AMPACITY return ampacity def map_emergency_ampacity(self, row): - emergency_ampacity = Current(float(row['Amps_4']),'amp') + emergency_ampacity = Current(float(row["Amps_4"]), "amp") if emergency_ampacity == 0.0: - emergency_ampacity = Current(600.0,'amp') + emergency_ampacity = DEFAULT_BRANCH_AMPACITY return emergency_ampacity def map_ac_resistance(self, row): - ac_resistance = ResistancePULength(float(row['R25']),'ohm/mile').to('ohm/km') + ac_resistance = ResistancePULength(float(row["R25"]), "ohm/mile").to("ohm/km") if ac_resistance == 0.0: - ac_resistance = ResistancePULength(0.555000,'ohm/mile').to('ohm/km') + ac_resistance = DEFAULT_BRANCH_RESISTANCE return ac_resistance def map_dc_resistance(self, row): - dc_resistance = ResistancePULength(float(row['R25']),'ohm/mile').to('ohm/km') + dc_resistance = ResistancePULength(float(row["R25"]), "ohm/mile").to("ohm/km") if dc_resistance == 0.0: - dc_resistance = ResistancePULength(0.555000,'ohm/mile').to('ohm/km') + dc_resistance = DEFAULT_BRANCH_RESISTANCE return dc_resistance + class GeometryBranchByPhaseEquipmentMapper(CymeMapper): def __init__(self, system): super().__init__(system) - cyme_file = 'Network' - cyme_section = 'OVERHEAD BYPHASE SETTING' + cyme_file = "Network" + cyme_section = "OVERHEAD BYPHASE SETTING" def parse(self, row, spacing_ids): name = self.map_name(row) @@ -285,32 +200,34 @@ def parse(self, row, spacing_ids): horizontal_positions = self.map_horizontal_positions(row, spacing_ids) vertical_positions = self.map_vertical_positions(row, spacing_ids) - return GeometryBranchEquipment.model_construct(name=name, - conductors=conductors, - horizontal_positions=horizontal_positions, - vertical_positions=vertical_positions) + return GeometryBranchEquipment.model_construct( + name=name, + conductors=conductors, + horizontal_positions=horizontal_positions, + vertical_positions=vertical_positions, + ) def map_name(self, row): - name = row['SectionID'] + name = row["DeviceNumber"] return name def map_vertical_positions(self, row, spacing_ids): - phase_A_conductor_name = row['CondID_A'] - phase_B_conductor_name = row['CondID_B'] - phase_C_conductor_name = row['CondID_C'] - if 'CondID_N1' in row: - neutral_conductor_name = row['CondID_N1'] + phase_A_conductor_name = row["CondID_A"] + phase_B_conductor_name = row["CondID_B"] + phase_C_conductor_name = row["CondID_C"] + if "CondID_N1" in row: + neutral_conductor_name = row["CondID_N1"] else: - neutral_conductor_name = row['CondID_N'] - spacing_id = row['SpacingID'] + neutral_conductor_name = row["CondID_N"] + spacing_id = row["SpacingID"] spacing = spacing_ids.loc[spacing_id] if not spacing.empty: vertical_positions = [] - cond1_y = spacing['PosOfCond1_Y'] - cond2_y = spacing['PosOfCond2_Y'] - cond3_y = spacing['PosOfCond3_Y'] - neutral_y = spacing['PosOfNeutralCond_Y'] + cond1_y = spacing["PosOfCond1_Y"] + cond2_y = spacing["PosOfCond2_Y"] + cond3_y = spacing["PosOfCond3_Y"] + neutral_y = spacing["PosOfNeutralCond_Y"] if cond1_y != "" and phase_A_conductor_name != "NONE": y1 = float(cond1_y) vertical_positions.append(y1) @@ -323,25 +240,25 @@ def map_vertical_positions(self, row, spacing_ids): if neutral_y != "" and neutral_conductor_name != "NONE": y_n = float(neutral_y) vertical_positions.append(y_n) - return Distance(vertical_positions, 'feet').to('m') + return Distance(vertical_positions, "feet").to("m") return None def map_horizontal_positions(self, row, spacing_ids): - phase_A_conductor_name = row['CondID_A'] - phase_B_conductor_name = row['CondID_B'] - phase_C_conductor_name = row['CondID_C'] - if 'CondID_N1' in row: - neutral_conductor_name = row['CondID_N1'] + phase_A_conductor_name = row["CondID_A"] + phase_B_conductor_name = row["CondID_B"] + phase_C_conductor_name = row["CondID_C"] + if "CondID_N1" in row: + neutral_conductor_name = row["CondID_N1"] else: - neutral_conductor_name = row['CondID_N'] - spacing_id = row['SpacingID'] + neutral_conductor_name = row["CondID_N"] + spacing_id = row["SpacingID"] spacing = spacing_ids.loc[spacing_id] if not spacing.empty: horizontal_positions = [] - cond1_x = spacing['PosOfCond1_X'] - cond2_x = spacing['PosOfCond2_X'] - cond3_x = spacing['PosOfCond3_X'] - neutral_x = spacing['PosOfNeutralCond_X'] + cond1_x = spacing["PosOfCond1_X"] + cond2_x = spacing["PosOfCond2_X"] + cond3_x = spacing["PosOfCond3_X"] + neutral_x = spacing["PosOfNeutralCond_X"] if cond1_x != "" and phase_A_conductor_name != "NONE": x1 = float(cond1_x) horizontal_positions.append(x1) @@ -354,17 +271,14 @@ def map_horizontal_positions(self, row, spacing_ids): if neutral_x != "" and neutral_conductor_name != "NONE": x_n = float(neutral_x) horizontal_positions.append(x_n) - return Distance(horizontal_positions, 'feet').to('m') + return Distance(horizontal_positions, "feet").to("m") return None - def map_conductors(self,row, spacing_ids): - phase_A_conductor_name = row['CondID_A'] - phase_B_conductor_name = row['CondID_B'] - phase_C_conductor_name = row['CondID_C'] - if 'CondID_N1' in row: - neutral_conductor_name = row['CondID_N1'] - else: - neutral_conductor_name = row['CondID_N'] + def map_conductors(self, row, spacing_ids): + phase_A_conductor_name = row["CondID_A"] + phase_B_conductor_name = row["CondID_B"] + phase_C_conductor_name = row["CondID_C"] + neutral_conductor_name = row["CondID_N1"] if "CondID_N1" in row else row["CondID_N"] phase_A_conductor = None phase_B_conductor = None @@ -372,23 +286,31 @@ def map_conductors(self,row, spacing_ids): neutral_conductor = None if phase_A_conductor_name != "NONE": - phase_A_conductor = self.system.get_component(component_type=BareConductorEquipment, name=phase_A_conductor_name) + phase_A_conductor = self.system.get_component( + component_type=BareConductorEquipment, name=phase_A_conductor_name + ) if phase_B_conductor_name != "NONE": - phase_B_conductor = self.system.get_component(component_type=BareConductorEquipment, name=phase_B_conductor_name) + phase_B_conductor = self.system.get_component( + component_type=BareConductorEquipment, name=phase_B_conductor_name + ) if phase_C_conductor_name != "NONE": - phase_C_conductor = self.system.get_component(component_type=BareConductorEquipment, name=phase_C_conductor_name) + phase_C_conductor = self.system.get_component( + component_type=BareConductorEquipment, name=phase_C_conductor_name + ) if neutral_conductor_name != "NONE": - neutral_conductor = self.system.get_component(component_type=BareConductorEquipment, name=neutral_conductor_name) - - spacing_id = row['SpacingID'] + neutral_conductor = self.system.get_component( + component_type=BareConductorEquipment, name=neutral_conductor_name + ) + spacing_id = row["SpacingID"] + # Spacing is used to see how many conductors there are spacing = spacing_ids.loc[spacing_id] if not spacing.empty: conductors = [] - cond1 = spacing['PosOfCond1_X'] - cond2 = spacing['PosOfCond2_X'] - cond3 = spacing['PosOfCond3_X'] - neutral = spacing['PosOfNeutralCond_X'] + cond1 = spacing["PosOfCond1_X"] + cond2 = spacing["PosOfCond2_X"] + cond3 = spacing["PosOfCond3_X"] + neutral = spacing["PosOfNeutralCond_X"] if cond1 != "" and phase_A_conductor is not None: conductors.append(phase_A_conductor) if cond2 != "" and phase_B_conductor is not None: @@ -398,5 +320,5 @@ def map_conductors(self,row, spacing_ids): if neutral != "" and neutral_conductor is not None: conductors.append(neutral_conductor) - return conductors - return None \ No newline at end of file + return conductors + return None diff --git a/src/ditto/readers/cyme/equipment/load_equipment.py b/src/ditto/readers/cyme/equipment/load_equipment.py index cb19d4d..2688604 100644 --- a/src/ditto/readers/cyme/equipment/load_equipment.py +++ b/src/ditto/readers/cyme/equipment/load_equipment.py @@ -1,16 +1,17 @@ -from loguru import logger from ditto.readers.cyme.cyme_mapper import CymeMapper from gdm.distribution.equipment.load_equipment import LoadEquipment from gdm.distribution.equipment.phase_load_equipment import PhaseLoadEquipment from gdm.quantities import ActivePower, ReactivePower from gdm.distribution.enums import ConnectionType +from loguru import logger + class LoadEquipmentMapper(CymeMapper): def __init__(self, system): super().__init__(system) - cyme_file = 'Load' - cyme_section = 'LOADS' + cyme_file = "Load" + cyme_section = "LOADS" def parse(self, row, network_row): name = self.map_name(row) @@ -19,24 +20,24 @@ def parse(self, row, network_row): phase_loads = self.map_phase_loads(network_row) if any(pl is None for pl in phase_loads): return None - return LoadEquipment.model_construct(name=name, - phase_loads=phase_loads, - connection_type=connection_type) + return LoadEquipment.model_construct( + name=name, phase_loads=phase_loads, connection_type=connection_type + ) def map_name(self, row): - return row['DeviceNumber'] + return row["DeviceNumber"] def map_connection_type(self, row): - connection_number = int(row['Connection']) + connection_number = int(row["Connection"]) connection_map = { - 0: ConnectionType.STAR, #Yg - 1: ConnectionType.STAR, #Y - 2: ConnectionType.DELTA, #Delta - 3: ConnectionType.OPEN_DELTA, #Open Delta - 4: ConnectionType.DELTA, #Closed Delta - 5: ConnectionType.ZIG_ZAG, # Zg - 6: ConnectionType.STAR, # CT - 7: ConnectionType.DELTA, # Dg - Not sure what this is? + 0: ConnectionType.STAR, # Yg + 1: ConnectionType.STAR, # Y + 2: ConnectionType.DELTA, # Delta + 3: ConnectionType.OPEN_DELTA, # Open Delta + 4: ConnectionType.DELTA, # Closed Delta + 5: ConnectionType.ZIG_ZAG, # Zg + 6: ConnectionType.STAR, # CT + 7: ConnectionType.DELTA, # Dg - Not sure what this is? } return connection_map[connection_number] @@ -47,18 +48,20 @@ def map_phase_loads(self, row): phase_loads = [phase_load_equipment] return phase_loads + class PhaseLoadEquipmentMapper(CymeMapper): def __init__(self, system): super().__init__(system) - cyme_file = 'Load' + cyme_file = "Load" cyme_section = "CUSTOMER LOADS" def parse(self, row): name = self.map_name(row) real_power = self.map_real_power(row) reactive_power = self.map_reactive_power(row) - if real_power ==0 and reactive_power ==0: + if real_power == 0 and reactive_power == 0: + logger.warning(f"Load {name} has 0 kW and 0 kVAR. Skipping...") return None z_real = self.map_z_real(row) z_imag = self.map_z_imag(row) @@ -66,54 +69,55 @@ def parse(self, row): i_imag = self.map_i_imag(row) p_real = self.map_p_real(row) p_imag = self.map_p_imag(row) - return PhaseLoadEquipment(name=name, - real_power=real_power, - reactive_power=reactive_power, - z_real=z_real, - z_imag=z_imag, - i_real=i_real, - i_imag=i_imag, - p_real=p_real, - p_imag=p_imag) + return PhaseLoadEquipment( + name=name, + real_power=real_power, + reactive_power=reactive_power, + z_real=z_real, + z_imag=z_imag, + i_real=i_real, + i_imag=i_imag, + p_real=p_real, + p_imag=p_imag, + ) def map_name(self, row): - phase = row['LoadPhase'] - return row['DeviceNumber']+"_"+str(phase) - + phase = row["LoadPhase"] + return row["DeviceNumber"] + "_" + str(phase) + def compute_powers(self, row): - v1 = float(row['Value1']) - v2 = float(row['Value2']) + v1 = float(row["Value1"]) + v2 = float(row["Value2"]) kw = None kvar = None - value_type = int(row['ValueType']) + value_type = int(row["ValueType"]) # kw and kvar if value_type == 0: kw = v1 kvar = v2 # kva and pf elif value_type == 1: - v2 = v2/100.0 + v2 = v2 / 100.0 kw = v1 * v2 kvar = v1 * (1 - v2**2) ** 0.5 # kw and pf elif value_type == 2: - v2 = v2/100.0 + v2 = v2 / 100.0 kw = v1 - kvar = v1 * (1/v2**2 - 1) ** 0.5 + kvar = v1 * (1 / v2**2 - 1) ** 0.5 # amp and pf else: pass - return ActivePower(kw, 'kilowatt'), ReactivePower(kvar, 'kilovar') - + return ActivePower(kw, "kilowatt"), ReactivePower(kvar, "kilovar") def map_real_power(self, row): kw, kvar = self.compute_powers(row) - return ActivePower(kw, 'kilowatt') + return ActivePower(kw, "kilowatt") def map_reactive_power(self, row): kw, kvar = self.compute_powers(row) - return ReactivePower(kvar, 'kilovar') - + return ReactivePower(kvar, "kilovar") + # Is this included in CYME 9.* ? It was in customer class in previous cyme versions def map_z_real(self, row): return 1 @@ -131,4 +135,4 @@ def map_p_real(self, row): return 0 def map_p_imag(self, row): - return 0 + return 0 diff --git a/src/ditto/readers/cyme/equipment/matrix_impedance_branch_equipment.py b/src/ditto/readers/cyme/equipment/matrix_impedance_branch_equipment.py index a41933f..3a308a8 100644 --- a/src/ditto/readers/cyme/equipment/matrix_impedance_branch_equipment.py +++ b/src/ditto/readers/cyme/equipment/matrix_impedance_branch_equipment.py @@ -1,17 +1,18 @@ - -from gdm.quantities import Current, Distance, ResistancePULength, Distance, Voltage +from gdm.quantities import ResistancePULength from ditto.readers.cyme.cyme_mapper import CymeMapper -from gdm.distribution.equipment.matrix_impedance_branch_equipment import MatrixImpedanceBranchEquipment -from gdm.distribution.enums import Phase +from gdm.distribution.equipment.matrix_impedance_branch_equipment import ( + MatrixImpedanceBranchEquipment, +) import numpy as np + class MatrixImpedanceBranchEquipmentMapper(CymeMapper): def __init__(self, system): super().__init__(system) - cyme_file = 'Equipment' - cyme_section = 'CABLE' - + cyme_file = "Equipment" + cyme_section = "CABLE" + def _sequence_impedance_to_phase_impedance_matrix(self, r1, r0, phases=3): """ Return the phase resistance matrix given @@ -19,18 +20,15 @@ def _sequence_impedance_to_phase_impedance_matrix(self, r1, r0, phases=3): Assumes r2 = r1 (typical for transposed lines/cables). """ if phases == 3: - r_s = (r0 + 2*r1) / 3.0 # self term - r_m = (r0 - r1) / 3.0 # mutual term - R = np.array([[r_s, r_m, r_m], - [r_m, r_s, r_m], - [r_m, r_m, r_s]], dtype=float) + r_s = (r0 + 2 * r1) / 3.0 # self term + r_m = (r0 - r1) / 3.0 # mutual term + R = np.array([[r_s, r_m, r_m], [r_m, r_s, r_m], [r_m, r_m, r_s]], dtype=float) elif phases == 2: - r_s = (r0 + 2*r1) / 3.0 # self term - r_m = (r0 - r1) / 3.0 # mutual term - R = np.array([[r_s, r_m], - [r_m, r_s]], dtype=float) + r_s = (r0 + 2 * r1) / 3.0 # self term + r_m = (r0 - r1) / 3.0 # mutual term + R = np.array([[r_s, r_m], [r_m, r_s]], dtype=float) elif phases == 1: - r_s = (r0 + 2*r1) / 3.0 # self term + r_s = (r0 + 2 * r1) / 3.0 # self term R = np.array([[r_s]], dtype=float) return R @@ -42,11 +40,13 @@ def parse(self, row, phases): c_matrix = self.map_c_matrix(row, num_phases) ampacity = self.map_ampacity(row) try: - return MatrixImpedanceBranchEquipment(name=name, - r_matrix=r_matrix, - x_matrix=x_matrix, - c_matrix=c_matrix, - ampacity=ampacity) + return MatrixImpedanceBranchEquipment( + name=name, + r_matrix=r_matrix, + x_matrix=x_matrix, + c_matrix=c_matrix, + ampacity=ampacity, + ) except Exception as e: print(f"Error creating MatrixImpedanceBranchEquipment {name}: {e}") return None @@ -54,31 +54,31 @@ def parse(self, row, phases): def map_name(self, row, phases): name = f"{row['ID']}_{phases}" return name - + def map_r_matrix(self, row, phases): - r1 = float(row['R1']) - r0 = float(row['R0']) + r1 = float(row["R1"]) + r0 = float(row["R0"]) matrix = self._sequence_impedance_to_phase_impedance_matrix(r1, r0, phases) matrix = ResistancePULength(np.array(matrix), "ohm/mile") return matrix def map_x_matrix(self, row, phases): - x1 = float(row['X1']) - x0 = float(row['X0']) + x1 = float(row["X1"]) + x0 = float(row["X0"]) matrix = self._sequence_impedance_to_phase_impedance_matrix(x1, x0, phases) - matrix = ResistancePULength(np.array(matrix), "ohm/mile") + matrix = ResistancePULength(np.array(matrix), "ohm/mile") return matrix def map_c_matrix(self, row, phases): - b1 = float(row['B1']) - b0 = float(row['B0']) + b1 = float(row["B1"]) + b0 = float(row["B0"]) susceptance_matrix = self._sequence_impedance_to_phase_impedance_matrix(b1, b0, phases) # Convert susceptance to capacitance: C = B / (2 * pi * f) frequency = 60 # Hz capacitance_matrix = susceptance_matrix / (2 * np.pi * frequency) - capacitance_matrix = ResistancePULength(np.array(capacitance_matrix), "farad/mile") + capacitance_matrix = ResistancePULength(np.array(capacitance_matrix), "microfarad/mile") return capacitance_matrix def map_ampacity(self, row): - ampacity = float(row['Amps']) - return ampacity \ No newline at end of file + ampacity = float(row["Amps"]) + return ampacity diff --git a/src/ditto/readers/cyme/equipment/matrix_impedance_fuse_equipment.py b/src/ditto/readers/cyme/equipment/matrix_impedance_fuse_equipment.py index 69d80cc..1323772 100644 --- a/src/ditto/readers/cyme/equipment/matrix_impedance_fuse_equipment.py +++ b/src/ditto/readers/cyme/equipment/matrix_impedance_fuse_equipment.py @@ -1,17 +1,19 @@ from ditto.readers.cyme.cyme_mapper import CymeMapper -from gdm.quantities import Distance, Current, ResistancePULength, ReactancePULength, CapacitancePULength +from gdm.quantities import Current, ResistancePULength, ReactancePULength, CapacitancePULength from gdm.distribution.equipment.matrix_impedance_fuse_equipment import MatrixImpedanceFuseEquipment from gdm.distribution.common.curve import TimeCurrentCurve from infrasys.quantities import Time from gdm.distribution.enums import LineType +from ditto.readers.cyme.constants import DEFAULT_C_MATRIX, DEFAULT_X_MATRIX, DEFAULT_R_MATRIX + class MatrixImpedanceFuseEquipmentMapper(CymeMapper): def __init__(self, system): super().__init__(system) - cyme_file = 'Equipment' - cyme_section = 'FUSE' + cyme_file = "Equipment" + cyme_section = "FUSE" def parse(self, row, phases): name = self.map_name(row, phases) @@ -30,55 +32,42 @@ def parse(self, row, phases): r_matrix=r_matrix, x_matrix=x_matrix, c_matrix=c_matrix, - ampacity=ampacity + ampacity=ampacity, ) def map_name(self, row, phases): return f"{row['ID']}_{len(phases)}" def map_r_matrix(self, phases): - default_matrix = [ - [1e-6, 0.0, 0.0], - [0.0, 1e-6, 0.0], - [0.0, 0.0, 1e-6], - ] - matrix = [row[:len(phases)] for row in default_matrix[:len(phases)]] + default_matrix = DEFAULT_R_MATRIX + matrix = [row[: len(phases)] for row in default_matrix[: len(phases)]] return ResistancePULength( matrix, "ohm/mi", ) def map_x_matrix(self, phases): - default_matrix = [ - [1e-4, 0.0, 0.0], - [0.0, 1e-4, 0.0], - [0.0, 0.0, 1e-4], - ] - matrix = [row[:len(phases)] for row in default_matrix[:len(phases)]] + default_matrix = DEFAULT_X_MATRIX + matrix = [row[: len(phases)] for row in default_matrix[: len(phases)]] return ReactancePULength( - matrix, - "ohm/mi", - ) + matrix, + "ohm/mi", + ) def map_c_matrix(self, phases): - default_matrix = [ - [0.0, 0.0, 0.0], - [0.0, 0.0, 0.0], - [0.0, 0.0, 0.0], - ] - matrix = [row[:len(phases)] for row in default_matrix[:len(phases)]] + default_matrix = DEFAULT_C_MATRIX + matrix = [row[: len(phases)] for row in default_matrix[: len(phases)]] return CapacitancePULength( - matrix, - "nanofarad/mi", - ) + matrix, + "nanofarad/mi", + ) def map_ampacity(self, row): - return Current(float(row['Amps']), "ampere") - + return Current(float(row["Amps"]), "ampere") + def map_delay(self, row): return Time(0, "minutes") - + def map_tcc_curve(self, row): return TimeCurrentCurve.example() - diff --git a/src/ditto/readers/cyme/equipment/matrix_impedance_recloser_equipment.py b/src/ditto/readers/cyme/equipment/matrix_impedance_recloser_equipment.py index f2ad7c9..9291aab 100644 --- a/src/ditto/readers/cyme/equipment/matrix_impedance_recloser_equipment.py +++ b/src/ditto/readers/cyme/equipment/matrix_impedance_recloser_equipment.py @@ -1,14 +1,18 @@ from ditto.readers.cyme.cyme_mapper import CymeMapper from gdm.quantities import Current, ResistancePULength, ReactancePULength, CapacitancePULength -from gdm.distribution.equipment.matrix_impedance_recloser_equipment import MatrixImpedanceRecloserEquipment +from gdm.distribution.equipment.matrix_impedance_recloser_equipment import ( + MatrixImpedanceRecloserEquipment, +) from gdm.distribution.enums import LineType +from ditto.readers.cyme.constants import DEFAULT_C_MATRIX, DEFAULT_X_MATRIX, DEFAULT_R_MATRIX + class MatrixImpedanceRecloserEquipmentMapper(CymeMapper): def __init__(self, system): super().__init__(system) - cyme_file = 'Equipment' - cyme_section = 'RECLOSER' + cyme_file = "Equipment" + cyme_section = "RECLOSER" def parse(self, row, phases): name = self.map_name(row, phases) @@ -23,50 +27,36 @@ def parse(self, row, phases): r_matrix=r_matrix, x_matrix=x_matrix, c_matrix=c_matrix, - ampacity=ampacity + ampacity=ampacity, ) def map_name(self, row, phases): return f"{row['ID']}_{len(phases)}" def map_r_matrix(self, phases): - default_matrix = [ - [1e-6, 0.0, 0.0], - [0.0, 1e-6, 0.0], - [0.0, 0.0, 1e-6], - ] - matrix = [row[:len(phases)] for row in default_matrix[:len(phases)]] + default_matrix = DEFAULT_R_MATRIX + matrix = [row[: len(phases)] for row in default_matrix[: len(phases)]] return ResistancePULength( matrix, "ohm/mi", ) def map_x_matrix(self, phases): - default_matrix = [ - [1e-4, 0.0, 0.0], - [0.0, 1e-4, 0.0], - [0.0, 0.0, 1e-4], - ] - matrix = [row[:len(phases)] for row in default_matrix[:len(phases)]] + default_matrix = DEFAULT_X_MATRIX + matrix = [row[: len(phases)] for row in default_matrix[: len(phases)]] return ReactancePULength( - matrix, - "ohm/mi", - ) + matrix, + "ohm/mi", + ) def map_c_matrix(self, phases): - default_matrix = [ - [0.0, 0.0, 0.0], - [0.0, 0.0, 0.0], - [0.0, 0.0, 0.0], - ] - matrix = [row[:len(phases)] for row in default_matrix[:len(phases)]] + default_matrix = DEFAULT_C_MATRIX + matrix = [row[: len(phases)] for row in default_matrix[: len(phases)]] return CapacitancePULength( - matrix, - "nanofarad/mi", - ) + matrix, + "nanofarad/mi", + ) def map_ampacity(self, row): - return Current(float(row['Amps']), "ampere") - - + return Current(float(row["Amps"]), "ampere") diff --git a/src/ditto/readers/cyme/equipment/matrix_impedance_switch_equipment.py b/src/ditto/readers/cyme/equipment/matrix_impedance_switch_equipment.py index 965e6f7..15d77a7 100644 --- a/src/ditto/readers/cyme/equipment/matrix_impedance_switch_equipment.py +++ b/src/ditto/readers/cyme/equipment/matrix_impedance_switch_equipment.py @@ -1,14 +1,19 @@ from ditto.readers.cyme.cyme_mapper import CymeMapper -from gdm.quantities import Distance, Current, ResistancePULength, ReactancePULength, CapacitancePULength -from gdm.distribution.equipment.matrix_impedance_switch_equipment import MatrixImpedanceSwitchEquipment +from gdm.quantities import Current, ResistancePULength, ReactancePULength, CapacitancePULength +from gdm.distribution.equipment.matrix_impedance_switch_equipment import ( + MatrixImpedanceSwitchEquipment, +) from gdm.distribution.enums import LineType +from ditto.readers.cyme.constants import DEFAULT_C_MATRIX, DEFAULT_X_MATRIX + + class MatrixImpedanceSwitchEquipmentMapper(CymeMapper): def __init__(self, system): super().__init__(system) - cyme_file = 'Equipment' - cyme_section = 'SWITCH' + cyme_file = "Equipment" + cyme_section = "SWITCH" def parse(self, row, phases): name = self.map_name(row, phases) @@ -23,50 +28,36 @@ def parse(self, row, phases): r_matrix=r_matrix, x_matrix=x_matrix, c_matrix=c_matrix, - ampacity=ampacity + ampacity=ampacity, ) def map_name(self, row, phases): return f"{row['ID']}_{len(phases)}" def map_r_matrix(self, phases): - default_matrix = [ - [1e-6, 0.0, 0.0], - [0.0, 1e-6, 0.0], - [0.0, 0.0, 1e-6], - ] - matrix = [row[:len(phases)] for row in default_matrix[:len(phases)]] + default_matrix = DEFAULT_C_MATRIX + matrix = [row[: len(phases)] for row in default_matrix[: len(phases)]] return ResistancePULength( matrix, "ohm/mi", ) def map_x_matrix(self, phases): - default_matrix = [ - [1e-4, 0.0, 0.0], - [0.0, 1e-4, 0.0], - [0.0, 0.0, 1e-4], - ] - matrix = [row[:len(phases)] for row in default_matrix[:len(phases)]] + default_matrix = DEFAULT_X_MATRIX + matrix = [row[: len(phases)] for row in default_matrix[: len(phases)]] return ReactancePULength( - matrix, - "ohm/mi", - ) + matrix, + "ohm/mi", + ) def map_c_matrix(self, phases): - default_matrix = [ - [0.0, 0.0, 0.0], - [0.0, 0.0, 0.0], - [0.0, 0.0, 0.0], - ] - matrix = [row[:len(phases)] for row in default_matrix[:len(phases)]] + default_matrix = DEFAULT_C_MATRIX + matrix = [row[: len(phases)] for row in default_matrix[: len(phases)]] return CapacitancePULength( - matrix, - "nanofarad/mi", - ) + matrix, + "nanofarad/mi", + ) def map_ampacity(self, row): - return Current(float(row['Amps']), "ampere") - - + return Current(float(row["Amps"]), "ampere") diff --git a/src/ditto/readers/cyme/reader.py b/src/ditto/readers/cyme/reader.py index fe55aea..b553e08 100644 --- a/src/ditto/readers/cyme/reader.py +++ b/src/ditto/readers/cyme/reader.py @@ -1,5 +1,3 @@ -from gdm.quantities import Distance, Current, ResistancePULength -from gdm.distribution.equipment.bare_conductor_equipment import BareConductorEquipment from gdm.distribution.distribution_system import DistributionSystem from ditto.readers.reader import AbstractReader from ditto.readers.cyme.utils import read_cyme_data, network_truncation @@ -34,7 +32,6 @@ class Reader(AbstractReader): "GeometryBranchEquipment", "GeometryBranchByPhaseEquipment", "GeometryBranch", - "GeometryBranchByPhase", "DistributionTransformerByPhase", "DistributionTransformer", "DistributionTransformerThreeWinding", @@ -83,20 +80,9 @@ def read( "MatrixImpedanceFuseEquipmentMapper", ] ) - default_conductor = BareConductorEquipment( - name="Default", - conductor_diameter=Distance(0.368000, "inch").to("mm"), - conductor_gmr=Distance(0.133200, "inch").to("mm"), - ampacity=Current(600.0, "amp"), - emergency_ampacity=Current(600.0, "amp"), - ac_resistance=ResistancePULength(0.555000, "ohm/mile").to("ohm/km"), - dc_resistance=ResistancePULength(0.555000, "ohm/mile").to("ohm/km"), - ) - self.system.add_component(default_conductor) node_feeder_map = {} node_substation_map = {} - network_voltage_map = {} load_record = {} used_sections = set() @@ -104,7 +90,6 @@ def read( network_file, "SECTION", node_feeder_map=node_feeder_map, - network_voltage_map=network_voltage_map, node_substation_map=node_substation_map, parse_feeders=True, parse_substation=True, @@ -159,8 +144,11 @@ def read( read_cyme_data(load_file, "LOADS", index_col="DeviceNumber"), load_record, ], - "GeometryBranchMapper": lambda: [used_sections, section_id_sections], - "GeometryBranchByPhaseMapper": lambda: [used_sections, section_id_sections], + "GeometryBranchMapper": lambda: [ + used_sections, + section_id_sections, + cyme_section, + ], "BareConductorEquipmentMapper": lambda: [], "GeometryBranchEquipmentMapper": lambda: [ read_cyme_data(equipment_file, "SPACING TABLE FOR LINE", index_col="ID") @@ -279,7 +267,7 @@ def _validate_model(self): console = Console() console.print(error_table) raise Exception( - "Validations errors occured when running the script. See the table above" + "Validations errors occurred when running the script. See the table above" ) def _prepare_data( @@ -359,7 +347,7 @@ def _start_queue_w_voltage_sources(self): vsource.bus.rated_voltage = ( vsource.equipment.sources[0].voltage * 1.732 if len(vsource.phases) > 1 - else vsource.equipment[0].voltage + else vsource.equipment.sources[0].voltage ) bus_queue.add(vsource.bus.name) diff --git a/src/ditto/readers/cyme/utils.py b/src/ditto/readers/cyme/utils.py index 2c6873f..83c7c7f 100644 --- a/src/ditto/readers/cyme/utils.py +++ b/src/ditto/readers/cyme/utils.py @@ -8,7 +8,15 @@ from functools import partial -def read_cyme_data(cyme_file, cyme_section, index_col=None, node_feeder_map = None, network_voltage_map = None, node_substation_map = None, parse_feeders=False, parse_substation=False): +def read_cyme_data( + cyme_file, + cyme_section, + index_col=None, + node_feeder_map=None, + node_substation_map=None, + parse_feeders=False, + parse_substation=False, +): all_data = [] headers = None with open(cyme_file) as f: @@ -17,53 +25,47 @@ def read_cyme_data(cyme_file, cyme_section, index_col=None, node_feeder_map = No feeder_object_map = {} substation_id = None substation_object_map = {} - + for line in f: if line.startswith(f"[{cyme_section}]"): reading = True continue - if reading: - if line.startswith(f"FORMAT_{cyme_section.replace(' ','')}"): - line_header = line.split("=")[1].strip() - headers = line_header.split(",") - continue - elif line.startswith("FORMAT") or line.startswith("FEEDER") or line.startswith("SUBSTATION"): - if parse_substation: - if line.startswith("SUBSTATION"): - substation_id = line.split(",")[0].split("=")[1].strip() - else: - substation_id = None - if parse_feeders: - if line.startswith("FEEDER"): - feeder_id = line.split(",")[0].split("=")[1].strip() - else: - feeder_id = None - # For SECTION Feeder headers - continue - elif line.strip() == "": - reading = False - break - else: - try: - line = line.strip() - line_data = line.split(",") - if cyme_section == 'SECTION': - node1 = line_data[1].strip() - node2 = line_data[3].strip() - if parse_feeders and (feeder_id is not None): - feeder = feeder_object_map.get(feeder_id, DistributionFeeder(name = feeder_id)) - node_feeder_map[node1] = feeder - node_feeder_map[node2] = feeder - feeder_object_map[feeder_id] = feeder - if parse_substation and (substation_id is not None): - substation = substation_object_map.get(substation_id, DistributionSubstation(name = substation_id, feeders = [])) - node_substation_map[node1] = substation - node_substation_map[node2] = substation - substation_object_map[substation_id] = substation - - all_data.append(line.split(",")) - except: - raise Exception(f"Failed to parse line: {line}") + if not reading: + continue + if line.strip() == "": + break + + if line.startswith(f"FORMAT_{cyme_section.replace(' ','')}"): + headers = line.split("=")[1].strip().split(",") + continue + elif ( + line.startswith("FORMAT") + or line.startswith("FEEDER") + or line.startswith("SUBSTATION") + ): + feeder_id, substation_id = _parse_context_line( + line, parse_feeders, parse_substation + ) + continue + else: + try: + line = line.strip() + line_data = line.split(",") + if cyme_section == "SECTION": + _track_section_nodes( + line_data, + feeder_id, + substation_id, + parse_feeders, + parse_substation, + node_feeder_map, + feeder_object_map, + node_substation_map, + substation_object_map, + ) + all_data.append(line_data) + except Exception as e: + raise Exception(f"Failed to parse line: {line}. Error: {e}") data = pd.DataFrame(all_data, columns=headers) if index_col is not None: @@ -71,13 +73,53 @@ def read_cyme_data(cyme_file, cyme_section, index_col=None, node_feeder_map = No return data -def network_truncation(system ,substation_names=None, feeder_names=None): +def _parse_context_line(line, parse_feeders, parse_substation): + """Extract feeder_id / substation_id from FEEDER= or SUBSTATION= lines.""" + feeder_id = None + substation_id = None + if parse_substation and line.startswith("SUBSTATION"): + substation_id = line.split(",")[0].split("=")[1].strip() + if parse_feeders and line.startswith("FEEDER"): + feeder_id = line.split(",")[0].split("=")[1].strip() + return feeder_id, substation_id + + +def _track_section_nodes( + line_data, + feeder_id, + substation_id, + parse_feeders, + parse_substation, + node_feeder_map, + feeder_object_map, + node_substation_map, + substation_object_map, +): + """Map SECTION nodes to their feeder/substation objects.""" + node1 = line_data[1].strip() + node2 = line_data[3].strip() + if parse_feeders and (feeder_id is not None): + feeder = feeder_object_map.get(feeder_id, DistributionFeeder(name=feeder_id)) + node_feeder_map[node1] = feeder + node_feeder_map[node2] = feeder + feeder_object_map[feeder_id] = feeder + if parse_substation and (substation_id is not None): + substation = substation_object_map.get( + substation_id, + DistributionSubstation(name=substation_id, feeders=[]), + ) + node_substation_map[node1] = substation + node_substation_map[node2] = substation + substation_object_map[substation_id] = substation + + +def network_truncation(system, substation_names=None, feeder_names=None): trunc_dist_sys = DistributionSystem(auto_add_composed_components=True) - buses = list(system.get_components(DistributionBus, filter_func=partial(filter_substation, substation_names=substation_names))) - buses.extend(list(system.get_components(DistributionBus, filter_func=partial(filter_feeder, feeder_names=feeder_names)))) - bus_set = set() - for bus in buses: - bus_set.add(bus.name) + + bus_set = _collect_connected_buses( + system, substation_names=substation_names, feeder_names=feeder_names + ) + print(f"Truncating to {len(bus_set)} buses") types = list(system.get_component_types()) for component_type in types: @@ -85,20 +127,14 @@ def network_truncation(system ,substation_names=None, feeder_names=None): length = len(components) print(f"Truncating components of type {component_type.__name__}, total: {length}") for i, comp in enumerate(components): - print(f"Truncating component {i+1} of {length}", end='\r', flush=True) + print(f"Truncating component {i+1} of {length}", end="\r", flush=True) if hasattr(comp, "bus"): if comp.bus.name in bus_set: - try: - trunc_dist_sys.add_component(comp) - except ISAlreadyAttached: - pass + _safe_add_component(trunc_dist_sys, comp) elif hasattr(comp, "buses"): for bus in comp.buses: if bus.name in bus_set: - try: - trunc_dist_sys.add_component(comp) - except ISAlreadyAttached: - pass + _safe_add_component(trunc_dist_sys, comp) break else: break @@ -106,16 +142,43 @@ def network_truncation(system ,substation_names=None, feeder_names=None): return trunc_dist_sys +def _collect_connected_buses(system, substation_names=None, feeder_names=None): + buses = list( + system.get_components( + DistributionBus, + filter_func=partial(filter_substation, substation_names=substation_names), + ) + ) + buses.extend( + list( + system.get_components( + DistributionBus, filter_func=partial(filter_feeder, feeder_names=feeder_names) + ) + ) + ) + bus_set = set(bus.name for bus in buses) + + return bus_set + + +def _safe_add_component(trunc_dist_sys, comp): + try: + trunc_dist_sys.add_component(comp) + except ISAlreadyAttached: + pass + + def filter_feeder(object, feeder_names=None): if not hasattr(object.feeder, "name"): return False if object.feeder.name in feeder_names: return True - return False + return False + def filter_substation(object, substation_names=None): if not hasattr(object.substation, "name"): return False if object.substation.name in substation_names: return True - return False \ No newline at end of file + return False diff --git a/tests/test_cyme/test_cyme_reader.py b/tests/test_cyme/test_cyme_reader.py index ba78454..66feb83 100644 --- a/tests/test_cyme/test_cyme_reader.py +++ b/tests/test_cyme/test_cyme_reader.py @@ -1,6 +1,5 @@ """ Module for testing parsers.""" from pathlib import Path -import os import pytest from ditto.readers.cyme.reader import Reader from ditto.writers.opendss.write import Writer @@ -27,21 +26,25 @@ if target_files.issubset(files_in_folder): matching_folders.append(folder) + @pytest.mark.parametrize("cyme_folder", matching_folders) def test_cyme_reader(cyme_folder: Path, tmp_path): - export_path = base_path / "dump_from_tests" / "cyme" / cyme_folder.name if not export_path.exists(): export_path.mkdir(parents=True, exist_ok=True) - reader = Reader(cyme_folder / cyme_network_name, cyme_folder / cyme_equipment_name, cyme_folder / cyme_load_name, '1') + load_model_id = "1" + reader = Reader( + cyme_folder / cyme_network_name, + cyme_folder / cyme_equipment_name, + cyme_folder / cyme_load_name, + load_model_id, + ) writer = Writer(reader.get_system()) writer.write(export_path / "opendss", separate_substations=False, separate_feeders=False) system = reader.get_system() json_path = (export_path / cyme_folder.stem.lower()).with_suffix(".json") system.to_json(json_path, overwrite=True, indent=4) system.to_geojson(export_path / (cyme_folder.stem.lower() + ".geojson")) - - assert json_path.exists(), "Failed to export the json file"