Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion src/ditto/readers/synergi/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
from ditto.readers.synergi.components.distribution_feeder import DistributionFeederMapper
from ditto.readers.synergi.components.distribution_vsource import DistributionVoltageSourceMapper
from ditto.readers.synergi.components.distribution_substation import DistributionSubstationMapper
from ditto.readers.synergi.components.distribution_bus import DistributionBusMapper
from ditto.readers.synergi.components.distribution_capacitor import DistributionCapacitorMapper
from ditto.readers.synergi.components.distribution_load import DistributionLoadMapper
from ditto.readers.synergi.components.distribution_transformer import DistributionTransformerMapper
from ditto.readers.synergi.equipment.distribution_transformer_equipment import DistributionTransformerEquipmentMapper
from ditto.readers.synergi.equipment.conductor_equipment import ConductorEquipmentMapper
from ditto.readers.synergi.equipment.geometry_branch_equipment import GeometryBranchEquipmentMapper
from ditto.readers.synergi.components.geometry_branch import GeometryBranchMapper
from ditto.readers.synergi.equipment.matrix_impedance_branch_equipment import MatrixImpedanceBranchEquipmentMapper
from ditto.readers.synergi.components.line_section import LineSectionMapper
from ditto.readers.synergi.components.matrix_impedance_switch import SwitchMapper, BreakerMapper
from ditto.readers.synergi.components.matrix_impedance_fuse import FuseMapper
from ditto.readers.synergi.components.matrix_impedance_recloser import RecloserMapper
from ditto.readers.synergi.components.solar import DistributionSolarMapper
from ditto.readers.synergi.components.generator import GeneratorMapper
from ditto.readers.synergi.components.battery import DistributionBatteryMapper
from ditto.readers.synergi.components.regulator import DistributionRegulatorMapper
from ditto.readers.synergi.equipment.generator_equipment import GeneratorEquipmentMapper
137 changes: 137 additions & 0 deletions src/ditto/readers/synergi/components/battery.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
from gdm.distribution.components.distribution_battery import DistributionBattery
from gdm.distribution.components.distribution_bus import DistributionBus
from gdm.distribution.enums import Phase, VoltageTypes
from gdm.distribution.equipment.battery_equipment import BatteryEquipment
from gdm.distribution.equipment.inverter_equipment import InverterEquipment
from gdm.quantities import ApparentPower, ReactivePower, EnergyDC
from infrasys.quantities import ActivePower, Voltage
from ditto.readers.synergi.synergi_mapper import SynergiMapper
from ditto.readers.synergi.utils import parse_phases, phases_without_neutral, sanitize_name, safe_float
from loguru import logger

_BATTERY_KW = {"battery", "batt"}


def _is_battery(gen_type: str, gen_equipment: dict) -> bool:
lower = gen_type.lower()
if any(kw in lower for kw in _BATTERY_KW):
return True
dev = gen_equipment.get(gen_type, {})
return dev.get("GeneratorType", "").lower() == "battery"


class DistributionBatteryMapper(SynergiMapper):
"""Maps InstGenerators rows classified as batteries to DistributionBattery components."""

synergi_table = "InstGenerators"
synergi_database = "Model"

def parse(self, row, unit_type, section_id_sections, from_node_sections, to_node_sections, gen_equipment=None):
gen_equipment = gen_equipment or {}
gen_type = str(row.get("GeneratorType", "")).strip()
if not _is_battery(gen_type, gen_equipment):
return None

section_id = str(row["SectionId"]).strip()
section = section_id_sections.get(section_id)
if section is None:
return None

bus = self.map_bus(row, section_id_sections)
if bus is None:
return None

phases = self.map_phases(row, bus)
if not phases:
return None

dev = gen_equipment.get(gen_type, {})
kw_rating = self.map_kw_rating(dev)
kv_rating = self.map_kv_rating(dev)
batt_kwhr = self.map_energy(row, dev)
eff_dis = self.map_eff_discharge(dev)
eff_chg = self.map_eff_charge(dev)

to_id = str(section.get("ToNodeId", "")).strip()
feeder, substation = self._lookup_feeder_substation(to_id)

