Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add voltage unbalance calculation #142

Merged
merged 3 commits into from
Oct 30, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
alihamdan marked this conversation as resolved.
Show resolved Hide resolved

#
# 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().m_as("percent"), 0)

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

# With neutral
bus = Bus("b3n", phases="abcn")
bus._res_potentials = np.array([va, vb, vc, 0])
assert np.isclose(bus.res_voltage_unbalance().m_as("percent"), 0)
bus._res_potentials = np.array([va, vb, vb, 0])
assert np.isclose(bus.res_voltage_unbalance().m_as("percent"), 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