diff --git a/roseau/load_flow/io/dgs.py b/roseau/load_flow/io/dgs.py index e36f3eee..70d167f3 100644 --- a/roseau/load_flow/io/dgs.py +++ b/roseau/load_flow/io/dgs.py @@ -170,7 +170,7 @@ def network_from_dgs( # noqa: C901 # Generate transformer parameters transformers_params_dict[idx] = TransformerParameters( - id=name, windings=windings, uhv=uhv, ulv=ulv, sn=sn, p0=p0, i0=i0, psc=psc, vsc=vsc + id=name, type=windings, uhv=uhv, ulv=ulv, sn=sn, p0=p0, i0=i0, psc=psc, vsc=vsc ) transformers_tap[idx] = typ_tr.at[idx, "dutap"] diff --git a/roseau/load_flow/io/dict.py b/roseau/load_flow/io/dict.py index 40f37239..bb10c1ca 100644 --- a/roseau/load_flow/io/dict.py +++ b/roseau/load_flow/io/dict.py @@ -15,7 +15,6 @@ VoltageSource, ) from roseau.load_flow.typing import Id, JsonDict -from roseau.load_flow.utils import TransformerType if TYPE_CHECKING: from roseau.load_flow.network import ElectricalNetwork @@ -347,7 +346,7 @@ def v0_to_v1_converter(data: JsonDict) -> JsonDict: # noqa: C901 branch["tap"] = old_branch["tap"] branch["type"] = branch_type # Transformers have no phases information, we need to infer it from the windings - w1, w2, _ = TransformerType.extract_windings(transformers_params[params_id]["type"]) + w1, w2, _ = TransformerParameters.extract_windings(transformers_params[params_id]["type"]) phases1 = "abcn" if ("y" in w1.lower() or "z" in w1.lower()) else "abc" phases2 = "abcn" if ("y" in w2.lower() or "z" in w2.lower()) else "abc" # Determine the "special element" connected to bus2 of the transformer diff --git a/roseau/load_flow/io/tests/test_dict.py b/roseau/load_flow/io/tests/test_dict.py index 11c454e1..d4a1725c 100644 --- a/roseau/load_flow/io/tests/test_dict.py +++ b/roseau/load_flow/io/tests/test_dict.py @@ -66,10 +66,10 @@ def test_to_dict(): # Same id, different transformer parameters -> fail tp1 = TransformerParameters( - "t", windings="Dyn11", uhv=20000, ulv=400, sn=160 * 1e3, p0=460, i0=2.3 / 100, psc=2350, vsc=4 / 100 + "t", type="Dyn11", uhv=20000, ulv=400, sn=160 * 1e3, p0=460, i0=2.3 / 100, psc=2350, vsc=4 / 100 ) tp2 = TransformerParameters( - "t", windings="Dyn11", uhv=20000, ulv=400, sn=200 * 1e3, p0=460, i0=2.3 / 100, psc=2350, vsc=4 / 100 + "t", type="Dyn11", uhv=20000, ulv=400, sn=200 * 1e3, p0=460, i0=2.3 / 100, psc=2350, vsc=4 / 100 ) transformer1 = Transformer(id="Transformer1", bus1=source_bus, bus2=load_bus, parameters=tp1) transformer2 = Transformer(id="Transformer2", bus1=source_bus, bus2=load_bus, parameters=tp2) @@ -88,7 +88,7 @@ def test_to_dict(): # Same id, same transformer parameters -> ok tp2 = TransformerParameters( - "t", windings="Dyn11", uhv=20000, ulv=400, sn=160 * 1e3, p0=460, i0=2.3 / 100, psc=2350, vsc=4 / 100 + "t", type="Dyn11", uhv=20000, ulv=400, sn=160 * 1e3, p0=460, i0=2.3 / 100, psc=2350, vsc=4 / 100 ) transformer2.parameters = tp2 en.to_dict() @@ -719,7 +719,7 @@ def test_v0_to_v1_converter(monkeypatch): tp = { "160kVA_Dd0": TransformerParameters( id="160kVA_Dd0", - windings="dd0", + type="dd0", sn=160000.0, uhv=20000.0, ulv=400.0, @@ -730,7 +730,7 @@ def test_v0_to_v1_converter(monkeypatch): ), "160kVA_Dd6": TransformerParameters( id="160kVA_Dd6", - windings="dd6", + type="dd6", sn=160000.0, uhv=20000.0, ulv=400.0, @@ -741,7 +741,7 @@ def test_v0_to_v1_converter(monkeypatch): ), "160kVA_Dyn11": TransformerParameters( id="160kVA_Dyn11", - windings="dyn11", + type="dyn11", sn=160000.0, uhv=20000.0, ulv=400.0, @@ -752,7 +752,7 @@ def test_v0_to_v1_converter(monkeypatch): ), "160kVA_Dyn5": TransformerParameters( id="160kVA_Dyn5", - windings="dyn5", + type="dyn5", sn=160000.0, uhv=20000.0, ulv=400.0, @@ -763,7 +763,7 @@ def test_v0_to_v1_converter(monkeypatch): ), "160kVA_Dzn0": TransformerParameters( id="160kVA_Dzn0", - windings="dzn0", + type="dzn0", sn=160000.0, uhv=20000.0, ulv=400.0, @@ -774,7 +774,7 @@ def test_v0_to_v1_converter(monkeypatch): ), "160kVA_Dzn6": TransformerParameters( id="160kVA_Dzn6", - windings="dzn6", + type="dzn6", sn=160000.0, uhv=20000.0, ulv=400.0, @@ -785,7 +785,7 @@ def test_v0_to_v1_converter(monkeypatch): ), "160kVA_Yd11": TransformerParameters( id="160kVA_Yd11", - windings="yd11", + type="yd11", sn=160000.0, uhv=20000.0, ulv=400.0, @@ -796,7 +796,7 @@ def test_v0_to_v1_converter(monkeypatch): ), "160kVA_Yd5": TransformerParameters( id="160kVA_Yd5", - windings="yd5", + type="yd5", sn=160000.0, uhv=20000.0, ulv=400.0, @@ -807,7 +807,7 @@ def test_v0_to_v1_converter(monkeypatch): ), "160kVA_Yyn0": TransformerParameters( id="160kVA_Yyn0", - windings="yyn0", + type="yyn0", sn=160000.0, uhv=20000.0, ulv=400.0, @@ -818,7 +818,7 @@ def test_v0_to_v1_converter(monkeypatch): ), "160kVA_Yyn6": TransformerParameters( id="160kVA_Yyn6", - windings="yyn6", + type="yyn6", sn=160000.0, uhv=20000.0, ulv=400.0, @@ -829,7 +829,7 @@ def test_v0_to_v1_converter(monkeypatch): ), "160kVA_Yzn11": TransformerParameters( id="160kVA_Yzn11", - windings="yzn11", + type="yzn11", sn=160000.0, uhv=20000.0, ulv=400.0, @@ -840,7 +840,7 @@ def test_v0_to_v1_converter(monkeypatch): ), "160kVA_Yzn5": TransformerParameters( id="160kVA_Yzn5", - windings="yzn5", + type="yzn5", sn=160000.0, uhv=20000.0, ulv=400.0, diff --git a/roseau/load_flow/models/core.py b/roseau/load_flow/models/core.py index 973aa39c..bfdca36c 100644 --- a/roseau/load_flow/models/core.py +++ b/roseau/load_flow/models/core.py @@ -47,12 +47,14 @@ def network(self) -> Optional["ElectricalNetwork"]: return self._network @classmethod - def _check_phases(cls, id: Id, **kwargs: str) -> None: + def _check_phases(cls, id: Id, allowed_phases: Optional[frozenset[str]] = None, **kwargs: str) -> None: + if allowed_phases is None: + allowed_phases = cls.allowed_phases name, phases = kwargs.popitem() # phases, phases1 or phases2 - if phases not in cls.allowed_phases: + if phases not in allowed_phases: msg = ( f"{cls.__name__} of id {id!r} got invalid {name} {phases!r}, allowed values are: " - f"{sorted(cls.allowed_phases)}" + f"{sorted(allowed_phases)}" ) logger.error(msg) raise RoseauLoadFlowException(msg, RoseauLoadFlowExceptionCode.BAD_PHASE) @@ -65,7 +67,7 @@ def _set_network(self, value: Optional["ElectricalNetwork"]) -> None: value: The new network for `self`. May also be None. """ - # The setter can not be used to replace an existing network + # The setter cannot be used to replace an existing network if self._network is not None and value is not None and self._network != value: self._raise_several_network() diff --git a/roseau/load_flow/models/loads/loads.py b/roseau/load_flow/models/loads/loads.py index f27e4917..31b8e6f2 100644 --- a/roseau/load_flow/models/loads/loads.py +++ b/roseau/load_flow/models/loads/loads.py @@ -343,7 +343,7 @@ def res_flexible_powers(self) -> Q_: # def to_dict(self) -> JsonDict: if self.bus is None: - msg = f"The load {self.id!r} is disconnected and can not be used anymore." + msg = f"The load {self.id!r} is disconnected and cannot be used anymore." logger.error(msg) raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.DISCONNECTED_ELEMENT) res = { @@ -428,7 +428,7 @@ def currents(self, value: Sequence[complex]) -> None: def to_dict(self) -> JsonDict: if self.bus is None: - msg = f"The load {self.id!r} is disconnected and can not be used anymore." + msg = f"The load {self.id!r} is disconnected and cannot be used anymore." logger.error(msg) raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.DISCONNECTED_ELEMENT) return { @@ -497,7 +497,7 @@ def impedances(self, impedances: Sequence[complex]) -> None: def to_dict(self) -> JsonDict: if self.bus is None: - msg = f"The load {self.id!r} is disconnected and can not be used anymore." + msg = f"The load {self.id!r} is disconnected and cannot be used anymore." logger.error(msg) raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.DISCONNECTED_ELEMENT) return { diff --git a/roseau/load_flow/models/sources.py b/roseau/load_flow/models/sources.py index 0afc34a5..02547b46 100644 --- a/roseau/load_flow/models/sources.py +++ b/roseau/load_flow/models/sources.py @@ -170,7 +170,7 @@ def from_dict(cls, data: JsonDict) -> Self: def to_dict(self) -> JsonDict: if self.bus is None: - msg = f"The voltage source {self.id!r} is disconnected and can not be used anymore." + msg = f"The voltage source {self.id!r} is disconnected and cannot be used anymore." logger.error(msg) raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.DISCONNECTED_ELEMENT) return { diff --git a/roseau/load_flow/models/tests/test_phases.py b/roseau/load_flow/models/tests/test_phases.py index 9220fd51..68b8c52d 100644 --- a/roseau/load_flow/models/tests/test_phases.py +++ b/roseau/load_flow/models/tests/test_phases.py @@ -217,11 +217,11 @@ def test_switches_phases(): assert switch.phases == switch.phases1 == switch.phases2 == "ca" -def test_transformer_phases(): +def test_transformer_three_phases(): bus1 = Bus("bus-1", phases="abcn") bus2 = Bus("bus-2", phases="abcn") - assert Transformer.allowed_phases == {"abc", "abcn"} + assert Transformer.allowed_phases == Bus.allowed_phases # Not allowed tp = TransformerParameters.from_name("H61_50kVA", "Dyn11") @@ -252,18 +252,163 @@ def test_transformer_phases(): # Default bus1.phases = "abc" bus2.phases = "abcn" - transformer = Transformer(id="tr1", bus1=bus1, bus2=bus2, parameters=tp, length=10) + transformer = Transformer(id="tr1", bus1=bus1, bus2=bus2, parameters=tp) assert transformer.phases1 == "abc" assert transformer.phases2 == "abcn" # Intersection bus1.phases = "abcn" bus2.phases = "abcn" - transformer = Transformer(id="tr1", bus1=bus1, bus2=bus2, parameters=tp, length=10) + transformer = Transformer(id="tr1", bus1=bus1, bus2=bus2, parameters=tp) assert transformer.phases1 == "abc" assert transformer.phases2 == "abcn" +def test_transformer_single_phases(): + bus1 = Bus("bus-1", phases="an") + bus2 = Bus("bus-2", phases="an") + + # Not allowed + tp = TransformerParameters.from_name("160kVA", "single") + for ph in ("ba", "nc", "anb", "nabc", "acb"): + with pytest.raises(RoseauLoadFlowException) as e: + Transformer("tr1", bus1, bus2, phases1=ph, phases2=ph, parameters=tp) + assert e.value.code == RoseauLoadFlowExceptionCode.BAD_PHASE + assert e.value.msg.startswith(f"Transformer of id 'tr1' got invalid phases1 '{ph}', allowed values are") + + # Allowed + Transformer("tr1", bus1, bus2, phases1="an", phases2="an", parameters=tp) + + # Not in bus + bus2.phases = "ab" + with pytest.raises(RoseauLoadFlowException) as e: + Transformer("tr1", bus1, bus2, phases1="an", phases2="an", parameters=tp) + assert e.value.code == RoseauLoadFlowExceptionCode.BAD_PHASE + assert e.value.msg == "Phases (2) ['n'] of transformer 'tr1' are not in phases 'ab' of bus 'bus-2'." + + # Default + bus1.phases = "ab" + bus2.phases = "ab" + transformer = Transformer(id="tr1", bus1=bus1, bus2=bus2, parameters=tp) + assert transformer.phases1 == "ab" + assert transformer.phases2 == "ab" + + # Intersection + bus1.phases = "abcn" + bus2.phases = "ab" + transformer = Transformer(id="tr1", bus1=bus1, bus2=bus2, parameters=tp) + assert transformer.phases1 == "ab" + assert transformer.phases2 == "ab" + + bus1.phases = "abc" + bus2.phases = "bcn" + transformer = Transformer(id="tr1", bus1=bus1, bus2=bus2, parameters=tp) + assert transformer.phases1 == "bc" + assert transformer.phases2 == "bc" + + bus1.phases = "abc" + bus2.phases = "ca" + transformer = Transformer(id="tr1", bus1=bus1, bus2=bus2, parameters=tp) + assert transformer.phases1 == "ca" + assert transformer.phases2 == "ca" + + # Cannot be deduced + bus1.phases = "abc" + bus2.phases = "abc" + with pytest.raises(RoseauLoadFlowException) as e: + Transformer(id="tr1", bus1=bus1, bus2=bus2, parameters=tp) + assert e.value.code == RoseauLoadFlowExceptionCode.BAD_PHASE + assert e.value.msg == "Phases (1) of transformer 'tr1' cannot be deduced from the buses, they need to be specified." + + bus1.phases = "abcn" + bus2.phases = "abn" + with pytest.raises(RoseauLoadFlowException) as e: + Transformer(id="tr1", bus1=bus1, bus2=bus2, parameters=tp) + assert e.value.code == RoseauLoadFlowExceptionCode.BAD_PHASE + assert e.value.msg == "Phases (1) of transformer 'tr1' cannot be deduced from the buses, they need to be specified." + + bus1.phases = "abcn" + bus2.phases = "a" + with pytest.raises(RoseauLoadFlowException) as e: + Transformer(id="tr1", bus1=bus1, bus2=bus2, parameters=tp) + assert e.value.code == RoseauLoadFlowExceptionCode.BAD_PHASE + assert e.value.msg == "Phases (1) of transformer 'tr1' cannot be deduced from the buses, they need to be specified." + + +def test_transformer_split_phases(): + bus1 = Bus("bus-1", phases="ab") + bus2 = Bus("bus-2", phases="abn") + + # Not allowed + tp = TransformerParameters.from_name("160kVA", "split") + for ph in ("ba", "nc", "anb", "nabc", "acb"): + with pytest.raises(RoseauLoadFlowException) as e: + Transformer("tr1", bus1, bus2, phases1=ph, phases2=ph, parameters=tp) + assert e.value.code == RoseauLoadFlowExceptionCode.BAD_PHASE + assert e.value.msg.startswith(f"Transformer of id 'tr1' got invalid phases1 '{ph}', allowed values are") + + # Allowed + Transformer("tr1", bus1, bus2, phases1="ab", phases2="abn", parameters=tp) + + # Not in bus 1 + bus1.phases = "acn" + with pytest.raises(RoseauLoadFlowException) as e: + Transformer("tr1", bus1, bus2, phases1="ab", phases2="abn", parameters=tp) + assert e.value.code == RoseauLoadFlowExceptionCode.BAD_PHASE + assert e.value.msg == "Phases (1) ['b'] of transformer 'tr1' are not in phases 'acn' of bus 'bus-1'." + + # Not in bus 2 + bus1.phases = "abc" + bus2.phases = "acn" + with pytest.raises(RoseauLoadFlowException) as e: + Transformer("tr1", bus1, bus2, phases1="ab", phases2="abn", parameters=tp) + assert e.value.code == RoseauLoadFlowExceptionCode.BAD_PHASE + assert e.value.msg == "Phases (2) ['b'] of transformer 'tr1' are not in phases 'acn' of bus 'bus-2'." + + # Default + bus1.phases = "ab" + bus2.phases = "abn" + transformer = Transformer(id="tr1", bus1=bus1, bus2=bus2, parameters=tp) + assert transformer.phases1 == "ab" + assert transformer.phases2 == "abn" + + # Intersection + bus1.phases = "abcn" + bus2.phases = "can" + transformer = Transformer(id="tr1", bus1=bus1, bus2=bus2, parameters=tp) + assert transformer.phases1 == "ca" + assert transformer.phases2 == "can" + + # Cannot be deduced + bus1.phases = "abc" + bus2.phases = "abcn" + with pytest.raises(RoseauLoadFlowException) as e: + Transformer(id="tr1", bus1=bus1, bus2=bus2, parameters=tp) + assert e.value.code == RoseauLoadFlowExceptionCode.BAD_PHASE + assert e.value.msg == "Phases (1) of transformer 'tr1' cannot be deduced from the buses, they need to be specified." + + bus1.phases = "a" + bus2.phases = "abn" + with pytest.raises(RoseauLoadFlowException) as e: + Transformer(id="tr1", bus1=bus1, bus2=bus2, parameters=tp) + assert e.value.code == RoseauLoadFlowExceptionCode.BAD_PHASE + assert e.value.msg == "Phases (1) of transformer 'tr1' cannot be deduced from the buses, they need to be specified." + + bus1.phases = "ab" + bus2.phases = "ab" + with pytest.raises(RoseauLoadFlowException) as e: + Transformer(id="tr1", bus1=bus1, bus2=bus2, parameters=tp) + assert e.value.code == RoseauLoadFlowExceptionCode.BAD_PHASE + assert e.value.msg == "Phases (2) of transformer 'tr1' cannot be deduced from the buses, they need to be specified." + + bus1.phases = "ab" + bus2.phases = "abc" + with pytest.raises(RoseauLoadFlowException) as e: + Transformer(id="tr1", bus1=bus1, bus2=bus2, parameters=tp) + assert e.value.code == RoseauLoadFlowExceptionCode.BAD_PHASE + assert e.value.msg == "Phases (2) of transformer 'tr1' cannot be deduced from the buses, they need to be specified." + + def test_voltage_phases(): # Bus bus = Bus("bus", phases="abcn") diff --git a/roseau/load_flow/models/tests/test_transformer_parameters.py b/roseau/load_flow/models/tests/test_transformer_parameters.py index e4dcb105..d4d11d00 100644 --- a/roseau/load_flow/models/tests/test_transformer_parameters.py +++ b/roseau/load_flow/models/tests/test_transformer_parameters.py @@ -264,3 +264,66 @@ def test_transformers_parameters_units(): DimensionalityError, match=r"Cannot convert from 'ampere' \(\[current\]\) to '\w+?' \(.+?\)" ): TransformerParameters.from_dict(copy_data) + + +def test_transformer_type(): + valid_windings = ("y", "yn", "z", "zn", "d") + valid_phase_displacements = (0, 5, 6, 11) + valid_types = {"dd", "yy", "yny", "yyn", "ynyn", "dz", "dzn", "dy", "dyn", "yd", "ynd", "yz", "ynz", "yzn", "ynzn"} + valid_full_types = { + "dd0", + "dd6", + "yy0", + "yy6", + "yny0", + "yny6", + "yyn0", + "yyn6", + "ynyn0", + "ynyn6", + "dz0", + "dz6", + "dzn0", + "dzn6", + "dy5", + "dy11", + "dyn5", + "dyn11", + "yd5", + "yd11", + "ynd5", + "ynd11", + "yz5", + "yz11", + "ynz5", + "ynz11", + "yzn5", + "yzn11", + "ynzn5", + "ynzn11", + } + + for winding1 in valid_windings: + for winding2 in valid_windings: + t = f"{winding1}{winding2}" + if t in valid_types: + with pytest.raises(RoseauLoadFlowException) as e: + TransformerParameters.extract_windings(t) + assert "Transformer windings cannot be extracted from the string" in e.value.msg + assert e.value.code == RoseauLoadFlowExceptionCode.BAD_TRANSFORMER_WINDINGS + for phase_displacement in valid_phase_displacements: + t = f"{winding1}{winding2}{phase_displacement}" + if t in valid_full_types: + w1, w2, p = TransformerParameters.extract_windings(t) + assert w1 == winding1.upper() + assert w2 == winding2 + assert p == phase_displacement + else: + with pytest.raises(RoseauLoadFlowException) as e: + TransformerParameters.extract_windings(t) + assert e.value.args[1] == RoseauLoadFlowExceptionCode.BAD_TRANSFORMER_WINDINGS + else: + with pytest.raises(RoseauLoadFlowException) as e: + TransformerParameters.extract_windings(t) + assert "Transformer windings cannot be extracted from the string" in e.value.msg + assert e.value.code == RoseauLoadFlowExceptionCode.BAD_TRANSFORMER_WINDINGS diff --git a/roseau/load_flow/models/transformers/parameters.py b/roseau/load_flow/models/transformers/parameters.py index 66c0c4af..7cd2de12 100644 --- a/roseau/load_flow/models/transformers/parameters.py +++ b/roseau/load_flow/models/transformers/parameters.py @@ -2,12 +2,13 @@ from typing import NoReturn import numpy as np +import regex from typing_extensions import Self from roseau.load_flow.exceptions import RoseauLoadFlowException, RoseauLoadFlowExceptionCode from roseau.load_flow.typing import Id, JsonDict from roseau.load_flow.units import Q_, ureg -from roseau.load_flow.utils import Identifiable, JsonMixin, TransformerType +from roseau.load_flow.utils import Identifiable, JsonMixin logger = logging.getLogger(__name__) @@ -15,11 +16,25 @@ class TransformerParameters(Identifiable, JsonMixin): """A class to store the parameters of the transformers.""" + _EXTRACT_WINDINGS_RE = regex.compile( + "(?(DEFINE)(?Pyn?)(?Pd)(?Pzn?)(?P[06])" + "(?P5|11))" + "" + "(?|(?P(?&y_winding))(?P(?&y_winding))(?P

(?&p_set_1))" # yy + "|(?P(?&y_winding))(?P(?&d_winding))(?P

(?&p_set_2))" # yd + "|(?P(?&y_winding))(?P(?&z_winding))(?P

(?&p_set_2))" # yz + "|(?P(?&d_winding))(?P(?&z_winding))(?P

(?&p_set_1))" # dz + "|(?P(?&d_winding))(?P(?&y_winding))(?P

(?&p_set_2))" # dy + "|(?P(?&d_winding))(?P(?&d_winding))(?P

(?&p_set_1)))", # dd + regex.IGNORECASE, + ) + """The pattern to extract the winding of the primary and of the secondary of the transformer.""" + @ureg.wraps(None, (None, None, None, "V", "V", "VA", "W", "", "W", ""), strict=False) def __init__( self, id: Id, - windings: str, + type: str, uhv: float, ulv: float, sn: float, @@ -34,8 +49,10 @@ def __init__( id: A unique ID of the transformer parameters, typically its canonical name. - windings: - The type of windings such as "Dyn11" + type: + The type of transformer parameters. It can be "single" for single-phase transformers, "split" for + split-phase transformers, or the name of the windings such as "Dyn11" for three-phase transformers. + Allowed windings are "D" for delta, "Y" for wye (star), and "Z" for zigzag. uhv: Phase-to-phase nominal voltages of the high voltages side (V) @@ -66,8 +83,13 @@ def __init__( self._p0 = p0 self._psc = psc self._vsc = vsc - self.windings = windings - self.winding1, self.winding2, self.phase_displacement = TransformerType.extract_windings(string=windings) + self.type = type + if type in ("single", "split"): + self.winding1 = None + self.winding2 = None + self.phase_displacement = None + else: + self.winding1, self.winding2, self.phase_displacement = self.extract_windings(string=type) # Check if uhv <= ulv: @@ -110,7 +132,7 @@ def __eq__(self, other: object) -> bool: else: return ( self.id == other.id - and self.windings == other.windings + and self.type == other.type and np.isclose(self._sn, other._sn) and np.isclose(self._p0, other._p0) and np.isclose(self._i0, other._i0) @@ -163,23 +185,21 @@ def vsc(self) -> float: return self._vsc @classmethod - def from_name(cls, name: str, windings: str) -> Self: - """Construct TransformerParameters from name and windings. + def from_name(cls, name: str, type: str) -> Self: + """Construct TransformerParameters from name and types. Args: name: The name of the transformer parameters, such as `"160kVA"` or `"H61_50kVA"`. - windings: - The type of windings such as `"Dyn11"`. + type: + The type of transformer parameters such as "Dyn11", "single", "split". Returns: The constructed transformer parameters. """ if name == "H61_50kVA": - return cls( - id=name, windings=windings, uhv=20000, ulv=400, sn=50 * 1e3, p0=145, i0=1.8 / 100, psc=1350, vsc=4 / 100 - ) + return cls(id=name, type=type, uhv=20000, ulv=400, sn=50 * 1e3, p0=145, i0=1.8 / 100, psc=1350, vsc=4 / 100) elif name[-3:] == "kVA": try: sn = float(name[:-3]) @@ -188,7 +208,7 @@ def from_name(cls, name: str, windings: str) -> Self: logger.error(msg) raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_TYPE_NAME_SYNTAX) from None else: - return cls(name, windings, 20000, 400, sn * 1e3, 460, 2.3 / 100, 2350, 4 / 100) + return cls(name, type, 20000, 400, sn * 1e3, 460, 2.3 / 100, 2350, 4 / 100) else: msg = f"The transformer type name does not follow the syntax rule. {name!r} was provided." logger.error(msg) @@ -208,9 +228,6 @@ def to_zyk(self) -> tuple[Q_, Q_, float, float]: Returns: The parameters (``z2``, ``ym``, ``k``, ``orientation``). """ - # Extract the windings of the primary and the secondary of the transformer - winding1, winding2, phase_displacement = TransformerType.extract_windings(self.windings) - # Off-load test # Iron losses resistance (Ohm) r_iron = self._uhv**2 / self._p0 @@ -229,22 +246,25 @@ def to_zyk(self) -> tuple[Q_, Q_, float, float]: # Change the voltages if the reference voltages is phase to neutral uhv = self._uhv ulv = self._ulv - if winding1[0] in ("y", "Y"): - uhv /= np.sqrt(3.0) - if winding2[0] in ("y", "Y"): - ulv /= np.sqrt(3.0) - if winding1[0] in ("z", "Z"): - uhv /= 3.0 - if winding2[0] in ("z", "Z"): - ulv /= 3.0 - - if phase_displacement in (5, 6): - # Reverse winding - return z2, ym, ulv / uhv, -1.0 + if self.type == "single" or self.type == "split": + orientation = 1.0 else: - # Normal winding - assert phase_displacement in (0, 11) - return z2, ym, ulv / uhv, 1.0 + # Extract the windings of the primary and the secondary of the transformer + if self.winding1[0] in ("y", "Y"): + uhv /= np.sqrt(3.0) + if self.winding2[0] in ("y", "Y"): + ulv /= np.sqrt(3.0) + if self.winding1[0] in ("z", "Z"): + uhv /= 3.0 + if self.winding2[0] in ("z", "Z"): + ulv /= 3.0 + if self.phase_displacement in (0, 11): # Normal winding + orientation = 1.0 + else: # Reverse winding + assert self.phase_displacement in (5, 6) + orientation = -1.0 + + return z2, ym, ulv / uhv, orientation # # Json Mixin interface @@ -253,7 +273,7 @@ def to_zyk(self) -> tuple[Q_, Q_, float, float]: def from_dict(cls, data: JsonDict) -> Self: return cls( id=data["id"], - windings=data["type"], # Windings of the transformer + type=data["type"], # Type of the transformer uhv=data["uhv"], # Phase-to-phase nominal voltages of the high voltages side (V) ulv=data["ulv"], # Phase-to-phase nominal voltages of the low voltages side (V) sn=data["sn"], @@ -273,7 +293,7 @@ def to_dict(self) -> JsonDict: "p0": self._p0, "psc": self._psc, "vsc": self._vsc, - "type": self.windings, + "type": self.type, } def _results_to_dict(self, warning: bool) -> NoReturn: @@ -285,3 +305,24 @@ def results_from_dict(self, data: JsonDict) -> NoReturn: msg = f"The {type(self).__name__} has no results to import." logger.error(msg) raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.JSON_NO_RESULTS) + + @classmethod + def extract_windings(cls, string: str) -> tuple[str, str, int]: + """Extract the windings and phase displacement from a given string + + Args: + string: + The string to parse. + + Returns: + The first winding, the second winding, and the phase displacement + """ + match = cls._EXTRACT_WINDINGS_RE.fullmatch(string=string) + if match: + groups = match.groupdict() + winding1, winding2, phase_displacement = groups["w1"], groups["w2"], groups["p"] + return winding1.upper(), winding2.lower(), int(phase_displacement) + else: + msg = f"Transformer windings cannot be extracted from the string {string!r}." + logger.error(msg) + raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_TRANSFORMER_WINDINGS) diff --git a/roseau/load_flow/models/transformers/transformers.py b/roseau/load_flow/models/transformers/transformers.py index f94ba6c0..56467aa9 100644 --- a/roseau/load_flow/models/transformers/transformers.py +++ b/roseau/load_flow/models/transformers/transformers.py @@ -16,19 +16,22 @@ class Transformer(AbstractBranch): """A generic transformer model. - The model parameters and windings type are defined in the ``parameters``. + The model parameters are defined in the ``parameters``. """ branch_type = BranchType.TRANSFORMER - allowed_phases = frozenset({"abc", "abcn"}) # Only these for now + allowed_phases = Bus.allowed_phases """The allowed phases for a transformer are: - - P-P-P or P-P-P-N: ``"abc"``, ``"abcn"`` - - .. note:: - Only 3-phase transformers are currently supported. + - P-P-P or P-P-P-N: ``"abc"``, ``"abcn"`` (three-phase transformer) + - P-P or P-N: ``"ab"``, ``"bc"``, ``"ca"``, ``"an"``, ``"bn"``, ``"cn"`` (single-phase + transformer or primary of split-phase transformer) + - P-P-N: ``"abn"``, ``"bcn"``, ``"can"`` (secondary of split-phase transformer) """ + _allowed_phases_three = frozenset({"abc", "abcn"}) + _allowed_phases_single = frozenset({"ab", "bc", "ca", "an", "bn", "cn"}) + _allowed_phases_split_secondary = frozenset({"abn", "bcn", "can"}) def __init__( self, @@ -65,7 +68,7 @@ def __init__( The phases of the first extremity of the transformer. A string like ``"abc"`` or ``"abcn"`` etc. The order of the phases is important. For a full list of supported phases, see the class attribute :attr:`allowed_phases`. All phases must be present - in the connected bus. By default determined from the transformer windings. + in the connected bus. By default determined from the transformer type. phases2: The phases of the second extremity of the transformer. See ``phases1``. @@ -78,24 +81,72 @@ def __init__( logger.error(msg) raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_GEOMETRY_TYPE) - # Compute the phases if not provided, check them if provided + if parameters.type == "single": + phases1, phases2 = self._compute_phases_single( + id=id, bus1=bus1, bus2=bus2, phases1=phases1, phases2=phases2 + ) + elif parameters.type == "split": + phases1, phases2 = self._compute_phases_split(id=id, bus1=bus1, bus2=bus2, phases1=phases1, phases2=phases2) + else: + phases1, phases2 = self._compute_phases_three( + id=id, bus1=bus1, bus2=bus2, parameters=parameters, phases1=phases1, phases2=phases2 + ) + + super().__init__(id, bus1, bus2, phases1=phases1, phases2=phases2, geometry=geometry, **kwargs) + self.tap = tap + self._parameters = parameters + + @property + def tap(self) -> float: + """The tap of the transformer, for example 1.02.""" + return self._tap + + @tap.setter + def tap(self, value: float) -> None: + if value > 1.1: + logger.warning(f"The provided tap {value:.2f} is higher than 1.1. A good value is between 0.9 and 1.1.") + if value < 0.9: + logger.warning(f"The provided tap {value:.2f} is lower than 0.9. A good value is between 0.9 and 1.1.") + self._tap = value + self._invalidate_network_results() + + @property + def parameters(self) -> TransformerParameters: + """The parameters of the transformer.""" + return self._parameters + + @parameters.setter + def parameters(self, value: TransformerParameters) -> None: + type1 = self._parameters.type + type2 = value.type + if type1 != type2: + msg = f"The updated type changed for transformer {self.id!r}: {type1} to {type2}." + logger.error(msg) + raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_TRANSFORMER_TYPE) + self._parameters = value + self._invalidate_network_results() + + def to_dict(self) -> JsonDict: + return {**super().to_dict(), "params_id": self.parameters.id, "tap": self.tap} + + def _compute_phases_three( + self, + id: Id, + bus1: Bus, + bus2: Bus, + parameters: TransformerParameters, + phases1: Optional[str], + phases2: Optional[str], + ) -> tuple[str, str]: w1_has_neutral = "y" in parameters.winding1.lower() or "z" in parameters.winding1.lower() w2_has_neutral = "y" in parameters.winding2.lower() or "z" in parameters.winding2.lower() if phases1 is None: phases1 = "abcn" if w1_has_neutral else "abc" phases1 = "".join(p for p in bus1.phases if p in phases1) - self._check_phases(id, phases1=phases1) + self._check_phases(id, allowed_phases=self._allowed_phases_three, phases1=phases1) else: - self._check_phases(id, phases1=phases1) - # Check that the phases are in the bus - phases_not_in_bus1 = set(phases1) - set(bus1.phases) - if phases_not_in_bus1: - msg = ( - f"Phases (1) {sorted(phases_not_in_bus1)} of transformer {id!r} are not in phases " - f"{bus1.phases!r} of bus {bus1.id!r}." - ) - logger.error(msg) - raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_PHASE) + self._check_phases(id, allowed_phases=self._allowed_phases_three, phases1=phases1) + self._check_bus_phases(id, bus1, phases1=phases1) transformer_phases = "abcn" if w1_has_neutral else "abc" phases_not_in_transformer = set(phases1) - set(transformer_phases) if phases_not_in_transformer: @@ -109,18 +160,10 @@ def __init__( if phases2 is None: phases2 = "abcn" if w2_has_neutral else "abc" phases2 = "".join(p for p in bus2.phases if p in phases2) - self._check_phases(id, phases2=phases2) + self._check_phases(id, allowed_phases=self._allowed_phases_three, phases2=phases2) else: - self._check_phases(id, phases2=phases2) - # Check that the phases are in the bus - phases_not_in_bus2 = set(phases2) - set(bus2.phases) - if phases_not_in_bus2: - msg = ( - f"Phases (2) {sorted(phases_not_in_bus2)} of transformer {id!r} are not in phases " - f"{bus2.phases!r} of bus {bus2.id!r}." - ) - logger.error(msg) - raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_PHASE) + self._check_phases(id, allowed_phases=self._allowed_phases_three, phases2=phases2) + self._check_bus_phases(id, bus2, phases2=phases2) transformer_phases = "abcn" if w2_has_neutral else "abc" phases_not_in_transformer = set(phases2) - set(transformer_phases) if phases_not_in_transformer: @@ -131,39 +174,70 @@ def __init__( logger.error(msg) raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_PHASE) - super().__init__(id, bus1, bus2, phases1=phases1, phases2=phases2, geometry=geometry, **kwargs) - self.tap = tap - self._parameters = parameters + return phases1, phases2 - @property - def tap(self) -> float: - """The tap of the transformer, for example 1.02.""" - return self._tap + def _compute_phases_single( + self, id: Id, bus1: Bus, bus2: Bus, phases1: Optional[str], phases2: Optional[str] + ) -> tuple[str, str]: + if phases1 is None: + phases1 = "".join(p for p in bus1.phases if p in bus2.phases) # can't use set because order is important + phases1 = phases1.replace("ac", "ca") + if phases1 not in self._allowed_phases_single: + msg = f"Phases (1) of transformer {id!r} cannot be deduced from the buses, they need to be specified." + logger.error(msg) + raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_PHASE) + else: + self._check_phases(id, allowed_phases=self._allowed_phases_single, phases1=phases1) + self._check_bus_phases(id, bus1, phases1=phases1) - @tap.setter - def tap(self, value: float) -> None: - if value > 1.1: - logger.warning(f"The provided tap {value:.2f} is higher than 1.1. A good value is between 0.9 and 1.1.") - if value < 0.9: - logger.warning(f"The provided tap {value:.2f} is lower than 0.9. A good value is between 0.9 and 1.1.") - self._tap = value - self._invalidate_network_results() + if phases2 is None: + phases2 = "".join(p for p in bus1.phases if p in bus2.phases) # can't use set because order is important + phases2 = phases2.replace("ac", "ca") + if phases2 not in self._allowed_phases_single: + msg = f"Phases (2) of transformer {id!r} cannot be deduced from the buses, they need to be specified." + logger.error(msg) + raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_PHASE) + else: + self._check_phases(id, allowed_phases=self._allowed_phases_single, phases2=phases2) + self._check_bus_phases(id, bus2, phases2=phases2) - @property - def parameters(self) -> TransformerParameters: - """The parameters of the transformer.""" - return self._parameters + return phases1, phases2 - @parameters.setter - def parameters(self, value: TransformerParameters) -> None: - windings1 = self._parameters.windings - windings2 = value.windings - if windings1 != windings2: - msg = f"The updated windings changed for transformer {self.id!r}: {windings1} to {windings2}." - logger.error(msg) - raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_TRANSFORMER_WINDINGS) - self._parameters = value - self._invalidate_network_results() + def _compute_phases_split( + self, id: Id, bus1: Bus, bus2: Bus, phases1: Optional[str], phases2: Optional[str] + ) -> tuple[str, str]: + if phases1 is None: + phases1 = "".join(p for p in bus2.phases if p in bus1.phases and p != "n") + phases1 = phases1.replace("ac", "ca") + if phases1 not in self._allowed_phases_single: + msg = f"Phases (1) of transformer {id!r} cannot be deduced from the buses, they need to be specified." + logger.error(msg) + raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_PHASE) + else: + self._check_phases(id, allowed_phases=self._allowed_phases_single, phases1=phases1) + self._check_bus_phases(id, bus1, phases1=phases1) - def to_dict(self) -> JsonDict: - return {**super().to_dict(), "params_id": self.parameters.id, "tap": self.tap} + if phases2 is None: + phases2 = "".join(p for p in bus2.phases if p in bus1.phases or p == "n") + if phases2 not in self._allowed_phases_split_secondary: + msg = f"Phases (2) of transformer {id!r} cannot be deduced from the buses, they need to be specified." + logger.error(msg) + raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_PHASE) + else: + self._check_phases(id, allowed_phases=self._allowed_phases_split_secondary, phases2=phases2) + self._check_bus_phases(id, bus2, phases2=phases2) + + return phases1, phases2 + + @staticmethod + def _check_bus_phases(id: Id, bus: Bus, **kwargs: str) -> None: + name, phases = kwargs.popitem() # phases1 or phases2 + name = "Phases (1)" if name == "phases1" else "Phases (2)" + phases_not_in_bus = set(phases) - set(bus.phases) + if phases_not_in_bus: + msg = ( + f"{name} {sorted(phases_not_in_bus)} of transformer {id!r} are not in phases " + f"{bus.phases!r} of bus {bus.id!r}." + ) + logger.error(msg) + raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_PHASE) diff --git a/roseau/load_flow/network.py b/roseau/load_flow/network.py index 45c41b51..d7306e56 100644 --- a/roseau/load_flow/network.py +++ b/roseau/load_flow/network.py @@ -933,7 +933,7 @@ def _disconnect_element(self, element: Element) -> None: """ # The C++ electrical network and the tape will be recomputed if isinstance(element, (Bus, AbstractBranch)): - msg = f"{element!r} is a {type(element).__name__} and it can not be disconnected from a network." + msg = f"{element!r} is a {type(element).__name__} and it cannot be disconnected from a network." logger.error(msg) raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_ELEMENT_OBJECT) elif isinstance(element, AbstractLoad): diff --git a/roseau/load_flow/tests/test_electrical_network.py b/roseau/load_flow/tests/test_electrical_network.py index 2f68b9f1..4b277bd7 100644 --- a/roseau/load_flow/tests/test_electrical_network.py +++ b/roseau/load_flow/tests/test_electrical_network.py @@ -209,7 +209,7 @@ def test_connect_and_disconnect(): assert load.bus is None with pytest.raises(RoseauLoadFlowException) as e: load.to_dict() - assert e.value.args[0] == "The load 'power load' is disconnected and can not be used anymore." + assert e.value.args[0] == "The load 'power load' is disconnected and cannot be used anymore." assert e.value.args[1] == RoseauLoadFlowExceptionCode.DISCONNECTED_ELEMENT new_load = PowerLoad(id="power load", phases="abcn", bus=load_bus, powers=[100 + 0j, 100 + 0j, 100 + 0j]) assert new_load.network == en @@ -221,7 +221,7 @@ def test_connect_and_disconnect(): assert vs.bus is None with pytest.raises(RoseauLoadFlowException) as e: vs.to_dict() - assert e.value.args[0] == "The voltage source 'vs' is disconnected and can not be used anymore." + assert e.value.args[0] == "The voltage source 'vs' is disconnected and cannot be used anymore." assert e.value.args[1] == RoseauLoadFlowExceptionCode.DISCONNECTED_ELEMENT # Bad key @@ -242,7 +242,7 @@ def test_connect_and_disconnect(): en._disconnect_element(line) assert ( e.value.msg - == "Line(id='line', phases1='abcn', phases2='abcn', bus1='source', bus2='load bus') is a Line and it can not " + == "Line(id='line', phases1='abcn', phases2='abcn', bus1='source', bus2='load bus') is a Line and it cannot " "be disconnected from a network." ) assert e.value.code == RoseauLoadFlowExceptionCode.BAD_ELEMENT_OBJECT @@ -447,7 +447,7 @@ def test_bad_networks(): # No potential reference bus3 = Bus("bus3", phases="abcn") tp = TransformerParameters( - "t", windings="Dyn11", uhv=20000, ulv=400, sn=160 * 1e3, p0=460, i0=2.3 / 100, psc=2350, vsc=4 / 100 + "t", type="Dyn11", uhv=20000, ulv=400, sn=160 * 1e3, p0=460, i0=2.3 / 100, psc=2350, vsc=4 / 100 ) t = Transformer("transfo", bus2, bus3, parameters=tp) with pytest.raises(RoseauLoadFlowException) as e: diff --git a/roseau/load_flow/utils/__init__.py b/roseau/load_flow/utils/__init__.py index 3dcc783c..b13638af 100644 --- a/roseau/load_flow/utils/__init__.py +++ b/roseau/load_flow/utils/__init__.py @@ -3,7 +3,7 @@ """ from roseau.load_flow.utils.constants import CX, DELTA_P, EPSILON_0, EPSILON_R, MU_0, MU_R, OMEGA, PI, RHO, TAN_D, F from roseau.load_flow.utils.mixins import Identifiable, JsonMixin -from roseau.load_flow.utils.types import BranchType, ConductorType, InsulationType, LineModel, LineType, TransformerType +from roseau.load_flow.utils.types import BranchType, ConductorType, InsulationType, LineModel, LineType __all__ = [ # Constants @@ -27,5 +27,4 @@ "ConductorType", "InsulationType", "BranchType", - "TransformerType", ] diff --git a/roseau/load_flow/utils/tests/test_types.py b/roseau/load_flow/utils/tests/test_types.py index 3fba5548..0222fa85 100644 --- a/roseau/load_flow/utils/tests/test_types.py +++ b/roseau/load_flow/utils/tests/test_types.py @@ -1,14 +1,13 @@ import pytest from roseau.load_flow.exceptions import RoseauLoadFlowException, RoseauLoadFlowExceptionCode -from roseau.load_flow.utils.types import ConductorType, InsulationType, LineModel, LineType, TransformerType +from roseau.load_flow.utils.types import ConductorType, InsulationType, LineModel, LineType TYPES = [ ConductorType, InsulationType, LineModel, LineType, - TransformerType, ] TYPES_IDS = [x.__name__ for x in TYPES] @@ -54,78 +53,6 @@ def test_conductor_type(): assert e.value.args[1] == RoseauLoadFlowExceptionCode.BAD_CONDUCTOR_TYPE -def test_transformer_type(): - valid_windings = ("y", "yn", "z", "zn", "d") - valid_phase_displacements = (0, 5, 6, 11) - valid_types = {"dd", "yy", "yny", "yyn", "ynyn", "dz", "dzn", "dy", "dyn", "yd", "ynd", "yz", "ynz", "yzn", "ynzn"} - valid_full_types = { - "dd0", - "dd6", - "yy0", - "yy6", - "yny0", - "yny6", - "yyn0", - "yyn6", - "ynyn0", - "ynyn6", - "dz0", - "dz6", - "dzn0", - "dzn6", - "dy5", - "dy11", - "dyn5", - "dyn11", - "yd5", - "yd11", - "ynd5", - "ynd11", - "yz5", - "yz11", - "ynz5", - "ynz11", - "yzn5", - "yzn11", - "ynzn5", - "ynzn11", - } - - for winding1 in valid_windings: - for winding2 in valid_windings: - t = f"{winding1}{winding2}" - if t in valid_types: - assert not TransformerType.validate_windings(t) - w1, w2, p = TransformerType.extract_windings(t) - assert w1 == winding1.upper() - assert w2 == winding2 - assert p is None - for phase_displacement in valid_phase_displacements: - t = f"{winding1}{winding2}{phase_displacement}" - if t in valid_full_types: - assert TransformerType.validate_windings(t) - w1, w2, p = TransformerType.extract_windings(t) - assert w1 == winding1.upper() - assert w2 == winding2 - assert p == phase_displacement - else: - assert not TransformerType.validate_windings(t) - with pytest.raises(RoseauLoadFlowException) as e: - TransformerType.extract_windings(t) - assert e.value.args[1] == RoseauLoadFlowExceptionCode.BAD_TRANSFORMER_WINDINGS - else: - assert not TransformerType.validate_windings(t) - with pytest.raises(RoseauLoadFlowException): - TransformerType.extract_windings(t) - assert e.value.args[1] == RoseauLoadFlowExceptionCode.BAD_TRANSFORMER_WINDINGS - - for x in TransformerType: - s = str(x) - w1, w2, phase_displacement = TransformerType.extract_windings(s) - assert f"{w1}{w2}" == s - assert phase_displacement is None - - def test_line_model(): assert LineModel.from_string("") == LineModel.UNKNOWN assert LineModel.from_string("nan") == LineModel.UNKNOWN diff --git a/roseau/load_flow/utils/types.py b/roseau/load_flow/utils/types.py index 9c298c18..0d014f93 100644 --- a/roseau/load_flow/utils/types.py +++ b/roseau/load_flow/utils/types.py @@ -1,8 +1,6 @@ import logging from enum import Enum, auto, unique -from typing import Optional -import regex from typing_extensions import Self from roseau.load_flow.exceptions import RoseauLoadFlowException, RoseauLoadFlowExceptionCode @@ -344,132 +342,3 @@ def from_string(cls, string: str) -> Self: msg = f"The string {string!r} cannot be converted into a BranchType." logger.error(msg) raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_BRANCH_TYPE) - - -EXTRACT_WINDINGS_RE = regex.compile( - "(?(DEFINE)(?Pyn?)(?Pd)(?Pzn?)(?P[06])" - "(?P5|11))" - "" - "(?|(?P(?&y_winding))(?P(?&y_winding))(?P

