Skip to content

Commit

Permalink
Add voltage unbalance calculation (#142)
Browse files Browse the repository at this point in the history
Closes #136

Add method `res_voltage_unbalance` to 3-phase buses that calculates the
voltage unbalance factor. Note that this is a method, not a property, so
that we can support more standards in the future by passing arguments to
this method.

Voltage unbalance does not make sense for non 3-phase buses thus we
raise an error with a helpful message.

I also fixed the symmetrical to phasor converters to preserve the shape
of the input array.
  • Loading branch information
alihamdan authored Oct 30, 2023
1 parent dcbaa65 commit 2857e43
Show file tree
Hide file tree
Showing 10 changed files with 123 additions and 21 deletions.
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ repos:
- id: blacken-docs
entry: bash -c "blacken-docs -l 90 $(find doc/ -name '*.md')"
args: [-l 90]
additional_dependencies: [black==23.9.1] # keep in sync with black above
additional_dependencies: [black==23.10.1] # keep in sync with black above
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v3.0.3
hooks:
Expand Down
3 changes: 2 additions & 1 deletion .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
{
"recommendations": [
"charliermarsh.ruff",
"esbenp.prettier-vscode",
"ms-python.black-formatter",
"ms-python.python",
"ms-python.vscode-pylance"
"ms-python.vscode-pylance",
],
"unwantedRecommendations": [
"ms-python.flake8", // We use ruff
Expand Down
6 changes: 6 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,10 @@
"source.organizeImports.ruff": "explicit",
},
},
// Prettier
"prettier.printWidth": 120,
"[markdown][yaml][html][css]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
}
}
11 changes: 11 additions & 0 deletions doc/Bibliography.bib
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,14 @@ @misc{wiki:Method_Of_Image_Charges
url = {http://en.wikipedia.org/w/index.php?title=Method\%20of\%20image\%20charges&oldid=1152888135},
note = "[Online; accessed 25-August-2023]"
}

@inproceedings{Girigoudar_2019,
author = {Girigoudar, Kshitij and Molzahn, Daniel K. and Roald, Line A.},
booktitle = {2019 North American Power Symposium (NAPS)},
title = {On The Relationships Among Different Voltage Unbalance Definitions},
year = {2019},
volume = {},
number = {},
pages = {1-6},
doi = {10.1109/NAPS46351.2019.9000231}
}
2 changes: 2 additions & 0 deletions doc/Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

**In development**

- {gh-pr}`142` {gh-issue}`136` Add `Bus.res_voltage_unbalance()` method to get the Voltage Unbalance
Factor (VUF) as defined by the IEC standard IEC 61000-3-14.
- {gh-pr}`141` {gh-issue}`137` Add `ElectricalNetwork.to_graph()` to get a `networkx.Graph` object
representing the electrical network for graph theory studies. Install with the `"graph"` extra to
get _networkx_.
Expand Down
24 changes: 24 additions & 0 deletions doc/usage/Extras.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,27 @@ the module documentation for more details.

An enumeration of available conductor types can be found in the {mod}`roseau.load_flow.utils.types`
module.

## Voltage unbalance

It is possible to calculate the voltage unbalance due to asymmetric operation. There are many
definitions of voltage unbalance (see {cite:p}`Girigoudar_2019`). In `roseau-load-flow`, you can
use the {meth}`~roseau.load_flow.models.Bus.res_voltage_unbalance` method on a 3-phase bus to get
the Voltage Unbalance Factor (VUF) as per the IEC definition:

```{math}
VUF = \frac{|V_n|}{|V_p|} * 100 (\%)
```

Where $V_n$ is the negative-sequence voltage and $V_p$ is the positive-sequence voltage.

```{note}
Other definitions of voltage unbalance could be added in the future. If you need a specific
definition, please open an issue on the GitHub repository.
```

## Bibliography

```{bibliography}
:filter: docname in docnames
```
12 changes: 8 additions & 4 deletions roseau/load_flow/converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,18 @@

def phasor_to_sym(v_abc: Sequence[complex]) -> np.ndarray[complex]:
"""Compute the symmetrical components `(0, +, -)` from the phasor components `(a, b, c)`."""
v_012 = _A_INV @ np.asarray(v_abc).reshape((3, 1))
return v_012
v_abc_array = np.asarray(v_abc)
orig_shape = v_abc_array.shape
v_012 = _A_INV @ v_abc_array.reshape((3, 1))
return v_012.reshape(orig_shape)


def sym_to_phasor(v_012: Sequence[complex]) -> np.ndarray[complex]:
"""Compute the phasor components `(a, b, c)` from the symmetrical components `(0, +, -)`."""
v_abc = A @ np.asarray(v_012).reshape((3, 1))
return v_abc
v_012_array = np.asarray(v_012)
orig_shape = v_012_array.shape
v_abc = A @ v_012_array.reshape((3, 1))
return v_abc.reshape(orig_shape)


def series_phasor_to_sym(s_abc: pd.Series) -> pd.Series:
Expand Down
25 changes: 24 additions & 1 deletion roseau/load_flow/models/buses.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from shapely import Point
from typing_extensions import Self

from roseau.load_flow.converters import calculate_voltage_phases, calculate_voltages
from roseau.load_flow.converters import calculate_voltage_phases, calculate_voltages, phasor_to_sym
from roseau.load_flow.exceptions import RoseauLoadFlowException, RoseauLoadFlowExceptionCode
from roseau.load_flow.models.core import Element
from roseau.load_flow.typing import Id, JsonDict
Expand Down Expand Up @@ -279,6 +279,29 @@ def get_connected_buses(self) -> Iterator[Id]:
to_add = set(element._connected_elements).difference(visited)
remaining.update(to_add)

@ureg_wraps("percent", (None,), strict=False)
def res_voltage_unbalance(self) -> Q_[float]:
"""Calculate the voltage unbalance on this bus according to the IEC definition.
Voltage Unbalance Factor:
:math:`VUF = \\frac{|V_n|}{|V_p|} * 100 (\\%)`
Where :math:`V_n` is the negative-sequence voltage and :math:`V_p` is the positive-sequence
voltage.
"""
# https://std.iec.ch/terms/terms.nsf/3385f156e728849bc1256e8c00278ad2/771c5188e62fade5c125793a0043f2a5?OpenDocument
if self.phases not in {"abc", "abcn"}:
msg = f"Voltage unbalance is only available for 3-phases buses, bus {self.id!r} has phases {self.phases!r}"
logger.error(msg)
raise RoseauLoadFlowException(msg, code=RoseauLoadFlowExceptionCode.BAD_PHASE)
# We use the potentials here which is equivalent to using the "line to neutral" voltages as
# defined by the standard. The standard also has this note:
# NOTE 1 Phase-to-phase voltages may also be used instead of line to neutral voltages.
potentials = self._res_potentials_getter(warning=True)
_, vp, vn = phasor_to_sym(potentials[:3]) # (0, +, -)
return abs(vn) / abs(vp) * 100

#
# Json Mixin interface
#
Expand Down
31 changes: 31 additions & 0 deletions roseau/load_flow/models/tests/test_buses.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,3 +324,34 @@ def test_get_connected_buses():
assert sorted(mvb.get_connected_buses()) == mv_bus_ids
for lvb in lv_buses:
assert sorted(lvb.get_connected_buses()) == lv_bus_ids


def test_res_voltage_unbalance():
bus = Bus("b3", phases="abc")

va = 230 + 0j
vb = 230 * np.exp(4j * np.pi / 3)
vc = 230 * np.exp(2j * np.pi / 3)

# Balanced system
bus._res_potentials = np.array([va, vb, vc])
assert np.isclose(bus.res_voltage_unbalance().magnitude, 0)

# Unbalanced system
bus._res_potentials = np.array([va, vb, vb])
assert np.isclose(bus.res_voltage_unbalance().magnitude, 100)

# With neutral
bus = Bus("b3n", phases="abcn")
bus._res_potentials = np.array([va, vb, vc, 0])
assert np.isclose(bus.res_voltage_unbalance().magnitude, 0)
bus._res_potentials = np.array([va, vb, vb, 0])
assert np.isclose(bus.res_voltage_unbalance().magnitude, 100)

# Non 3-phase bus
bus = Bus("b1", phases="an")
bus._res_potentials = np.array([va, 0])
with pytest.raises(RoseauLoadFlowException) as e:
bus.res_voltage_unbalance()
assert e.value.code == RoseauLoadFlowExceptionCode.BAD_PHASE
assert e.value.msg == "Voltage unbalance is only available for 3-phases buses, bus 'b1' has phases 'an'"
28 changes: 14 additions & 14 deletions roseau/load_flow/tests/test_converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,23 @@ def test_phasor_to_sym():
vc = 230 * np.e ** (1j * 2 * np.pi / 3)

# Test balanced direct system: positive sequence
expected = np.array([[0], [230], [0]], dtype=complex)
expected = np.array([0, 230, 0], dtype=complex)
assert np.allclose(phasor_to_sym([va, vb, vc]), expected)
# Also test numpy array input with different shapes
assert np.allclose(phasor_to_sym(np.array([va, vb, vc])), expected)
assert np.allclose(phasor_to_sym(np.array([[va], [vb], [vc]])), expected)
assert np.allclose(phasor_to_sym(np.array([[va], [vb], [vc]])), expected.reshape((3, 1)))

# Test balanced indirect system: negative sequence
expected = np.array([[0], [0], [230]], dtype=complex)
expected = np.array([0, 0, 230], dtype=complex)
assert np.allclose(phasor_to_sym([va, vc, vb]), expected)

# Test unbalanced system: zero sequence
expected = np.array([[230], [0], [0]], dtype=complex)
expected = np.array([230, 0, 0], dtype=complex)
assert np.allclose(phasor_to_sym([va, va, va]), expected)

# Test unbalanced system: general case
va = 200 + 0j
expected = np.array([[10 * np.e ** (1j * np.pi)], [220], [10 * np.e ** (1j * np.pi)]], dtype=complex)
expected = np.array([10 * np.exp(1j * np.pi), 220, 10 * np.exp(1j * np.pi)], dtype=complex)
assert np.allclose(phasor_to_sym([va, vb, vc]), expected)


Expand All @@ -40,23 +40,23 @@ def test_sym_to_phasor():
vc = 230 * np.e ** (1j * 2 * np.pi / 3)

# Test balanced direct system: positive sequence
expected = np.array([[va], [vb], [vc]], dtype=complex)
expected = np.array([va, vb, vc], dtype=complex)
assert np.allclose(sym_to_phasor([0, va, 0]), expected)
# Also test numpy array input with different shapes
assert np.allclose(sym_to_phasor(np.array([0, va, 0])), expected)
assert np.allclose(sym_to_phasor(np.array([[0], [va], [0]])), expected)
assert np.allclose(sym_to_phasor(np.array([[0], [va], [0]])), expected.reshape((3, 1)))

# Test balanced indirect system: negative sequence
expected = np.array([[va], [vc], [vb]], dtype=complex)
expected = np.array([va, vc, vb], dtype=complex)
assert np.allclose(sym_to_phasor([0, 0, va]), expected)

# Test unbalanced system: zero sequence
expected = np.array([[va], [va], [va]], dtype=complex)
expected = np.array([va, va, va], dtype=complex)
assert np.allclose(sym_to_phasor([va, 0, 0]), expected)

# Test unbalanced system: general case
va = 200 + 0j
expected = np.array([[va], [vb], [vc]], dtype=complex)
expected = np.array([va, vb, vc], dtype=complex)
assert np.allclose(sym_to_phasor([10 * np.e ** (1j * np.pi), 220, 10 * np.e ** (1j * np.pi)]), expected)


Expand All @@ -66,17 +66,17 @@ def test_phasor_sym_roundtrip():
vc = 230 * np.e ** (1j * 2 * np.pi / 3)

# Test balanced direct system: positive sequence
assert np.allclose(sym_to_phasor(phasor_to_sym([va, vb, vc])), np.array([[va], [vb], [vc]]))
assert np.allclose(sym_to_phasor(phasor_to_sym([va, vb, vc])), np.array([va, vb, vc]))

# Test balanced indirect system: negative sequence
assert np.allclose(sym_to_phasor(phasor_to_sym([va, vc, vb])), np.array([[va], [vc], [vb]]))
assert np.allclose(sym_to_phasor(phasor_to_sym([va, vc, vb])), np.array([va, vc, vb]))

# Test unbalanced system: zero sequence
assert np.allclose(sym_to_phasor(phasor_to_sym([va, va, va])), np.array([[va], [va], [va]]))
assert np.allclose(sym_to_phasor(phasor_to_sym([va, va, va])), np.array([va, va, va]))

# Test unbalanced system: general case
va = 200 + 0j
assert np.allclose(sym_to_phasor(phasor_to_sym([va, vb, vc])), np.array([[va], [vb], [vc]]))
assert np.allclose(sym_to_phasor(phasor_to_sym([va, vb, vc])), np.array([va, vb, vc]))


def test_series_phasor_to_sym():
Expand Down

0 comments on commit 2857e43

Please sign in to comment.