return DistributionBattery(
name=self.map_name(row),
bus=bus,
phases=phases,
equipment=self.map_equipment(row, kw_rating, kv_rating, batt_kwhr, eff_dis, eff_chg),
inverter=self.map_inverter(row, kw_rating, eff_dis),
active_power=self.map_active_power(row, kw_rating),
reactive_power=ReactivePower(0, "kilovar"),
controller=None,
substation=substation,
feeder=feeder,
)

def map_name(self, row):
device_id = str(row.get("UniqueDeviceId", row["SectionId"])).strip()
return f"batt_{sanitize_name(device_id)}"

def map_bus(self, row, section_id_sections):
section_id = str(row["SectionId"]).strip()
section = section_id_sections.get(section_id, {})
to_id = sanitize_name(str(section.get("ToNodeId", "")).strip())
try:
return self.system.get_component(DistributionBus, to_id)
except Exception:
device_id = str(row.get("UniqueDeviceId", section_id)).strip()
logger.warning(f"Battery {device_id}: bus {to_id} not found")
return None

def map_phases(self, row, bus):
phases = phases_without_neutral(parse_phases(str(row.get("ConnectedPhases", "ABC"))))
bus_phases = {p for p in bus.phases if p != Phase.N}
return [p for p in phases if p in bus_phases]

def map_kw_rating(self, dev):
return safe_float(dev.get("KwRating"), 1000) or 1000

def map_kv_rating(self, dev):
return safe_float(dev.get("KvRating"), 13.2) or 13.2

def map_energy(self, row, dev):
kwhr = safe_float(row.get("BattRatedKwHr"), 0) or 0
if kwhr <= 0:
kwhr = safe_float(dev.get("BattRatkWhr"), 4000) or 4000
return max(kwhr, 1)

def map_eff_discharge(self, dev):
return safe_float(dev.get("BattRatPctEffDis"), 84) or 84

def map_eff_charge(self, dev):
return safe_float(dev.get("BattRatPctEffChg"), 82) or 82

def map_equipment(self, row, kw_rating, kv_rating, batt_kwhr, eff_dis, eff_chg):
device_id = str(row.get("UniqueDeviceId", row["SectionId"])).strip()
return BatteryEquipment(
name=f"batt_equip_{sanitize_name(device_id)}",
rated_energy=EnergyDC(batt_kwhr, "kilowatthour"),
rated_power=ActivePower(kw_rating, "kilowatt"),
charging_efficiency=eff_chg,
discharging_efficiency=eff_dis,
idling_efficiency=99.0,
rated_voltage=Voltage(kv_rating, "kilovolt"),
voltage_type=VoltageTypes.LINE_TO_LINE,
)

def map_inverter(self, row, kw_rating, eff_dis):
device_id = str(row.get("UniqueDeviceId", row["SectionId"])).strip()
return InverterEquipment(
name=f"batt_inverter_{sanitize_name(device_id)}",
rated_apparent_power=ApparentPower(kw_rating, "kilova"),
rise_limit=None,
fall_limit=None,
dc_to_ac_efficiency=eff_dis,
cutin_percent=5.0,
cutout_percent=5.0,
eff_curve=None,
)

def map_active_power(self, row, kw_rating):
output_pct = safe_float(row.get("OutputPowerPercentage"), 100) or 100
return ActivePower(kw_rating * output_pct / 100.0, "kilowatt")
108 changes: 45 additions & 63 deletions src/ditto/readers/synergi/components/distribution_bus.py
Original file line number Diff line number Diff line change
@@ -1,76 +1,58 @@
import math
from infrasys.location import Location
from gdm.distribution.components.distribution_bus import DistributionBus
from gdm import VoltageTypes, Phase, PositiveVoltage
from gdm.distribution.components.distribution_feeder import DistributionFeeder
from gdm.distribution.components.distribution_substation import DistributionSubstation
from gdm.distribution.enums import VoltageTypes, Phase
from gdm.quantities import Voltage
from ditto.readers.synergi.synergi_mapper import SynergiMapper
from ditto.readers.synergi.utils import parse_phases, sort_phases, sanitize_name

