Coverage for src/distopf/cim_converter/processors/capacitor_processor.py: 92%
51 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-09 17:44 -0700
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-09 17:44 -0700
1from distopf.cim_converter.processors.base_processor import BaseProcessor
2from distopf.cim_converter.utils import PhaseUtils
3import cimgraph.data_profile.cimhub_2023 as cim
6class CapacitorProcessor(BaseProcessor):
7 """Processor for LinearShuntCompensator (capacitor) objects."""
9 def process(self, network) -> list[dict]:
10 """Process all LinearShuntCompensator objects for cap_data.csv."""
11 results = []
12 for capacitor in network.list_by_class(cim.LinearShuntCompensator):
13 cap_data = self._process_single_capacitor(capacitor)
14 if cap_data:
15 results.append(cap_data)
16 return results
18 def _process_single_capacitor(self, capacitor) -> dict | None:
19 """Process an individual capacitor."""
20 if not hasattr(capacitor, "Terminals") or not capacitor.Terminals:
21 return None
22 bus = capacitor.Terminals[0].ConnectivityNode
23 cap_data = {
24 "id": bus.name,
25 "name": capacitor.name,
26 "qa": 0.0,
27 "qb": 0.0,
28 "qc": 0.0,
29 "phases": "",
30 }
32 v_ln_base = self._get_bus_voltage_base(bus)
34 phase_data = {}
35 active_phases = set()
37 capacitor_phases = (
38 capacitor.ShuntCompensatorPhase
39 if hasattr(capacitor, "ShuntCompensatorPhase")
40 else []
41 )
43 if capacitor_phases:
44 for phase_comp in capacitor_phases:
45 phase_letter = self._get_phase_str(phase_comp.phase)
46 if phase_letter:
47 # bPerSection is Susceptance (B). Q = V^2 * B
48 b_per_section = (
49 float(phase_comp.bPerSection) if phase_comp.bPerSection else 0.0
50 )
51 q_var = (v_ln_base**2) * b_per_section
52 phase_data[phase_letter] = q_var / self.s_base
53 active_phases.add(phase_letter)
54 else:
55 b_per_section = (
56 float(capacitor.bPerSection)
57 if hasattr(capacitor, "bPerSection") and capacitor.bPerSection
58 else 0.0
59 )
60 eq_phases_str = PhaseUtils.get_equipment_phases(capacitor)
61 dist_phases = [p for p in eq_phases_str if p in ["a", "b", "c"]] or [
62 "a",
63 "b",
64 "c",
65 ]
67 for phase_letter in dist_phases:
68 q_var = (v_ln_base**2) * b_per_section
69 phase_data[phase_letter] = q_var / self.s_base
70 active_phases.add(phase_letter)
72 cap_data["qa"] = phase_data.get("a", 0.0)
73 cap_data["qb"] = phase_data.get("b", 0.0)
74 cap_data["qc"] = phase_data.get("c", 0.0)
75 cap_data["phases"] = "".join(sorted(active_phases))
76 return cap_data
78 def _get_phase_str(self, phase_code) -> str | None:
79 if not hasattr(phase_code, "value"):
80 return None
81 phase_str = str(phase_code.value).lower()
82 if "a" in phase_str:
83 return "a"
84 elif "b" in phase_str:
85 return "b"
86 elif "c" in phase_str:
87 return "c"
88 return None