Skip to content

Commit 6e558f6

Browse files
authored
Make violations of lines and buses per-phase (#307)
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.
1 parent 9118949 commit 6e558f6

File tree

9 files changed

+61
-49
lines changed

9 files changed

+61
-49
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}`307` {gh-issue}`296` Make `line.res_violated` and `bus.res_violated` return a boolean array
23+
indicating if the corresponding phase is violated. This is consistent with the dataframe results
24+
`en.res_lines` and `en.res_buses_voltages`. For old behavior, use `line_or_bus.res_violated.any()`.
2225
- {gh-pr}`305` Add missing `tap` column to `en.transformers_frame`.
2326
- {gh-pr}`305` Add `element_type` column to `en.potential_refs_frame` to indicate if the potential
2427
reference is connected to a bus or a ground.

doc/usage/Getting_Started.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,7 @@ Below are the results of the load flow for `en`, rounded to 2 decimal places:
373373
>>> en.res_transformers # empty as the network does not contain transformers
374374
```
375375

376-
[//]: # "TODO"
376+
<!-- TODO -->
377377

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

452-
Or, if you prefer degrees:
452+
Or, if you prefer the angles in degrees:
453453

454454
```pycon
455455
>>> import functools as ft
@@ -481,7 +481,7 @@ not violated.
481481

482482
```pycon
483483
>>> load_bus.res_violated
484-
False
484+
array([False, False, False])
485485
```
486486

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

498498
The maximal loading of the transformer can be defined using the `max_loading` argument of the

roseau/load_flow/models/buses.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from roseau.load_flow.exceptions import RoseauLoadFlowException, RoseauLoadFlowExceptionCode
1515
from roseau.load_flow.models.core import Element
1616
from roseau.load_flow.sym import phasor_to_sym
17-
from roseau.load_flow.typing import ComplexArray, ComplexArrayLike1D, FloatArray, Id, JsonDict
17+
from roseau.load_flow.typing import BoolArray, ComplexArray, ComplexArrayLike1D, FloatArray, Id, JsonDict
1818
from roseau.load_flow.units import Q_, ureg_wraps
1919
from roseau.load_flow.utils import find_stack_level
2020
from roseau.load_flow_engine.cy_engine import CyBus
@@ -294,7 +294,7 @@ def max_voltage(self) -> Q_[float] | None:
294294
)
295295

296296
@property
297-
def res_violated(self) -> bool | None:
297+
def res_violated(self) -> BoolArray | None:
298298
"""Whether the bus has voltage limits violations.
299299
300300
Returns ``None`` if the bus has no voltage limits are not set.
@@ -303,13 +303,15 @@ def res_violated(self) -> bool | None:
303303
u_max = self._max_voltage_level
304304
if u_min is None and u_max is None:
305305
return None
306-
u_nom = self._nominal_voltage
307-
if u_nom is None:
308-
return None
309306
voltage_levels = self._res_voltage_levels_getter(warning=True)
310-
return (u_min is not None and bool(min(voltage_levels) < u_min)) or (
311-
u_max is not None and bool(max(voltage_levels) > u_max)
312-
)
307+
if voltage_levels is None:
308+
return None
309+
violated = np.full_like(voltage_levels, fill_value=False, dtype=np.bool_)
310+
if u_min is not None:
311+
violated |= voltage_levels < u_min
312+
if u_max is not None:
313+
violated |= voltage_levels > u_max
314+
return violated
313315

314316
def propagate_limits(self, force: bool = False) -> None:
315317
"""Propagate the voltage limits to galvanically connected buses.

roseau/load_flow/models/lines/lines.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from roseau.load_flow.models.buses import Bus
1111
from roseau.load_flow.models.grounds import Ground
1212
from roseau.load_flow.models.lines.parameters import LineParameters
13-
from roseau.load_flow.typing import ComplexArray, FloatArray, Id, JsonDict
13+
from roseau.load_flow.typing import BoolArray, ComplexArray, FloatArray, Id, JsonDict
1414
from roseau.load_flow.units import Q_, ureg_wraps
1515
from roseau.load_flow_engine.cy_engine import CyShuntLine, CySimplifiedLine
1616

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

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

367367
#
368368
# Json Mixin interface