(?&p_set_1)?)" # yy - "|(?P(?&y_winding))(?P(?&d_winding))(?P

(?&p_set_2)?)" # yd - "|(?P(?&y_winding))(?P(?&z_winding))(?P

(?&p_set_2)?)" # yz - "|(?P(?&d_winding))(?P(?&z_winding))(?P

(?&p_set_1)?)" # dz - "|(?P(?&d_winding))(?P(?&y_winding))(?P

(?&p_set_2)?)" # dy - "|(?P(?&d_winding))(?P(?&d_winding))(?P

(?&p_set_1)?))", # dd - regex.IGNORECASE, -) -"""The pattern to extract the winding of the primary and of the secondary of the transformer.""" - - -@unique -class TransformerType(Enum): - """The type of transformer.""" - - Yy = auto() - """A Wye-Wye transformer without neutral connected to the rest of the network.""" - YNy = auto() - """A Wye-Wye transformer with a neutral connected to the network on the first winding.""" - YNyn = auto() - """A Wye-Wye transformer with a neutral connected to the network on the two windings.""" - Yyn = auto() - """A Wye-Wye transformer with a neutral connected to the network on the second winding.""" - Dz = auto() - """A Delta-Zigzag transformer without neutral connected to the rest of the network.""" - Dzn = auto() - """A Delta-Zigzag transformer with a neutral connected to the network on the second winding.""" - Dy = auto() - """A Delta-Wye transformer without neutral connected to the rest of the network.""" - Dyn = auto() - """A Delta-Wye transformer with a neutral connected to the network on the second winding.""" - Yz = auto() - """A Wye-Zigzag transformer without neutral connected to the rest of the network.""" - YNz = auto() - """A Wye-Zigzag transformer with a neutral connected to the network on the first winding.""" - YNzn = auto() - """A Wye-Zigzag transformer with a neutral connected to the network on the two windings.""" - Yzn = auto() - """A Wye-Zigzag transformer with a neutral connected to the network on the second winding.""" - Yd = auto() - """A Wye-Delta transformer without neutral connected to the rest of the network.""" - YNd = auto() - """A Wye-Delta transformer with a neutral connected to the network on the first winding.""" - Dd = auto() - """A Delta-Delta transformer without neutral connected to the rest of the network.""" - - def __str__(self) -> str: - """Print a `TransformerType` - - Returns: - A printable string of the transformer type. - """ - return self.name - - @classmethod - def from_string(cls, string: str) -> Self: - """Convert a string into a TransformerType - - Args: - string: - The string to convert - - Returns: - The corresponding TransformerType - """ - winding1, winding2, phase_displacement = cls.extract_windings(string=string) - try: - return getattr(cls, f"{winding1}{winding2}") - except AttributeError: - msg = f"The string {string!r} cannot be converted into a TransformerType." - logger.error(msg) - raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_TRANSFORMER_TYPE) from None - - @property - def windings(self) -> tuple[str, str]: - """Retrieve the windings and the phase displacement of the current - - Returns: - The high voltages winding and the low voltages winding. - """ - winding1, winding2, phase_displacement = self.extract_windings(str(self)) - return winding1, winding2 - - @classmethod - def validate_windings(cls, string: str) -> bool: - """Validate the windings of the high and low voltages sides - - Args: - string: - A string depicting a winding - - Returns: - True if the provided string corresponds to valid transformer windings. - """ - try: - match = EXTRACT_WINDINGS_RE.fullmatch(string=string) - return bool(match) and bool(match.group("p")) - except Exception: - return False - - @classmethod - def extract_windings(cls, string: str) -> tuple[str, str, Optional[int]]: - """Extract the windings of the high and low voltages sides - - Args: - string: - A string depicting a winding - - Returns: - The high voltages winding, the low voltages winding, and the phase displacement. - """ - match = EXTRACT_WINDINGS_RE.fullmatch(string=string) - if match: - groups = match.groupdict() - winding1, winding2, phase_displacement = groups["w1"], groups["w2"], groups["p"] - if phase_displacement: - return winding1.upper(), winding2.lower(), int(phase_displacement) - else: - return winding1.upper(), winding2.lower(), None - else: - msg = f"Transformer windings cannot be extracted from the string {string!r}." - logger.error(msg) - raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_TRANSFORMER_WINDINGS)