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/enumerations.py b/src/ditto/enumerations.py index 43446c3..c8e47e2 100644 --- a/src/ditto/enumerations.py +++ b/src/ditto/enumerations.py @@ -6,12 +6,15 @@ 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" LINE_GEOMETRIES_FILE = "LineGeometry.dss" SWITCH_CODES_FILE = "SwitchCodes.dss" 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/readers/cyme/__init__.py b/src/ditto/readers/cyme/__init__.py new file mode 100644 index 0000000..0d4db49 --- /dev/null +++ b/src/ditto/readers/cyme/__init__.py @@ -0,0 +1,46 @@ +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.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.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.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, +) +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 new file mode 100644 index 0000000..924de32 --- /dev/null +++ b/src/ditto/readers/cyme/components/distribution_bus.py @@ -0,0 +1,83 @@ +from infrasys.location import Location +from gdm.distribution.components.distribution_bus import DistributionBus +from gdm.distribution.enums import VoltageTypes, Phase +from gdm.quantities import Voltage +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" + + 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, + ) + + def map_name(self, row): + name = row["NodeID"] + return name + + def map_coordinate(self, row): + 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): + # 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"] + 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) + + 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 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 new file mode 100644 index 0000000..92f5d7b --- /dev/null +++ b/src/ditto/readers/cyme/components/distribution_capacitor.py @@ -0,0 +1,89 @@ +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.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" + + 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, section_id_sections) + 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, + ) + + def map_name(self, row): + return row["DeviceNumber"] + + def map_phases(self, row, section_id_sections): + phases = [] + 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 "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: + 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): + 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 + + 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 + return from_bus + + def map_controllers(self, row): + return [] + + def map_equipment(self, row, equipment_data): + mapper = CapacitorEquipmentMapper(self.system) + capacitor_id = row["ShuntCapacitorID"] + if capacitor_id not in equipment_data.index: + 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"]) + 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/components/distribution_load.py b/src/ditto/readers/cyme/components/distribution_load.py new file mode 100644 index 0000000..250997f --- /dev/null +++ b/src/ditto/readers/cyme/components/distribution_load.py @@ -0,0 +1,90 @@ +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.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" + + 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: + # 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 + return None + + if len(phases) == 0: + 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_record[name] = load + return load + + def map_name(self, row): + 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 + + 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 + return from_bus + + def map_phases(self, row): + phases = [] + 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, equipment_file): + mapper = LoadEquipmentMapper(self.system) + 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 new file mode 100644 index 0000000..dc08016 --- /dev/null +++ b/src/ditto/readers/cyme/components/distribution_transformer.py @@ -0,0 +1,247 @@ +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.enums import Phase + + +class DistributionTransformerMapper(CymeMapper): + def __init__(self, system): + super().__init__(system) + + cyme_file = "Network" + cyme_section = "TRANSFORMER SETTING" + + def parse(self, row, used_sections, section_id_sections, equipment_data): + 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, 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 + ) + 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"] + + 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, phase): + section_id = str(row["SectionID"]) + section = section_id_sections[section_id] + + 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 = [] + # 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: + 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, phase): + mapper = DistributionTransformerEquipmentMapper(self.system) + if equipment_row is not None: + equipment = mapper.parse(equipment_row, row, phase) + if equipment is not None: + return equipment + return None + + +class DistributionTransformerByPhaseMapper(CymeMapper): + def __init__(self, system): + super().__init__(system) + + 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] == "" + ): + continue + 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 + ) + ) + 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 = [] + + # 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 + 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, equipment_row): + mapper = DistributionTransformerEquipmentMapper(self.system) + if equipment_row is not None: + 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" + + 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: + return equipment + return None 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..8755a7d --- /dev/null +++ b/src/ditto/readers/cyme/components/distribution_voltage_source.py @@ -0,0 +1,61 @@ +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 = 'SOURCE' + + def parse(self, row): + name = self.map_name(row) + bus = self.map_bus(row) + feeder = bus.feeder + substation = bus.substation + 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 + + phases = [phs for phs in bus.phases] + equipment = self.map_equipment(bus, voltage) + + return DistributionVoltageSource.model_construct(name=name, + feeder=feeder, + substation=substation, + 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, voltage): + mapper = PhaseVoltageSourceEquipmentMapper(self.system) + sources = mapper.parse(bus, voltage) + return VoltageSourceEquipment.model_construct( + name=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 new file mode 100644 index 0000000..b8a3b49 --- /dev/null +++ b/src/ditto/readers/cyme/components/geometry_branch.py @@ -0,0 +1,77 @@ +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", "OVERHEAD BYPHASE SETTING"] + + 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, cyme_section) + 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"] + 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") + 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 = 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 + else: + return phases + + def map_equipment(self, row, cyme_section): + line_id = ( + row["LineCableID"] if cyme_section == "OVERHEADLINE SETTING" else row["DeviceNumber"] + ) + + 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 new file mode 100644 index 0000000..7070945 --- /dev/null +++ b/src/ditto/readers/cyme/components/matrix_impedance_branch.py @@ -0,0 +1,111 @@ +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.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"] + + 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: + return None + 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 + ) + 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, row, cyme_section): + if cyme_section == "UNDERGROUNDLINE SETTING": + length = Distance(float(row["Length"]), "foot").to("km") + else: + length = DEFAULT_BRANCH_LENGTH + + 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, cyme_section): + 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 = DEFAULT_R_MATRIX + r_matrix = ResistancePULength( + [row[: len(phases)] for row in r[: len(phases)]], + "ohm/mi", + ) + x = DEFAULT_X_MATRIX + x_matrix = ReactancePULength( + [row[: len(phases)] for row in x[: len(phases)]], + "ohm/mi", + ) + c = DEFAULT_C_MATRIX + c_matrix = CapacitancePULength( + [row[: len(phases)] for row in c[: len(phases)]], + "nanofarad/mi", + ) + 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 new file mode 100644 index 0000000..19d5b83 --- /dev/null +++ b/src/ditto/readers/cyme/components/matrix_impedance_fuse.py @@ -0,0 +1,79 @@ +from ditto.readers.cyme.cyme_mapper import CymeMapper +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.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" + + 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() + 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, + 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): + length = DEFAULT_BRANCH_LENGTH + 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["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 new file mode 100644 index 0000000..586b274 --- /dev/null +++ b/src/ditto/readers/cyme/components/matrix_impedance_recloser.py @@ -0,0 +1,90 @@ +from ditto.readers.cyme.cyme_mapper import CymeMapper +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.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" + + 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() + 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) + + used_sections.add(name) + + 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): + length = DEFAULT_BRANCH_LENGTH + 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["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 + ) + return recloser 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..3e906aa --- /dev/null +++ b/src/ditto/readers/cyme/components/matrix_impedance_switch.py @@ -0,0 +1,80 @@ +from ditto.readers.cyme.cyme_mapper import CymeMapper +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.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" + + 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) + used_sections.add(name) + 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 = DEFAULT_BRANCH_LENGTH + 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["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 + ) + 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/cyme_mapper.py b/src/ditto/readers/cyme/cyme_mapper.py new file mode 100644 index 0000000..882efb3 --- /dev/null +++ b/src/ditto/readers/cyme/cyme_mapper.py @@ -0,0 +1,6 @@ +from abc import ABC + +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..7cb49f5 --- /dev/null +++ b/src/ditto/readers/cyme/equipment/capacitor_equipment.py @@ -0,0 +1,74 @@ +from ditto.readers.cyme.cyme_mapper import CymeMapper +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 +from gdm.quantities import Voltage + + +class CapacitorEquipmentMapper(CymeMapper): + def __init__(self, system): + super().__init__(system) + + 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, + ) + + 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 + return VoltageTypes.LINE_TO_LINE + + +class PhaseCapacitorEquipmentMapper(CymeMapper): + def __init__(self, system, num_banks_on): + super().__init__(system) + self.num_banks_on = num_banks_on + + 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 + ) + + 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_reactive_power(self, row): + 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 new file mode 100644 index 0000000..5067e22 --- /dev/null +++ b/src/ditto/readers/cyme/equipment/distribution_transformer_equipment.py @@ -0,0 +1,337 @@ +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, 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" + + 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, 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, + ) + + 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"]) * 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): + 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) + if is_center_tapped: + winding_reactances = [reactance_pu * 100, reactance_pu * 100, reactance_pu * 100] + else: + winding_reactances = [reactance_pu * 100] + return winding_reactances + + def map_is_center_tapped(self, row): + # 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 + + return False + + 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, phase=phase) + winding_mapper2 = WindingEquipmentMapper(self.system) + 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, 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)] + else: + coupling = [SequencePair(0, 1)] + return coupling + + +class 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", + } + """ + + # The documentation is confusing on where the below connection types are used + # 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", + 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(phase) + connection_type = self.map_connection_type(row, winding_number) + 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, + ) + + def map_name(self, row): + 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) + return resistance_pu * 100 + + 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 + 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: + grounded = False + elif "YO" in winding_type: + grounded = False + else: + grounded = True + return 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 = 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"): + return VoltageTypes.LINE_TO_GROUND + 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, phase): + num_phases = len(phase) + return num_phases + + def map_connection_type(self, row, winding_number): + connection_type = row["Conn"] + if winding_number == 1: + winding = 0 + elif winding_number == 2 or winding_number == 3: + winding = 1 + + 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) + + 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" + else: + connection_type = "STAR" + + return ConnectionType(connection_type) + + 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 = 1.0 + else: + tap = network_row.get("PrimTap", None) + if tap is None: + tap = network_row.get("PrimaryTapSettingA", 100) + tap = float(tap) / 100 + + elif winding_number == 2 or 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", 100) + tap = float(tap) / 100 + + if row["Taps"] == "" or row["Taps"] is None: + tap = 1.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 = 32 + 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: + 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["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/distribution_transformer_three_winding_equipment.py b/src/ditto/readers/cyme/equipment/distribution_transformer_three_winding_equipment.py new file mode 100644 index 0000000..3ef510d --- /dev/null +++ b/src/ditto/readers/cyme/equipment/distribution_transformer_three_winding_equipment.py @@ -0,0 +1,348 @@ +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, 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 [1.0 for _ in range(num_phases)] + if int(tap_location) != winding_number: + return [1.0 for _ in range(num_phases)] + + if network_row is None: + tap = 1.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 = 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: + 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/geometry_branch_equipment.py b/src/ditto/readers/cyme/equipment/geometry_branch_equipment.py new file mode 100644 index 0000000..1cbf774 --- /dev/null +++ b/src/ditto/readers/cyme/equipment/geometry_branch_equipment.py @@ -0,0 +1,324 @@ +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 + + +class GeometryBranchEquipmentMapper(CymeMapper): + def __init__(self, system): + super().__init__(system) + + cyme_file = "Equipment" + cyme_section = "LINE" + + 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.model_construct( + 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, spacing_ids): + 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"] + 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"] + 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, 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 + ) + except Exception as e: + 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 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"] + 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 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.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"] + return name + + def map_conductor_diameter(self, row): + 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") + return conductor_gmr + + def map_ampacity(self, row): + ampacity = Current(float(row["Amps"]), "amp") + if ampacity == 0.0: + ampacity = DEFAULT_BRANCH_AMPACITY + return ampacity + + def map_emergency_ampacity(self, row): + emergency_ampacity = Current(float(row["Amps_4"]), "amp") + if emergency_ampacity == 0.0: + 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") + if ac_resistance == 0.0: + 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") + if dc_resistance == 0.0: + 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" + + 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.model_construct( + name=name, + conductors=conductors, + horizontal_positions=horizontal_positions, + vertical_positions=vertical_positions, + ) + + def map_name(self, row): + 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"] + else: + 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"] + if cond1_y != "" and phase_A_conductor_name != "NONE": + y1 = float(cond1_y) + vertical_positions.append(y1) + if cond2_y != "" and phase_B_conductor_name != "NONE": + y2 = float(cond2_y) + vertical_positions.append(y2) + if cond3_y != "" and phase_C_conductor_name != "NONE": + y3 = float(cond3_y) + vertical_positions.append(y3) + 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"] + 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: + 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 != "" and phase_A_conductor_name != "NONE": + x1 = float(cond1_x) + horizontal_positions.append(x1) + if cond2_x != "" and phase_B_conductor_name != "NONE": + x2 = float(cond2_x) + horizontal_positions.append(x2) + if cond3_x != "" and phase_C_conductor_name != "NONE": + x3 = float(cond3_x) + horizontal_positions.append(x3) + 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 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"] if "CondID_N1" in row else row["CondID_N"] + + 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 + ) + if phase_B_conductor_name != "NONE": + 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 + ) + if neutral_conductor_name != "NONE": + 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"] + if cond1 != "" and phase_A_conductor is not None: + conductors.append(phase_A_conductor) + if cond2 != "" and phase_B_conductor is not None: + conductors.append(phase_B_conductor) + if cond3 != "" and phase_C_conductor is not None: + conductors.append(phase_C_conductor) + if neutral != "" and neutral_conductor is not None: + conductors.append(neutral_conductor) + + return conductors + return None 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..2688604 --- /dev/null +++ b/src/ditto/readers/cyme/equipment/load_equipment.py @@ -0,0 +1,138 @@ +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" + + 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(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 + ) + + def map_name(self, row): + return row["DeviceNumber"] + + 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: 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] + + def map_phase_loads(self, row): + # Get the PhaseLoadEquipment with the same name as the 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): + def __init__(self, system): + super().__init__(system) + + 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: + 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) + 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 = row["LoadPhase"] + return row["DeviceNumber"] + "_" + str(phase) + + 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: + 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 + else: + pass + return ActivePower(kw, "kilowatt"), ReactivePower(kvar, "kilovar") + + 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 + + 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/equipment/matrix_impedance_branch_equipment.py b/src/ditto/readers/cyme/equipment/matrix_impedance_branch_equipment.py new file mode 100644 index 0000000..3a308a8 --- /dev/null +++ b/src/ditto/readers/cyme/equipment/matrix_impedance_branch_equipment.py @@ -0,0 +1,84 @@ +from gdm.quantities import ResistancePULength +from ditto.readers.cyme.cyme_mapper import CymeMapper +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" + + 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): + 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, + 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) + 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): + 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), "microfarad/mile") + return capacitance_matrix + + def map_ampacity(self, row): + 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 new file mode 100644 index 0000000..1323772 --- /dev/null +++ b/src/ditto/readers/cyme/equipment/matrix_impedance_fuse_equipment.py @@ -0,0 +1,73 @@ +from ditto.readers.cyme.cyme_mapper import CymeMapper +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" + + def parse(self, row, phases): + 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) + 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, + construction=LineType.OVERHEAD, + r_matrix=r_matrix, + x_matrix=x_matrix, + c_matrix=c_matrix, + ampacity=ampacity, + ) + + def map_name(self, row, phases): + return f"{row['ID']}_{len(phases)}" + + def map_r_matrix(self, 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 = DEFAULT_X_MATRIX + matrix = [row[: len(phases)] for row in default_matrix[: len(phases)]] + return ReactancePULength( + matrix, + "ohm/mi", + ) + + def map_c_matrix(self, phases): + default_matrix = DEFAULT_C_MATRIX + 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..9291aab --- /dev/null +++ b/src/ditto/readers/cyme/equipment/matrix_impedance_recloser_equipment.py @@ -0,0 +1,62 @@ +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.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" + + def parse(self, row, phases): + 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) + ampacity = self.map_ampacity(row) + + return MatrixImpedanceRecloserEquipment( + name=name, + construction=LineType.OVERHEAD, + r_matrix=r_matrix, + x_matrix=x_matrix, + c_matrix=c_matrix, + ampacity=ampacity, + ) + + def map_name(self, row, phases): + return f"{row['ID']}_{len(phases)}" + + def map_r_matrix(self, 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 = DEFAULT_X_MATRIX + matrix = [row[: len(phases)] for row in default_matrix[: len(phases)]] + return ReactancePULength( + matrix, + "ohm/mi", + ) + + def map_c_matrix(self, phases): + default_matrix = DEFAULT_C_MATRIX + 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/equipment/matrix_impedance_switch_equipment.py b/src/ditto/readers/cyme/equipment/matrix_impedance_switch_equipment.py new file mode 100644 index 0000000..15d77a7 --- /dev/null +++ b/src/ditto/readers/cyme/equipment/matrix_impedance_switch_equipment.py @@ -0,0 +1,63 @@ +from ditto.readers.cyme.cyme_mapper import CymeMapper +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" + + def parse(self, row, phases): + 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) + ampacity = self.map_ampacity(row) + + return MatrixImpedanceSwitchEquipment( + name=name, + construction=LineType.OVERHEAD, + r_matrix=r_matrix, + x_matrix=x_matrix, + c_matrix=c_matrix, + ampacity=ampacity, + ) + + def map_name(self, row, phases): + return f"{row['ID']}_{len(phases)}" + + def map_r_matrix(self, 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 = DEFAULT_X_MATRIX + matrix = [row[: len(phases)] for row in default_matrix[: len(phases)]] + return ReactancePULength( + matrix, + "ohm/mi", + ) + + def map_c_matrix(self, phases): + default_matrix = DEFAULT_C_MATRIX + 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/equipment/phase_voltagesource_equipment.py b/src/ditto/readers/cyme/equipment/phase_voltagesource_equipment.py new file mode 100644 index 0000000..d89a42a --- /dev/null +++ b/src/ditto/readers/cyme/equipment/phase_voltagesource_equipment.py @@ -0,0 +1,27 @@ +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 +from gdm.quantities import Voltage + + +class PhaseVoltageSourceEquipmentMapper(CymeMapper): + def __init__(self, system): + super().__init__(system) + + def parse(self, bus, source_voltage): + sources = [] + num_phases = len(bus.phases) + for i in range(num_phases): + source = PhaseVoltageSourceEquipment.model_construct( + 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=Voltage(source_voltage, 'kilovolt'), + 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 new file mode 100644 index 0000000..b553e08 --- /dev/null +++ b/src/ditto/readers/cyme/reader.py @@ -0,0 +1,365 @@ +from gdm.distribution.distribution_system import DistributionSystem +from ditto.readers.reader import AbstractReader +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 collections import defaultdict + + +from gdm.distribution.components.distribution_bus import DistributionBus +from gdm.distribution.components import DistributionVoltageSource + + +class Reader(AbstractReader): + # Order of components is important + component_types = [ + "DistributionBus", # First as other components connect to buses + "DistributionVoltageSource", + "MatrixImpedanceRecloserEquipment", + "MatrixImpedanceRecloser", + "MatrixImpedanceSwitchEquipment", + "MatrixImpedanceSwitch", + "MatrixImpedanceFuseEquipment", + "MatrixImpedanceFuse", + "DistributionCapacitor", + "DistributionLoad", + "BareConductorEquipment", + "MatrixImpedanceBranchEquipment", + "GeometryBranchEquipment", + "GeometryBranchByPhaseEquipment", + "GeometryBranch", + "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, + 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, + ): + # 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", + ] + ) + + node_feeder_map = {} + node_substation_map = {} + load_record = {} + used_sections = set() + + section_data = read_cyme_data( + network_file, + "SECTION", + node_feeder_map=node_feeder_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() + ) + + for component_type in self.component_types: + 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.") + continue + + mapper = getattr(cyme_mapper, mapper_name)(self.system) + + 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] + + for cyme_section in all_cyme_sections: + data = self._prepare_data( + cyme_file, cyme_section, load_model_id, network_file, equipment_file, load_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, + ], + "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, + cyme_section, + ], + "BareConductorEquipmentMapper": lambda: [], + "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" + ), + ], + "MatrixImpedanceBranchEquipmentMapper": lambda: [], + "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 + + 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] + 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) + + self.assign_bus_voltages() + + 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) + 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"], + ] + ) + + 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 occurred 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): + observed_buses = set() + observed_components = set() + + bus_obj_map = self._create_bus_obj_map() + + bus_queue = self._start_queue_w_voltage_sources() + + while bus_queue: + current_bus_name = bus_queue.pop() + + current_bus = self.system.get_component(DistributionBus, name=current_bus_name) + current_voltage = current_bus.rated_voltage + current_voltage_type = current_bus.voltage_type + 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 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) + + 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.sources[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 diff --git a/src/ditto/readers/cyme/utils.py b/src/ditto/readers/cyme/utils.py new file mode 100644 index 0000000..83c7c7f --- /dev/null +++ b/src/ditto/readers/cyme/utils.py @@ -0,0 +1,184 @@ +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, + 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 + continue + 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: + data.set_index(index_col, inplace=True, drop=False) + return data + + +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) + + 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: + 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: + _safe_add_component(trunc_dist_sys, comp) + elif hasattr(comp, "buses"): + for bus in comp.buses: + if bus.name in bus_set: + _safe_add_component(trunc_dist_sys, comp) + break + else: + break + + 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 + + +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 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 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, diff --git a/src/ditto/writers/opendss/__init__.py b/src/ditto/writers/opendss/__init__.py index 6f3a798..e24f784 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,7 +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.components.distribution_capacitor import DistributionCapacitorMapper from ditto.writers.opendss.components.distribution_load import DistributionLoadMapper from ditto.writers.opendss.components.distribution_transformer import DistributionTransformerMapper @@ -28,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/distribution_branch.py b/src/ditto/writers/opendss/components/distribution_branch.py index da80012..311abc7 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 @@ -13,18 +13,21 @@ 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(" ","_").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(" ","_").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] 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/distribution_bus.py b/src/ditto/writers/opendss/components/distribution_bus.py index 2107128..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 + 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 d848463..0a196b3 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 @@ -13,15 +13,15 @@ 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(" ", "_").replace(".", "_") def map_bus(self): - self.opendss_dict["Bus1"] = self.model.bus.name + 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] # 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 @@ -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" diff --git a/src/ditto/writers/opendss/components/distribution_load.py b/src/ditto/writers/opendss/components/distribution_load.py index 465e3b0..a954b08 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 @@ -15,16 +15,16 @@ 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(" ","_").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(" ","_").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? - 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): diff --git a/src/ditto/writers/opendss/components/distribution_transformer.py b/src/ditto/writers/opendss/components/distribution_transformer.py index a0c14c9..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 + 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) + 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) + 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 + 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 599775f..c9059dd 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(" ", "_").replace(".", "_") def map_bus(self): - self.opendss_dict["Bus1"] = self.model.bus.name + self.opendss_dict["Bus1"] = self.model.bus.name.replace(" ","_").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/components/geometry_branch.py b/src/ditto/writers/opendss/components/geometry_branch.py index 812ac09..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 + 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 8413576..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 + 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 e8fb87a..bb2d03d 100644 --- a/src/ditto/writers/opendss/components/matrix_impedance_fuse.py +++ b/src/ditto/writers/opendss/components/matrix_impedance_fuse.py @@ -10,8 +10,13 @@ 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 + 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 new file mode 100644 index 0000000..0e211c8 --- /dev/null +++ b/src/ditto/writers/opendss/components/matrix_impedance_recloser.py @@ -0,0 +1,23 @@ +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_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(".", "_") + + 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/components/matrix_impedance_switch.py b/src/ditto/writers/opendss/components/matrix_impedance_switch.py index ea37181..4714502 100644 --- a/src/ditto/writers/opendss/components/matrix_impedance_switch.py +++ b/src/ditto/writers/opendss/components/matrix_impedance_switch.py @@ -10,8 +10,13 @@ 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 + 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 a895903..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 + 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 d73c855..ce87a66 100644 --- a/src/ditto/writers/opendss/equipment/bare_conductor_equipment.py +++ b/src/ditto/writers/opendss/equipment/bare_conductor_equipment.py @@ -11,17 +11,23 @@ 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(" ","_").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 new file mode 100644 index 0000000..3e70e6d --- /dev/null +++ b/src/ditto/writers/opendss/equipment/concentric_cable_equipment.py @@ -0,0 +1,76 @@ +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.replace(" ","_").replace(".","_") + + def map_strand_diameter(self): + self.opendss_dict["DiaStrand"] = self.model.strand_diameter.magnitude + + def map_conductor_diameter(self): + 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_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): + 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") + 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/equipment/distribution_transformer_equipment.py b/src/ditto/writers/opendss/equipment/distribution_transformer_equipment.py index 623d7d1..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 + 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 @@ -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) diff --git a/src/ditto/writers/opendss/equipment/geometry_branch_equipment.py b/src/ditto/writers/opendss/equipment/geometry_branch_equipment.py index de90a5f..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 + 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}") + 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 33f13da..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 + 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/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 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..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 + self.opendss_dict["Name"] = self.model.name.replace(" ", "_").replace(".", "_") def map_common(self): self.opendss_dict["Units"] = "km" 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/src/ditto/writers/opendss/write.py b/src/ditto/writers/opendss/write.py index f9ac130..55c235d 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 @@ -23,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) @@ -43,7 +45,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 @@ -52,6 +54,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) @@ -60,6 +63,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) @@ -75,9 +79,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) @@ -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("") diff --git a/tests/test_cyme/test_cyme_reader.py b/tests/test_cyme/test_cyme_reader.py new file mode 100644 index 0000000..66feb83 --- /dev/null +++ b/tests/test_cyme/test_cyme_reader.py @@ -0,0 +1,50 @@ +""" Module for testing parsers.""" +from pathlib import Path +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" +cyme_load_name = "Load.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) + + 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" 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")