Skip to content

Commit

Permalink
Improve line enumeration types and fix errors in Coiffier's model (#311)
Browse files Browse the repository at this point in the history
- 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.
  • Loading branch information
alihamdan authored Jan 7, 2025
1 parent a7e8a61 commit 613c085
Show file tree
Hide file tree
Showing 9 changed files with 294 additions and 184 deletions.
3 changes: 3 additions & 0 deletions doc/Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ og:description: See what's new in the latest release of Roseau Load Flow !

## Unreleased

- {gh-pr}`311` Add French aliases to line enumeration types.
- {gh-pr}`311` 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.
- {gh-pr}`310` {gh-issue}`308` Support star and zig-zag windings with non-brought out neutral. In
earlier versions, vector groups like "Yd11" were considered identical to "YNd11".
- {gh-pr}`307` {gh-issue}`296` Make `line.res_violated` and `bus.res_violated` return a boolean array
Expand Down
23 changes: 16 additions & 7 deletions roseau/load_flow/io/dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
TransformerParameters,
VoltageSource,
)
from roseau.load_flow.types import Insulator, Material
from roseau.load_flow.typing import Id, JsonDict
from roseau.load_flow.utils import find_stack_level

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

# Rename `maximal_current` in `ampacities` and uses array
# Rename `section` in `sections` and uses array
# Rename `insulator_type` in `insulators` and uses array. `Unknown` is deleted
# Rename `material` in `materials` and uses array
# Rename `maximal_current` to `ampacities` and use array
# Rename `section` to `sections` and use array
# Rename `insulator_type` to `insulators` and use array. `Unknown` is deleted
# Rename `material` to `materials` and use array
old_lines_params = data.get("lines_params", [])
lines_params = []
for line_param_data in old_lines_params:
Expand Down Expand Up @@ -770,9 +771,8 @@ def v3_to_v4_converter(data: JsonDict) -> JsonDict:
for tr in data["transformers"]:
tr_phases_per_params[tr["params_id"]].append((tr["phases1"], tr["phases2"]))

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

line_params = []
for line_param_data in data["lines_params"]:
# Normalize the insulator and material types
if (materials := line_param_data.pop("materials", None)) is not None:
line_param_data["materials"] = [Material(material).name for material in materials]
if (insulators := line_param_data.pop("insulators", None)) is not None:
line_param_data["insulators"] = [Insulator(insulator).name for insulator in insulators]
line_params.append(line_param_data)

results = {
"version": 4,
"is_multiphase": data["is_multiphase"], # Unchanged
Expand All @@ -794,7 +803,7 @@ def v3_to_v4_converter(data: JsonDict) -> JsonDict:
"transformers": data["transformers"], # <---- Unchanged
"loads": data["loads"], # Unchanged
"sources": data["sources"], # Unchanged
"lines_params": data["lines_params"], # <---- Unchanged
"lines_params": line_params, # <---- Changed
"transformers_params": transformer_params, # <---- Changed
}
if "short_circuits" in data:
Expand Down
2 changes: 1 addition & 1 deletion roseau/load_flow/io/tests/test_dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ def test_to_dict():
lp_dict = res["lines_params"][0]
assert np.allclose(lp_dict["ampacities"], 1000)
assert lp_dict["line_type"] == "UNDERGROUND"
assert lp_dict["materials"] == ["AA"] * 4
assert lp_dict["materials"] == ["ACSR"] * 4
assert lp_dict["insulators"] == ["PVC"] * 4
assert np.allclose(lp_dict["sections"], 120)
assert "results" not in res_bus0
Expand Down
137 changes: 70 additions & 67 deletions roseau/load_flow/models/lines/parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,6 @@
class LineParameters(Identifiable, JsonMixin, CatalogueMixin[pd.DataFrame]):
"""Parameters that define electrical models of lines."""

_type_re = "|".join(x.code() for x in LineType)
_material_re = "|".join(x.code() for x in Material)
_insulator_re = "|".join(x.code() for x in Insulator)
_section_re = r"[1-9][0-9]*"
_REGEXP_LINE_TYPE_NAME = re.compile(
rf"^({_type_re})_({_material_re})_({_insulator_re}_)?{_section_re}$", flags=re.IGNORECASE
)