roseau/load_flow/models/lines/parameters.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ def __init__(
9999
from the catalogue.
100100
101101
materials:
102-
The types of the conductor material (Aluminum, Copper, ...). The materials are
102+
The types of the conductors material (Aluminum, Copper, ...). The materials are
103103
optional, they are informative only and are not used in the load flow. This field gets
104104
automatically filled when the line parameters are created from a geometric model or
105105
from the catalogue.

roseau/load_flow/models/tests/test_buses.py

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
Ground,
1010
Line,
1111
LineParameters,
12+
PositiveSequence,
1213
PotentialRef,
1314
PowerLoad,
1415
RoseauLoadFlowException,
@@ -214,31 +215,29 @@ def test_voltage_limits(recwarn):
214215
def test_res_voltages():
215216
# With a neutral
216217
bus = Bus(id="bus", phases="abcn")
217-
direct_seq = np.exp([0, -2 / 3 * np.pi * 1j, 2 / 3 * np.pi * 1j])
218-
direct_seq_neutral = np.array([1, np.exp(-2 / 3 * np.pi * 1j), np.exp(2 / 3 * np.pi * 1j), 0])
218+
direct_seq_neutral = np.array([*PositiveSequence, 0])
219219
bus._res_potentials = (230 + 0j) * direct_seq_neutral
220220

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

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

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

237237

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

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

250249
# Only min voltage
251250
bus.min_voltage_level = 0.9
252-
assert bus.res_violated is False
251+
assert (bus.res_violated == [False, False, False]).all()
253252
bus.min_voltage_level = 1.1
254-
assert bus.res_violated is True
253+
assert (bus.res_violated == [True, True, True]).all()
255254

256255
# Only max voltage
257256
bus.min_voltage_level = None
258257
bus.max_voltage_level = 1.1
259-
assert bus.res_violated is False
258+
assert (bus.res_violated == [False, False, False]).all()
260259
bus.max_voltage_level = 0.9
261-
assert bus.res_violated is True
260+
assert (bus.res_violated == [True, True, True]).all()
262261

263262
# Both min and max voltage
264263
# min <= v <= max
265264
bus.min_voltage_level = 0.9
266265
bus.max_voltage_level = 1.1
267-
assert bus.res_violated is False
266+
assert (bus.res_violated == [False, False, False]).all()
268267
# v < min
269268
bus.min_voltage_level = 1.1
270-
assert bus.res_violated is True
269+
assert (bus.res_violated == [True, True, True]).all()
271270
# v > max
272271
bus.min_voltage_level = 0.9
273272
bus.max_voltage_level = 0.9
274-
assert bus.res_violated is True
273+
assert (bus.res_violated == [True, True, True]).all()
274+
275+
# Not all phases are violated
276+
bus.min_voltage_level = 0.9
277+
bus.max_voltage_level = 1.1
278+
bus._res_potentials[0] = 300 + 0j
279+
assert (bus.res_violated == [True, False, True]).all()
275280

276281

277282
def test_propagate_limits(): # noqa: C901

roseau/load_flow/models/tests/test_lines.py

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -162,39 +162,39 @@ def test_res_violated():
162162

163163
# No constraint violated
164164
lp.ampacities = 11
165-
assert line.res_violated is False
165+
assert (line.res_violated == [False, False, False]).all()
166166
np.testing.assert_allclose(line.res_loading.m, 10 / 11)
167167

168168
# Reduced max_loading
169169
line.max_loading = Q_(50, "%")
170170
assert line.max_loading.m == 0.5
171-
assert line.res_violated is True
171+
assert (line.res_violated == [True, True, True]).all()
172172
np.testing.assert_allclose(line.res_loading.m, 10 / 11)
173173

174-
# Two violations
174+
# Two sides violations
175175
lp.ampacities = 9
176176
line.max_loading = 1
177-
assert line.res_violated is True
177+
assert (line.res_violated == [True, True, True]).all()
178178
np.testing.assert_allclose(line.res_loading.m, 10 / 9)
179179

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

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

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

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

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

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

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

228228

roseau/load_flow/typing.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
MapOrSeq: TypeAlias = Mapping[int, T] | Mapping[str, T] | Mapping[Id, T] | Sequence[T]
7575
ComplexArray: TypeAlias = NDArray[np.complex128]
7676
FloatArray: TypeAlias = NDArray[np.float64]
77+
BoolArray: TypeAlias = NDArray[np.bool_]
7778
QtyOrMag: TypeAlias = Q_[T] | T
7879

7980
Int: TypeAlias = int | np.integer[Any]
@@ -99,6 +100,7 @@
99100
"ProjectionType",
100101
"Solver",
101102
"MapOrSeq",
103+
"BoolArray",
102104
"FloatArray",
103105
"ComplexArray",
104106
"ComplexArrayLike1D",

roseau/load_flow/utils/doc_utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ def to_markdown(df: pd.DataFrame, *, floatfmt: str = "g", index: bool = True, no
6565
):
6666
colalign.append("right")
6767
if is_complex_dtype:
68-
df[c] = df[c].apply(lambda x: f"{x.real:{floatfmt}}{x.imag:+{floatfmt}}")
68+
df[c] = df[c].apply(lambda x: f"{x.real:{floatfmt}}{x.imag:+{floatfmt}}j")
6969
else:
7070
colalign.append("left")
7171

0 commit comments

Comments
 (0)