Skip to content

Commit

Permalink
Make violations of lines and buses per-phase (#307)
Browse files Browse the repository at this point in the history
Closes #296

This makes it easier to check for an overloaded neutral or to check
which phase has overvoltage for example. It also makes the dataframe
results dataframe consistent with the results of the elements.
  • Loading branch information
alihamdan authored Jan 3, 2025
1 parent 9118949 commit 6e558f6
Show file tree
Hide file tree
Showing 9 changed files with 61 additions and 49 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}`307` {gh-issue}`296` Make `line.res_violated` and `bus.res_violated` return a boolean array
indicating if the corresponding phase is violated. This is consistent with the dataframe results
`en.res_lines` and `en.res_buses_voltages`. For old behavior, use `line_or_bus.res_violated.any()`.
- {gh-pr}`305` Add missing `tap` column to `en.transformers_frame`.
- {gh-pr}`305` Add `element_type` column to `en.potential_refs_frame` to indicate if the potential
reference is connected to a bus or a ground.
Expand Down
8 changes: 4 additions & 4 deletions doc/usage/Getting_Started.md
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,7 @@ Below are the results of the load flow for `en`, rounded to 2 decimal places:
>>> en.res_transformers # empty as the network does not contain transformers
```

[//]: # "TODO"
<!-- TODO -->

| transformer_id | phase | current1 | current2 | power1 | power2 | potential1 | potential2 | violated | loading | max_loading | sn |
| :------------- | :---- | -------: | -------: | -----: | -----: | ---------: | ---------: | :------- | ------: | ----------: | --: |
Expand Down Expand Up @@ -449,7 +449,7 @@ to magnitude and angle values (radians).
| lb | bn | 221.928 | -2.0944 |
| lb | cn | 221.928 | 2.0944 |

Or, if you prefer degrees:
Or, if you prefer the angles in degrees:

```pycon
>>> import functools as ft
Expand Down Expand Up @@ -481,7 +481,7 @@ not violated.

```pycon
>>> load_bus.res_violated
False
array([False, False, False])
```