@ureg_wraps(None, (None, None, "ohm/km", "S/km", "A", None, None, None, "mm²"))
def __init__(
self,
Expand Down Expand Up @@ -142,11 +134,11 @@ def __init__(
def __repr__(self) -> str:
s = f"<{type(self).__name__}: id={self.id!r}"
if self._line_type is not None:
s += f", line_type={str(self._line_type)!r}"
s += f", line_type='{self._line_type!s}'"
if self._insulators is not None:
s += f", insulators={self._insulators}"
s += f", insulators='{self._insulators!s}'"
if self._materials is not None:
s += f", materials={self._materials}"
s += f", materials='{self._materials!s}'"
if self._sections is not None:
s += f", sections={self._sections}"
if self._ampacities is not None:
Expand Down Expand Up @@ -471,11 +463,11 @@ def from_geometry(
cls,
id: Id,
*,
line_type: LineType,
material: Material | None = None,
material_neutral: Material | None = None,
insulator: Insulator | None = None,
insulator_neutral: Insulator | None = None,
line_type: LineType | str,
material: Material | str | None = None,
material_neutral: Material | str | None = None,
insulator: Insulator | str | None = None,
insulator_neutral: Insulator | str | None = None,
section: float | Q_[float],
section_neutral: float | Q_[float] | None = None,
height: float | Q_[float],
Expand Down Expand Up @@ -565,11 +557,11 @@ def from_geometry(
def _from_geometry(
cls,
id: Id,
line_type: LineType,
material: Material | None,
material_neutral: Material | None,
insulator: Insulator | None,
insulator_neutral: Insulator | None,
line_type: LineType | str,
material: Material | str | None,
material_neutral: Material | str | None,
insulator: Insulator | str | None,
insulator_neutral: Insulator | str | None,
section: float,
section_neutral: float | None,
height: float,
Expand Down Expand Up @@ -623,23 +615,14 @@ def _from_geometry(
# dpn = data["dpn"] # Distance phase-to-neutral (m)
# dsh = data["dsh"] # Diameter of the sheath (mm)

# Normalize enumerations and fill optional values
line_type = LineType(line_type)
if material is None:
material = _DEFAULT_MATERIAL[line_type]
if insulator is None:
insulator = _DEFAULT_INSULATOR[line_type]
if material_neutral is None:
material_neutral = material
if insulator_neutral is None:
insulator_neutral = insulator
material = _DEFAULT_MATERIAL[line_type] if material is None else Material(material)
insulator = _DEFAULT_INSULATOR[line_type] if insulator is None else Insulator(insulator)
material_neutral = material if material_neutral is None else Material(material_neutral)
insulator_neutral = insulator if insulator_neutral is None else Insulator(insulator_neutral)
if section_neutral is None:
section_neutral = section
material = Material(material)
material_neutral = Material(material_neutral)
if insulator is not None:
insulator = Insulator(insulator)
if insulator_neutral is not None:
insulator_neutral = Insulator(insulator_neutral)

# Geometric configuration
coord, coord_prim, epsilon, epsilon_neutral = cls._get_geometric_configuration(
Expand Down Expand Up @@ -728,8 +711,8 @@ def _from_geometry(
@staticmethod
def _get_geometric_configuration(
line_type: LineType,
insulator: Insulator | None,
insulator_neutral: Insulator | None,
insulator: Insulator,
insulator_neutral: Insulator,
height: float,
external_diameter: float,
) -> tuple[FloatArray, FloatArray, float, float]:
Expand Down Expand Up @@ -762,7 +745,7 @@ def _get_geometric_configuration(
if line_type in (LineType.OVERHEAD, LineType.TWISTED):
# TODO This configuration is for twisted lines... Create a overhead configuration.
if height <= 0:
msg = f"The height of a '{line_type}' line must be a positive number."
msg = f"The height of '{line_type}' line must be a positive number."
logger.error(msg)
raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_LINE_MODEL)
x = SQRT3 * external_diameter / 8
Expand All @@ -786,7 +769,7 @@ def _get_geometric_configuration(
epsilon_neutral = EPSILON_0.m # TODO assume no insulator. Maybe valid for overhead but not for twisted...
elif line_type == LineType.UNDERGROUND:
if height >= 0:
msg = f"The height of a '{line_type}' line must be a negative number."
msg = f"The height of '{line_type}' line must be a negative number."
logger.error(msg)
raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_LINE_MODEL)
x = np.sqrt(2) * external_diameter / 8
Expand All @@ -796,9 +779,7 @@ def _get_geometric_configuration(
epsilon = (EPSILON_0 * EPSILON_R[insulator]).m
epsilon_neutral = (EPSILON_0 * EPSILON_R[insulator_neutral]).m
else:
msg = f"The line type {line_type!r} of the line {id!r} is unknown."
logger.error(msg)
raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_LINE_TYPE)
raise NotImplementedError(line_type) # unreachable

return coord, coord_prim, epsilon, epsilon_neutral

Expand All @@ -809,7 +790,8 @@ def from_coiffier_model(cls, name: str, nb_phases: int = 3, id: Id | None = None
Args:
name:
The canonical name of the line parameters. It must be in the format
`LineType_Material_CrossSection`. E.g. "U_AL_150".
`LineType_Material_CrossSection` (e.g. "S_AL_150") or
`LineType_Material_Insulator_CrossSection` (e.g. "S_AL_PE_150").
nb_phases:
The number of phases of the line between 1 and 4, defaults to 3. It represents the
Expand All @@ -823,30 +805,44 @@ def from_coiffier_model(cls, name: str, nb_phases: int = 3, id: Id | None = None
The corresponding line parameters.
"""
# Check the user input and retrieve enumerated types
m = re.match(
r"^(?P<line_type>[a-z]+)_(?P<material>[a-z]+)_(?:(?P<insulator>[a-z]+)_)?(?P<section>[1-9][0-9]*)$",
name,
flags=re.IGNORECASE,
)
try:
if cls._REGEXP_LINE_TYPE_NAME.fullmatch(string=name) is None:
if m is None:
raise AssertionError
line_type_s, material_s, section_s = name.split("_")
line_type = LineType(line_type_s)
material = Material(material_s)
section = Q_(float(section_s), "mm**2")
except Exception:
matches = m.groupdict()
line_type = LineType(matches["line_type"])
material = Material(matches["material"])
insulator = Insulator(matches["insulator"]) if matches["insulator"] is not None else None
section = Q_(float(matches["section"]), "mm**2")
except Exception as e:
msg = (
f"The Coiffier line parameter name {name!r} is not valid, expected format is "
"'LineType_Material_CrossSection'."
"'LineType_Material_CrossSection' or 'LineType_Material_Insulator_CrossSection'"
)
if m is not None:
msg += f": {e}"
logger.error(msg)
raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_TYPE_NAME_SYNTAX) from None

if insulator is not None:
# TODO: add insulator support
warnings.warn(
f"The insulator is currently ignored in the Coiffier model, got '{insulator.upper()}'.",
category=UserWarning,
stacklevel=find_stack_level(),
)
r = RHO[material] / section
if line_type == LineType.OVERHEAD:
c_b1 = Q_(50, "µF/km")
c_b2 = Q_(0, "µF/(km*mm**2)")
x = Q_(0.35, "ohm/km")
if material == Material.AA:
if section <= 50:
if section <= Q_(50, "mm**2"):
c_imax = 14.20
elif 50 < section <= 100:
elif section <= Q_(100, "mm**2"):
c_imax = 12.10
else:
c_imax = 15.70
Expand All @@ -855,14 +851,14 @@ def from_coiffier_model(cls, name: str, nb_phases: int = 3, id: Id | None = None
elif material == Material.CU:
c_imax = 21
elif material == Material.LA:
if section <= 50:
if section <= Q_(50, "mm**2"):
c_imax = 13.60
elif 50 < section <= 100:
elif section <= Q_(100, "mm**2"):
c_imax = 12.10
else:
c_imax = 15.60
else:
c_imax = 15.90
c_imax = 15.90 # pragma: no-cover # unreachable
elif line_type == LineType.TWISTED:
c_b1 = Q_(1750, "µF/km")
c_b2 = Q_(5, "µF/(km*mm**2)")
Expand All @@ -883,9 +879,7 @@ def from_coiffier_model(cls, name: str, nb_phases: int = 3, id: Id | None = None
c_imax = 16.5
x = Q_(0.1, "ohm/km")
else:
msg = f"The line type {line_type!r} of the line {name!r} is unknown."
logger.error(msg)
raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_LINE_TYPE)
raise NotImplementedError(line_type) # unreachable
b = (c_b1 + c_b2 * section) * 1e-4 * OMEGA
b = b.to("S/km")

Expand Down Expand Up @@ -1329,7 +1323,7 @@ def _get_catalogue(
)
try:
mask = enum_series == enum_class(value)
except RoseauLoadFlowException:
except ValueError:
mask = pd.Series(data=False, index=catalogue_data.index)
if raise_if_not_found and mask.sum() == 0:
cls._raise_not_found_in_catalogue(
Expand Down Expand Up @@ -1669,14 +1663,23 @@ def _check_matrix(self) -> None:

@staticmethod
def _check_enum_array(
value: _StrEnumType | Sequence[_StrEnumType] | None,
enum_class: type[_StrEnumType],
value: str | Sequence[str] | NDArray | None,
enum_class: type[StrEnum],
name: Literal["insulators", "materials"],
size: int,
) -> NDArray[_StrEnumType] | None:
) -> NDArray[np.object_] | None:
value_isna = pd.isna(value)

def convert(v):
try:
return enum_class(v)
except ValueError as e:
raise RoseauLoadFlowException(
msg=str(e), code=RoseauLoadFlowExceptionCode[f"BAD_{enum_class.__name__.upper()}"]
) from None

if np.isscalar(value_isna):
return None if value_isna else np.array([enum_class(value) for _ in range(size)], dtype=np.object_)
return None if value_isna else np.array([convert(value) for _ in range(size)], dtype=np.object_)
elif np.all(value_isna):
return None
else:
Expand All @@ -1686,9 +1689,9 @@ def _check_enum_array(
raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode[f"BAD_{name.upper()}_VALUE"])

# Build the numpy array fails with pd.NA inside
values = np.array([enum_class(v) for v in value], dtype=np.object_)
if len(value) != size:
msg = f"Incorrect number of {name}: {len(value)} instead of {size}."
values = np.array([convert(v) for v in value], dtype=np.object_)
if len(values) != size:
msg = f"Incorrect number of {name}: {len(values)} instead of {size}."
logger.error(msg)
raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode[f"BAD_{name.upper()}_SIZE"])
return values
Expand Down
Loading

0 comments on commit 613c085

Please sign in to comment.