Skip to content

Commit 613c085

Browse files
authored
Improve line enumeration types and fix errors in Coiffier's model (#311)
- Add French aliases to line enumeration types. - Fix `TypeError`s in the `LineParameters.from_coiffier_model`. The error message of invalid models now indicates whether the line type or the conductor material is invalid. - Add `TransformerCooling` enumeration. It is not currently used but the plan is to include it in the catalogue as this information is included in the catalogues we have.
1 parent a7e8a61 commit 613c085

File tree

9 files changed

+294
-184
lines changed

9 files changed

+294
-184
lines changed

doc/Changelog.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ og:description: See what's new in the latest release of Roseau Load Flow !
1919

2020
## Unreleased
2121

22+
- {gh-pr}`311` Add French aliases to line enumeration types.
23+
- {gh-pr}`311` Fix `TypeError`s in the `LineParameters.from_coiffier_model`. The error message of
24+
invalid models now indicates whether the line type or the conductor material is invalid.
2225
- {gh-pr}`310` {gh-issue}`308` Support star and zig-zag windings with non-brought out neutral. In
2326
earlier versions, vector groups like "Yd11" were considered identical to "YNd11".
2427
- {gh-pr}`307` {gh-issue}`296` Make `line.res_violated` and `bus.res_violated` return a boolean array

roseau/load_flow/io/dict.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
TransformerParameters,
2929
VoltageSource,
3030
)
31+
from roseau.load_flow.types import Insulator, Material
3132
from roseau.load_flow.typing import Id, JsonDict
3233
from roseau.load_flow.utils import find_stack_level
3334

@@ -700,10 +701,10 @@ def v2_to_v3_converter(data: JsonDict) -> JsonDict: # noqa: C901
700701
transformers_params_max_loading[transformer_param_data["id"]] = loading
701702
transformers_params.append(transformer_param_data)
702703

703-
# Rename `maximal_current` in `ampacities` and uses array
704-
# Rename `section` in `sections` and uses array
705-
# Rename `insulator_type` in `insulators` and uses array. `Unknown` is deleted
706-
# Rename `material` in `materials` and uses array
704+
# Rename `maximal_current` to `ampacities` and use array
705+
# Rename `section` to `sections` and use array
706+
# Rename `insulator_type` to `insulators` and use array. `Unknown` is deleted
707+
# Rename `material` to `materials` and use array
707708
old_lines_params = data.get("lines_params", [])
708709
lines_params = []
709710
for line_param_data in old_lines_params:
@@ -770,9 +771,8 @@ def v3_to_v4_converter(data: JsonDict) -> JsonDict:
770771
for tr in data["transformers"]:
771772
tr_phases_per_params[tr["params_id"]].append((tr["phases1"], tr["phases2"]))
772773

773-
old_transformer_params = data["transformers_params"]
774774
transformer_params = []
775-
for tp_data in old_transformer_params:
775+
for tp_data in data["transformers_params"]:
776776
w1, w2, clock = TransformerParameters.extract_windings(tp_data["vg"])
777777
# Handle brought out neutrals that were not declared as such
778778
if w1 in ("Y", "Z") and any(tr_phases[0] == "abcn" for tr_phases in tr_phases_per_params[tp_data["id"]]):
@@ -783,6 +783,15 @@ def v3_to_v4_converter(data: JsonDict) -> JsonDict:
783783
tp_data["vg"] = f"{w1}{w2}{clock}"
784784
transformer_params.append(tp_data)
785785

786+
line_params = []
787+
for line_param_data in data["lines_params"]:
788+
# Normalize the insulator and material types
789+
if (materials := line_param_data.pop("materials", None)) is not None:
790+
line_param_data["materials"] = [Material(material).name for material in materials]
791+
if (insulators := line_param_data.pop("insulators", None)) is not None:
792+
line_param_data["insulators"] = [Insulator(insulator).name for insulator in insulators]
793+
line_params.append(line_param_data)
794+
786795
results = {
787796
"version": 4,
788797
"is_multiphase": data["is_multiphase"], # Unchanged
@@ -794,7 +803,7 @@ def v3_to_v4_converter(data: JsonDict) -> JsonDict:
794803
"transformers": data["transformers"], # <---- Unchanged
795804
"loads": data["loads"], # Unchanged
796805
"sources": data["sources"], # Unchanged
797-
"lines_params": data["lines_params"], # <---- Unchanged
806+
"lines_params": line_params, # <---- Changed
798807
"transformers_params": transformer_params, # <---- Changed
799808
}
800809
if "short_circuits" in data:

roseau/load_flow/io/tests/test_dict.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ def test_to_dict():
139139
lp_dict = res["lines_params"][0]
140140
assert np.allclose(lp_dict["ampacities"], 1000)
141141
assert lp_dict["line_type"] == "UNDERGROUND"
142-
assert lp_dict["materials"] == ["AA"] * 4
142+
assert lp_dict["materials"] == ["ACSR"] * 4
143143
assert lp_dict["insulators"] == ["PVC"] * 4
144144
assert np.allclose(lp_dict["sections"], 120)
145145
assert "results" not in res_bus0

roseau/load_flow/models/lines/parameters.py

Lines changed: 70 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -50,14 +50,6 @@
5050
class LineParameters(Identifiable, JsonMixin, CatalogueMixin[pd.DataFrame]):
5151
"""Parameters that define electrical models of lines."""
5252

53-
_type_re = "|".join(x.code() for x in LineType)
54-
_material_re = "|".join(x.code() for x in Material)
55-
_insulator_re = "|".join(x.code() for x in Insulator)
56-
_section_re = r"[1-9][0-9]*"
57-
_REGEXP_LINE_TYPE_NAME = re.compile(
58-
rf"^({_type_re})_({_material_re})_({_insulator_re}_)?{_section_re}$", flags=re.IGNORECASE
59-
)
60-
6153
@ureg_wraps(None, (None, None, "ohm/km", "S/km", "A", None, None, None, "mm²"))
6254
def __init__(
6355
self,
@@ -142,11 +134,11 @@ def __init__(
142134
def __repr__(self) -> str:
143135
s = f"<{type(self).__name__}: id={self.id!r}"
144136
if self._line_type is not None:
145-
s += f", line_type={str(self._line_type)!r}"
137+
s += f", line_type='{self._line_type!s}'"
146138
if self._insulators is not None:
147-
s += f", insulators={self._insulators}"
139+
s += f", insulators='{self._insulators!s}'"
148140
if self._materials is not None:
149-
s += f", materials={self._materials}"
141+
s += f", materials='{self._materials!s}'"
150142
if self._sections is not None:
151143
s += f", sections={self._sections}"
152144
if self._ampacities is not None:
@@ -471,11 +463,11 @@ def from_geometry(
471463
cls,
472464
id: Id,
473465
*,
474-
line_type: LineType,
475-
material: Material | None = None,
476-
material_neutral: Material | None = None,
477-
insulator: Insulator | None = None,
478-
insulator_neutral: Insulator | None = None,
466+
line_type: LineType | str,
467+
material: Material | str | None = None,
468+
material_neutral: Material | str | None = None,
469+
insulator: Insulator | str | None = None,
470+
insulator_neutral: Insulator | str | None = None,
479471
section: float | Q_[float],
480472
section_neutral: float | Q_[float] | None = None,
481473
height: float | Q_[float],
@@ -565,11 +557,11 @@ def from_geometry(
565557
def _from_geometry(
566558
cls,
567559
id: Id,
568-
line_type: LineType,
569-
material: Material | None,
570-
material_neutral: Material | None,
571-
insulator: Insulator | None,
572-
insulator_neutral: Insulator | None,
560+
line_type: LineType | str,
561+
material: Material | str | None,
562+
material_neutral: Material | str | None,
563+
insulator: Insulator | str | None,
564+
insulator_neutral: Insulator | str | None,
573565
section: float,
574566
section_neutral: float | None,
575567
height: float,
@@ -623,23 +615,14 @@ def _from_geometry(
623615
# dpn = data["dpn"] # Distance phase-to-neutral (m)
624616
# dsh = data["dsh"] # Diameter of the sheath (mm)
625617

618+
# Normalize enumerations and fill optional values
626619
line_type = LineType(line_type)
627-
if material is None:
628-
material = _DEFAULT_MATERIAL[line_type]
629-
if insulator is None:
630-
insulator = _DEFAULT_INSULATOR[line_type]
631-
if material_neutral is None:
632-
material_neutral = material
633-
if insulator_neutral is None:
634-
insulator_neutral = insulator
620+
material = _DEFAULT_MATERIAL[line_type] if material is None else Material(material)
621+
insulator = _DEFAULT_INSULATOR[line_type] if insulator is None else Insulator(insulator)
622+
material_neutral = material if material_neutral is None else Material(material_neutral)
623+
insulator_neutral = insulator if insulator_neutral is None else Insulator(insulator_neutral)
635624
if section_neutral is None:
636625
section_neutral = section
637-
material = Material(material)
638-
material_neutral = Material(material_neutral)
639-
if insulator is not None:
640-
insulator = Insulator(insulator)
641-
if insulator_neutral is not None:
642-
insulator_neutral = Insulator(insulator_neutral)
643626

644627
# Geometric configuration
645628
coord, coord_prim, epsilon, epsilon_neutral = cls._get_geometric_configuration(
@@ -728,8 +711,8 @@ def _from_geometry(
728711
@staticmethod
729712
def _get_geometric_configuration(
730713
line_type: LineType,
731-
insulator: Insulator | None,
732-
insulator_neutral: Insulator | None,
714+
insulator: Insulator,
715+
insulator_neutral: Insulator,
733716
height: float,
734717
external_diameter: float,
735718
) -> tuple[FloatArray, FloatArray, float, float]:
@@ -762,7 +745,7 @@ def _get_geometric_configuration(
762745
if line_type in (LineType.OVERHEAD, LineType.TWISTED):
763746
# TODO This configuration is for twisted lines... Create a overhead configuration.
764747
if height <= 0:
765-
msg = f"The height of a '{line_type}' line must be a positive number."
748+
msg = f"The height of '{line_type}' line must be a positive number."
766749
logger.error(msg)
767750
raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_LINE_MODEL)
768751
x = SQRT3 * external_diameter / 8
@@ -786,7 +769,7 @@ def _get_geometric_configuration(
786769
epsilon_neutral = EPSILON_0.m # TODO assume no insulator. Maybe valid for overhead but not for twisted...
787770
elif line_type == LineType.UNDERGROUND:
788771
if height >= 0:
789-
msg = f"The height of a '{line_type}' line must be a negative number."
772+
msg = f"The height of '{line_type}' line must be a negative number."
790773
logger.error(msg)
791774
raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_LINE_MODEL)
792775
x = np.sqrt(2) * external_diameter / 8
@@ -796,9 +779,7 @@ def _get_geometric_configuration(
796779
epsilon = (EPSILON_0 * EPSILON_R[insulator]).m
797780
epsilon_neutral = (EPSILON_0 * EPSILON_R[insulator_neutral]).m
798781
else:
799-
msg = f"The line type {line_type!r} of the line {id!r} is unknown."
800-
logger.error(msg)
801-
raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_LINE_TYPE)
782+
raise NotImplementedError(line_type) # unreachable
802783

803784
return coord, coord_prim, epsilon, epsilon_neutral
804785

@@ -809,7 +790,8 @@ def from_coiffier_model(cls, name: str, nb_phases: int = 3, id: Id | None = None
809790
Args:
810791
name:
811792
The canonical name of the line parameters. It must be in the format
812-
`LineType_Material_CrossSection`. E.g. "U_AL_150".
793+
`LineType_Material_CrossSection` (e.g. "S_AL_150") or
794+
`LineType_Material_Insulator_CrossSection` (e.g. "S_AL_PE_150").
813795
814796
nb_phases:
815797
The number of phases of the line between 1 and 4, defaults to 3. It represents the
@@ -823,30 +805,44 @@ def from_coiffier_model(cls, name: str, nb_phases: int = 3, id: Id | None = None
823805
The corresponding line parameters.
824806
"""
825807
# Check the user input and retrieve enumerated types
808+
m = re.match(
809+
r"^(?P<line_type>[a-z]+)_(?P<material>[a-z]+)_(?:(?P<insulator>[a-z]+)_)?(?P<section>[1-9][0-9]*)$",
810+
name,
811+
flags=re.IGNORECASE,
812+
)
826813
try:
827-
if cls._REGEXP_LINE_TYPE_NAME.fullmatch(string=name) is None:
814+
if m is None:
828815
raise AssertionError
829-
line_type_s, material_s, section_s = name.split("_")
830-
line_type = LineType(line_type_s)
831-
material = Material(material_s)
832-
section = Q_(float(section_s), "mm**2")
833-
except Exception:
816+
matches = m.groupdict()
817+
line_type = LineType(matches["line_type"])
818+
material = Material(matches["material"])
819+
insulator = Insulator(matches["insulator"]) if matches["insulator"] is not None else None
820+
section = Q_(float(matches["section"]), "mm**2")
821+
except Exception as e:
834822
msg = (
835823
f"The Coiffier line parameter name {name!r} is not valid, expected format is "
836-
"'LineType_Material_CrossSection'."
824+
"'LineType_Material_CrossSection' or 'LineType_Material_Insulator_CrossSection'"
837825
)
826+
if m is not None:
827+
msg += f": {e}"
838828
logger.error(msg)
839829
raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_TYPE_NAME_SYNTAX) from None
840-
830+
if insulator is not None:
831+
# TODO: add insulator support
832+
warnings.warn(
833+
f"The insulator is currently ignored in the Coiffier model, got '{insulator.upper()}'.",
834+
category=UserWarning,
835+
stacklevel=find_stack_level(),
836+
)
841837
r = RHO[material] / section
842838
if line_type == LineType.OVERHEAD:
843839
c_b1 = Q_(50, "µF/km")
844840
c_b2 = Q_(0, "µF/(km*mm**2)")
845841
x = Q_(0.35, "ohm/km")
846842
if material == Material.AA:
847-
if section <= 50:
843+
if section <= Q_(50, "mm**2"):
848844
c_imax = 14.20
849-
elif 50 < section <= 100:
845+
elif section <= Q_(100, "mm**2"):
850846
c_imax = 12.10
851847
else:
852848
c_imax = 15.70
@@ -855,14 +851,14 @@ def from_coiffier_model(cls, name: str, nb_phases: int = 3, id: Id | None = None
855851
elif material == Material.CU:
856852
c_imax = 21
857853
elif material == Material.LA:
858-
if section <= 50:
854+
if section <= Q_(50, "mm**2"):
859855
c_imax = 13.60
860-
elif 50 < section <= 100:
856+
elif section <= Q_(100, "mm**2"):
861857
c_imax = 12.10
862858
else:
863859
c_imax = 15.60
864860
else:
865-
c_imax = 15.90
861+
c_imax = 15.90 # pragma: no-cover # unreachable
866862
elif line_type == LineType.TWISTED:
867863
c_b1 = Q_(1750, "µF/km")
868864
c_b2 = Q_(5, "µF/(km*mm**2)")
@@ -883,9 +879,7 @@ def from_coiffier_model(cls, name: str, nb_phases: int = 3, id: Id | None = None
883879
c_imax = 16.5
884880
x = Q_(0.1, "ohm/km")
885881
else:
886-
msg = f"The line type {line_type!r} of the line {name!r} is unknown."
887-
logger.error(msg)
888-
raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_LINE_TYPE)
882+
raise NotImplementedError(line_type) # unreachable
889883
b = (c_b1 + c_b2 * section) * 1e-4 * OMEGA
890884
b = b.to("S/km")
891885

@@ -1329,7 +1323,7 @@ def _get_catalogue(
13291323
)
13301324
try:
13311325
mask = enum_series == enum_class(value)
1332-
except RoseauLoadFlowException:
1326+
except ValueError:
13331327
mask = pd.Series(data=False, index=catalogue_data.index)
13341328
if raise_if_not_found and mask.sum() == 0:
13351329
cls._raise_not_found_in_catalogue(
@@ -1669,14 +1663,23 @@ def _check_matrix(self) -> None:
16691663

16701664
@staticmethod
16711665
def _check_enum_array(
1672-
value: _StrEnumType | Sequence[_StrEnumType] | None,
1673-
enum_class: type[_StrEnumType],
1666+
value: str | Sequence[str] | NDArray | None,
1667+
enum_class: type[StrEnum],
16741668
name: Literal["insulators", "materials"],
16751669
size: int,
1676-
) -> NDArray[_StrEnumType] | None:
1670+
) -> NDArray[np.object_] | None:
16771671
value_isna = pd.isna(value)
1672+
1673+
def convert(v):
1674+
try:
1675+
return enum_class(v)
1676+
except ValueError as e:
1677+
raise RoseauLoadFlowException(
1678+
msg=str(e), code=RoseauLoadFlowExceptionCode[f"BAD_{enum_class.__name__.upper()}"]
1679+
) from None
1680+
16781681
if np.isscalar(value_isna):
1679-
return None if value_isna else np.array([enum_class(value) for _ in range(size)], dtype=np.object_)
1682+
return None if value_isna else np.array([convert(value) for _ in range(size)], dtype=np.object_)
16801683
elif np.all(value_isna):
16811684
return None
16821685
else:
@@ -1686,9 +1689,9 @@ def _check_enum_array(
16861689
raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode[f"BAD_{name.upper()}_VALUE"])
16871690

16881691
# Build the numpy array fails with pd.NA inside
1689-
values = np.array([enum_class(v) for v in value], dtype=np.object_)
1690-
if len(value) != size:
1691-
msg = f"Incorrect number of {name}: {len(value)} instead of {size}."
1692+
values = np.array([convert(v) for v in value], dtype=np.object_)
1693+
if len(values) != size:
1694+
msg = f"Incorrect number of {name}: {len(values)} instead of {size}."
16921695
logger.error(msg)
16931696
raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode[f"BAD_{name.upper()}_SIZE"])
16941697
return values

0 commit comments

Comments
 (0)