Similarly, if you set `ampacities` on a line parameters and `max_loading` (default 100% of the ampacity) on a line, the
Expand All @@ -492,7 +492,7 @@ loading of the line in any phase exceeds the limit. Here, the current limit is n
>>> line.res_loading
<Quantity([0.09012, 0.09012, 0.09012, 0.], 'dimensionless')>
>>> line.res_violated
False
array([False, False, False, False])
```

The maximal loading of the transformer can be defined using the `max_loading` argument of the
Expand Down
18 changes: 10 additions & 8 deletions roseau/load_flow/models/buses.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from roseau.load_flow.exceptions import RoseauLoadFlowException, RoseauLoadFlowExceptionCode
from roseau.load_flow.models.core import Element
from roseau.load_flow.sym import phasor_to_sym
from roseau.load_flow.typing import ComplexArray, ComplexArrayLike1D, FloatArray, Id, JsonDict
from roseau.load_flow.typing import BoolArray, ComplexArray, ComplexArrayLike1D, FloatArray, Id, JsonDict
from roseau.load_flow.units import Q_, ureg_wraps
from roseau.load_flow.utils import find_stack_level
from roseau.load_flow_engine.cy_engine import CyBus
Expand Down Expand Up @@ -294,7 +294,7 @@ def max_voltage(self) -> Q_[float] | None:
)

@property
def res_violated(self) -> bool | None:
def res_violated(self) -> BoolArray | None:
"""Whether the bus has voltage limits violations.
Returns ``None`` if the bus has no voltage limits are not set.
Expand All @@ -303,13 +303,15 @@ def res_violated(self) -> bool | None:
u_max = self._max_voltage_level
if u_min is None and u_max is None:
return None
u_nom = self._nominal_voltage
if u_nom is None:
return None
voltage_levels = self._res_voltage_levels_getter(warning=True)
return (u_min is not None and bool(min(voltage_levels) < u_min)) or (
u_max is not None and bool(max(voltage_levels) > u_max)
)
if voltage_levels is None:
return None
violated = np.full_like(voltage_levels, fill_value=False, dtype=np.bool_)
if u_min is not None:
violated |= voltage_levels < u_min
if u_max is not None:
violated |= voltage_levels > u_max
return violated

def propagate_limits(self, force: bool = False) -> None:
"""Propagate the voltage limits to galvanically connected buses.
Expand Down
6 changes: 3 additions & 3 deletions roseau/load_flow/models/lines/lines.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from roseau.load_flow.models.buses import Bus
from roseau.load_flow.models.grounds import Ground
from roseau.load_flow.models.lines.parameters import LineParameters
from roseau.load_flow.typing import ComplexArray, FloatArray, Id, JsonDict
from roseau.load_flow.typing import BoolArray, ComplexArray, FloatArray, Id, JsonDict
from roseau.load_flow.units import Q_, ureg_wraps
from roseau.load_flow_engine.cy_engine import CyShuntLine, CySimplifiedLine

Expand Down Expand Up @@ -356,13 +356,13 @@ def res_loading(self) -> Q_[FloatArray] | None:
return None if loading is None else Q_(loading, "")

@property
def res_violated(self) -> bool | None:
def res_violated(self) -> BoolArray | None:
"""Whether the line current loading exceeds its maximal loading.
Returns ``None`` if the ``self.parameters.ampacities`` is not set.
"""
loading = self._res_loading_getter(warning=True)
return None if loading is None else bool((loading > self._max_loading).any())
return None if loading is None else (loading > self._max_loading)

#
# Json Mixin interface
Expand Down
2 changes: 1 addition & 1 deletion roseau/load_flow/models/lines/parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ def __init__(
from the catalogue.
materials:
The types of the conductor material (Aluminum, Copper, ...). The materials are
The types of the conductors material (Aluminum, Copper, ...). The materials are
optional, they are informative only and are not used in the load flow. This field gets
automatically filled when the line parameters are created from a geometric model or
from the catalogue.
Expand Down
37 changes: 21 additions & 16 deletions roseau/load_flow/models/tests/test_buses.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
Ground,
Line,
LineParameters,
PositiveSequence,
PotentialRef,
PowerLoad,
RoseauLoadFlowException,
Expand Down Expand Up @@ -214,31 +215,29 @@ def test_voltage_limits(recwarn):
def test_res_voltages():
# With a neutral
bus = Bus(id="bus", phases="abcn")
direct_seq = np.exp([0, -2 / 3 * np.pi * 1j, 2 / 3 * np.pi * 1j])
direct_seq_neutral = np.array([1, np.exp(-2 / 3 * np.pi * 1j), np.exp(2 / 3 * np.pi * 1j), 0])
direct_seq_neutral = np.array([*PositiveSequence, 0])
bus._res_potentials = (230 + 0j) * direct_seq_neutral

assert np.allclose(bus.res_potentials.m, (230 + 0j) * direct_seq_neutral)
assert np.allclose(bus.res_voltages.m, (230 + 0j) * direct_seq)
assert np.allclose(bus.res_voltages.m, (230 + 0j) * PositiveSequence)
assert bus.res_voltage_levels is None
bus.nominal_voltage = 400 # V
assert np.allclose(bus.res_voltage_levels.m, 230 / 400 * np.sqrt(3))

# Without a neutral
bus = Bus(id="bus", phases="abc")
bus._res_potentials = (20_000 + 0j) * direct_seq / np.sqrt(3)
bus._res_potentials = (20e3 + 0j) * PositiveSequence / np.sqrt(3)

assert np.allclose(bus.res_potentials.m, (20_000 + 0j) * direct_seq / np.sqrt(3))
assert np.allclose(bus.res_voltages.m, (20_000 + 0j) * direct_seq * np.exp(np.pi * 1j / 6))
assert np.allclose(bus.res_potentials.m, (20e3 + 0j) * PositiveSequence / np.sqrt(3))
assert np.allclose(bus.res_voltages.m, (20e3 + 0j) * PositiveSequence * np.exp(np.pi * 1j / 6))
assert bus.res_voltage_levels is None
bus.nominal_voltage = 20_000 # V
bus.nominal_voltage = 20e3 # V
assert np.allclose(bus.res_voltage_levels.m, 1.0)


def test_res_violated():
bus = Bus(id="bus", phases="abc")
direct_seq = np.exp([0, -2 / 3 * np.pi * 1j, 2 / 3 * np.pi * 1j])
bus._res_potentials = (230 + 0j) * direct_seq
bus._res_potentials = (230 + 0j) * PositiveSequence

# No limits
assert bus.res_violated is None
Expand All @@ -249,29 +248,35 @@ def test_res_violated():

# Only min voltage
bus.min_voltage_level = 0.9
assert bus.res_violated is False
assert (bus.res_violated == [False, False, False]).all()
bus.min_voltage_level = 1.1
assert bus.res_violated is True
assert (bus.res_violated == [True, True, True]).all()

# Only max voltage
bus.min_voltage_level = None
bus.max_voltage_level = 1.1
assert bus.res_violated is False
assert (bus.res_violated == [False, False, False]).all()
bus.max_voltage_level = 0.9
assert bus.res_violated is True
assert (bus.res_violated == [True, True, True]).all()

# Both min and max voltage
# min <= v <= max
bus.min_voltage_level = 0.9
bus.max_voltage_level = 1.1
assert bus.res_violated is False
assert (bus.res_violated == [False, False, False]).all()
# v < min
bus.min_voltage_level = 1.1
assert bus.res_violated is True
assert (bus.res_violated == [True, True, True]).all()
# v > max
bus.min_voltage_level = 0.9
bus.max_voltage_level = 0.9
assert bus.res_violated is True
assert (bus.res_violated == [True, True, True]).all()

# Not all phases are violated
bus.min_voltage_level = 0.9
bus.max_voltage_level = 1.1
bus._res_potentials[0] = 300 + 0j
assert (bus.res_violated == [True, False, True]).all()


def test_propagate_limits(): # noqa: C901
Expand Down
32 changes: 16 additions & 16 deletions roseau/load_flow/models/tests/test_lines.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,39 +162,39 @@ def test_res_violated():

# No constraint violated
lp.ampacities = 11
assert line.res_violated is False
assert (line.res_violated == [False, False, False]).all()
np.testing.assert_allclose(line.res_loading.m, 10 / 11)

# Reduced max_loading
line.max_loading = Q_(50, "%")
assert line.max_loading.m == 0.5
assert line.res_violated is True
assert (line.res_violated == [True, True, True]).all()
np.testing.assert_allclose(line.res_loading.m, 10 / 11)

# Two violations
# Two sides violations
lp.ampacities = 9
line.max_loading = 1
assert line.res_violated is True
assert (line.res_violated == [True, True, True]).all()
np.testing.assert_allclose(line.res_loading.m, 10 / 9)

# Side 1 violation
lp.ampacities = 11
line._res_currents = 12 * PosSeq, -10 * PosSeq
assert line.res_violated is True
assert (line.res_violated == [True, True, True]).all()
np.testing.assert_allclose(line.res_loading.m, 12 / 11)

# Side 2 violation
lp.ampacities = 11
line._res_currents = 10 * PosSeq, -12 * PosSeq
assert line.res_violated is True
assert (line.res_violated == [True, True, True]).all()
np.testing.assert_allclose(line.res_loading.m, 12 / 11)

# A single phase violation
lp.ampacities = 11
line._res_currents = 10 * PosSeq, -10 * PosSeq
line._res_currents[0][0] = 12 * PosSeq[0]
line._res_currents[1][0] = -12 * PosSeq[0]
assert line.res_violated is True
line._res_currents[0][0] = 12
line._res_currents[1][0] = -12
assert (line.res_violated == [True, False, False]).all()
np.testing.assert_allclose(line.res_loading.m, [12 / 11, 10 / 11, 10 / 11])

#
Expand All @@ -205,24 +205,24 @@ def test_res_violated():
# No constraint violated
lp.ampacities = [11, 12, 13]
line.max_loading = 1
assert line.res_violated is False
assert (line.res_violated == [False, False, False]).all()
np.testing.assert_allclose(line.res_loading.m, [10 / 11, 10 / 12, 10 / 13])

# Two violations
# Two sides violations
lp.ampacities = [9, 9, 12]
assert line.res_violated is True
assert (line.res_violated == [True, True, False]).all()
np.testing.assert_allclose(line.res_loading.m, [10 / 9, 10 / 9, 10 / 12])

# Side 1 violation
lp.ampacities = [11, 10, 9]
lp.ampacities = [11, 13, 11]
line._res_currents = 12 * PosSeq, -10 * PosSeq
assert line.res_violated is True
np.testing.assert_allclose(line.res_loading.m, [12 / 11, 12 / 10, 12 / 9])
assert (line.res_violated == [True, False, True]).all()
np.testing.assert_allclose(line.res_loading.m, [12 / 11, 12 / 13, 12 / 11])

# Side 2 violation
lp.ampacities = [11, 11, 13]
line._res_currents = 10 * PosSeq, -12 * PosSeq
assert line.res_violated is True
assert (line.res_violated == [True, True, False]).all()
np.testing.assert_allclose(line.res_loading.m, [12 / 11, 12 / 11, 12 / 13])


Expand Down
2 changes: 2 additions & 0 deletions roseau/load_flow/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
MapOrSeq: TypeAlias = Mapping[int, T] | Mapping[str, T] | Mapping[Id, T] | Sequence[T]
ComplexArray: TypeAlias = NDArray[np.complex128]
FloatArray: TypeAlias = NDArray[np.float64]
BoolArray: TypeAlias = NDArray[np.bool_]
QtyOrMag: TypeAlias = Q_[T] | T

Int: TypeAlias = int | np.integer[Any]
Expand All @@ -99,6 +100,7 @@
"ProjectionType",
"Solver",
"MapOrSeq",
"BoolArray",
"FloatArray",
"ComplexArray",
"ComplexArrayLike1D",
Expand Down
2 changes: 1 addition & 1 deletion roseau/load_flow/utils/doc_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ def to_markdown(df: pd.DataFrame, *, floatfmt: str = "g", index: bool = True, no
):
colalign.append("right")
if is_complex_dtype:
df[c] = df[c].apply(lambda x: f"{x.real:{floatfmt}}{x.imag:+{floatfmt}}")
df[c] = df[c].apply(lambda x: f"{x.real:{floatfmt}}{x.imag:+{floatfmt}}j")
else:
colalign.append("left")

Expand Down

0 comments on commit 6e558f6

Please sign in to comment.