class DistributionBusMapper(SynergiMapper):
def __init__(self, system):

super().__init__(system)
class DistributionBusMapper(SynergiMapper):

synergi_table = "Node"
synergi_database = "Model"

def parse(self, row, unit_type, section_id_sections, from_node_sections, to_node_sections):
name = self.map_name(row)
coordinate = self.map_coordinate(row)
nominal_voltage = self.map_nominal_voltage(row)
phases = self.map_phases(row, from_node_sections, to_node_sections)
voltage_limits = self.map_voltagelimits(row)
voltage_type = self.map_voltage_type(row)
return DistributionBus(name=name,
coordinate=coordinate,
nominal_voltage=nominal_voltage,
phases=phases,
voltagelimits=voltage_limits,
voltage_type=voltage_type)

def map_name(self, row):
name = row["NodeId"]
return name
node_id = str(row["NodeId"]).strip()
feeder_info = self.node_feeder_map.get(node_id, {})

feeder = None
substation = None
if feeder_info:
try:
feeder = self.system.get_component(DistributionFeeder, name=sanitize_name(feeder_info["feeder_id"]))
except Exception:
pass

Check notice on line 27 in src/ditto/readers/synergi/components/distribution_bus.py

View check run for this annotation

codefactor.io / CodeFactor

src/ditto/readers/synergi/components/distribution_bus.py#L26-L27

Try, Except, Pass detected. (B110)
try:
substation = self.system.get_component(DistributionSubstation, name=sanitize_name(feeder_info["sub_id"]))
except Exception:
pass

Check notice on line 31 in src/ditto/readers/synergi/components/distribution_bus.py

View check run for this annotation

codefactor.io / CodeFactor

src/ditto/readers/synergi/components/distribution_bus.py#L30-L31

Try, Except, Pass detected. (B110)

return DistributionBus(
name=sanitize_name(node_id),
coordinate=self.map_coordinate(row),
rated_voltage=self.map_nominal_voltage(feeder_info),
phases=self.map_phases(node_id, from_node_sections, to_node_sections),
voltagelimits=[],
voltage_type=VoltageTypes.LINE_TO_GROUND,
substation=substation,
feeder=feeder,
)

def map_coordinate(self, row):
X, Y = row["X"], row["Y"]
#crs = SAI_Control.ProjectionWKT
crs = None
location = Location(x = X, y = Y, crs = crs)
return location

# Nominal voltage is only defined by transformers
def map_nominal_voltage(self, row):
return PositiveVoltage(12.47, "kilovolts")

def map_phases(self, row, from_node_sections, to_node_sections):
node_id = row["NodeId"]
section = None
all_phases = set()
if node_id in from_node_sections:
for section in from_node_sections[node_id]:
phases = section["SectionPhases"].replace(" ","")
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["SectionPhases"].replace(" ","")
for phase in phases:
all_phases.add(phase)

all_phases = sorted(list(all_phases))
phases = []
if "A" in all_phases:
phases.append(Phase.A)
if "B" in all_phases:
phases.append(Phase.B)
if "C" in all_phases:
phases.append(Phase.C)
if "N" in all_phases:
phases.append(Phase.N)

return phases

def map_voltagelimits(self, row):
return []

def map_voltage_type(self, row):
return VoltageTypes.LINE_TO_LINE.value
return Location(x=row["X"], y=row["Y"])

def map_nominal_voltage(self, feeder_info: dict) -> Voltage:
nominal_kvll = feeder_info.get("nominal_kvll", 12.47) or 12.47
return Voltage(nominal_kvll / math.sqrt(3), "kilovolt")

def map_phases(self, node_id, from_node_sections, to_node_sections):
all_phases: set[Phase] = set()
for section in from_node_sections.get(node_id, []):
all_phases.update(parse_phases(section["SectionPhases"]))
for section in to_node_sections.get(node_id, []):
all_phases.update(parse_phases(section["SectionPhases"]))
return sort_phases(all_phases)

Loading