Skip to content

Transformer: support periodic clock input #1011

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

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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 docs/examples/Transformer Examples.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,7 @@
"three_winding_transformer[\"winding_2\"] = [1]\n",
"three_winding_transformer[\"winding_3\"] = [1]\n",
"three_winding_transformer[\"clock_12\"] = [5]\n",
"three_winding_transformer[\"clock_13\"] = [5]\n",
"three_winding_transformer[\"clock_13\"] = [-7] # supports periodic input\n",
"three_winding_transformer[\"tap_side\"] = [0]\n",
"three_winding_transformer[\"tap_pos\"] = [0]\n",
"three_winding_transformer[\"tap_min\"] = [-10]\n",
Expand Down
6 changes: 3 additions & 3 deletions docs/user_manual/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ An example of usage of transformer is given in [Transformer Examples](../example
| `p0` | `double` | watt (W) | no-load (iron) loss | ✔ | ❌ | `>= 0` |
| `winding_from` | {py:class}`WindingType <power_grid_model.enum.WindingType>` | - | from-side winding type | &#10004; | &#10060; | |
| `winding_to` | {py:class}`WindingType <power_grid_model.enum.WindingType>` | - | to-side winding type | &#10004; | &#10060; | |
| `clock` | `int8_t` | - | clock number of phase shift. Even number is not possible if one side is Y(N) winding and the other side is not Y(N) winding. Odd number is not possible, if both sides are Y(N) winding or both sides are not Y(N) winding. | &#10004; | &#10060; | `>= 0` and `<= 12` |
| `clock` | `int8_t` | - | clock number of phase shift. Even number is not possible if one side is Y(N) winding and the other side is not Y(N) winding. Odd number is not possible, if both sides are Y(N) winding or both sides are not Y(N) winding. | &#10004; | &#10060; | `>= -12` and `<= 12` |
| `tap_side` | {py:class}`BranchSide <power_grid_model.enum.BranchSide>` | - | side of tap changer | &#10004; | &#10060; | |
| `tap_pos` | `int8_t` | - | current position of tap changer | &#10060; default `tap_nom`, if no `tap_nom` default `0` | &#10004; | `(tap_min <= tap_pos <= tap_max)` or `(tap_min >= tap_pos >= tap_max)` |
| `tap_min` | `int8_t` | - | position of tap changer at minimum voltage | &#10004; | &#10060; | |
Expand Down Expand Up @@ -514,8 +514,8 @@ An example of usage of three-winding transformer is given in
| `winding_1` | {py:class}`WindingType <power_grid_model.enum.WindingType>` | - | side 1 winding type | &#10004; | &#10060; | |
| `winding_2` | {py:class}`WindingType <power_grid_model.enum.WindingType>` | - | side 2 winding type | &#10004; | &#10060; | |
| `winding_3` | {py:class}`WindingType <power_grid_model.enum.WindingType>` | - | side 3 winding type | &#10004; | &#10060; | |
| `clock_12` | `int8_t` | - | clock number of phase shift across side 1-2, odd number is only allowed for Dy(n) or Y(N)d configuration. | &#10004; | &#10060; | `>= 0` and `<= 12` |
| `clock_13` | `int8_t` | - | clock number of phase shift across side 1-3, odd number is only allowed for Dy(n) or Y(N)d configuration. | &#10004; | &#10060; | `>= 0` and `<= 12` |
| `clock_12` | `int8_t` | - | clock number of phase shift across side 1-2, odd number is only allowed for Dy(n) or Y(N)d configuration. | &#10004; | &#10060; | `>= -12` and `<= 12` |
| `clock_13` | `int8_t` | - | clock number of phase shift across side 1-3, odd number is only allowed for Dy(n) or Y(N)d configuration. | &#10004; | &#10060; | `>= -12` and `<= 12` |
| `tap_side` | {py:class}`Branch3Side <power_grid_model.enum.Branch3Side>` | - | side of tap changer | &#10004; | &#10060; | `side_1` or `side_2` or `side_3` |
| `tap_pos` | `int8_t` | - | current position of tap changer | &#10060; default `tap_nom`, if no `tap_nom` default `0` | &#10004; | `(tap_min <= tap_pos <= tap_max)` or `(tap_min >= tap_pos >= tap_max)` |
| `tap_min` | `int8_t` | - | position of tap changer at minimum voltage | &#10004; | &#10060; | |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

#pragma once

#include <cmath>
#include <complex>
#include <cstddef>
#include <cstdint>
Expand Down Expand Up @@ -108,4 +109,21 @@ struct IncludeAll {
};
constexpr IncludeAll include_all{};

// function to handle periodic mapping
template <typename T> constexpr T map_to_cyclic_range(T value, T period) {
static_assert(std::is_arithmetic_v<T>, "T must be an arithmetic type (integral or floating-point)");
if constexpr (std::is_integral_v<T>) {
return static_cast<T>((value % period + period) % period);
} else {
if (std::is_constant_evaluated()) {
T quotient = value / period;
Idx const floored_quotient =
(quotient >= T{0}) ? static_cast<Idx>(quotient) : static_cast<Idx>(quotient) - 1;
T result = value - static_cast<T>(floored_quotient) * period;
return result;
}
return std::fmod(std::fmod(value, period) + period, period);
}
}

} // namespace power_grid_model
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,9 @@ class ThreeWindingTransformer : public Branch3 {
throw InvalidTransformerClock{id(), clock_13_};
}

// set clock to zero if it is 12
clock_12_ = static_cast<IntS>(clock_12_ % 12);
clock_13_ = static_cast<IntS>(clock_13_ % 12);
// handle periodic clock input -> in range [0, 11]
clock_12_ = map_to_cyclic_range(clock_12_, IntS{12});
clock_13_ = map_to_cyclic_range(clock_13_, IntS{12});
// check tap bounds
tap_pos_ = tap_limit(tap_pos_);
}
Expand All @@ -119,6 +119,8 @@ class ThreeWindingTransformer : public Branch3 {
constexpr IntS tap_min() const { return tap_min_; }
constexpr IntS tap_max() const { return tap_max_; }
constexpr IntS tap_nom() const { return tap_nom_; }
constexpr IntS clock_12() const { return clock_12_; }
constexpr IntS clock_13() const { return clock_13_; }

// setter
constexpr bool set_tap(IntS new_tap) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ class Transformer : public Branch {
throw InvalidTransformerClock{id(), clock_};
}

// set clock to zero if it is 12
clock_ = static_cast<IntS>(clock_ % 12);
// handle periodic clock input -> in range [0, 11]
clock_ = map_to_cyclic_range(clock_, IntS{12});
// check tap bounds
tap_pos_ = tap_limit(tap_pos_);
}
Expand All @@ -82,6 +82,7 @@ class Transformer : public Branch {
constexpr IntS tap_min() const { return tap_min_; }
constexpr IntS tap_max() const { return tap_max_; }
constexpr IntS tap_nom() const { return tap_nom_; }
constexpr IntS clock() const { return clock_; }

// setter
constexpr bool set_tap(IntS new_tap) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,17 +45,14 @@ constexpr double tap_adjust_impedance(double tap_pos, double tap_min, double tap
constexpr bool is_valid_clock(IntS clock, WindingType winding_from, WindingType winding_to) {
using enum WindingType;

bool const clock_in_range = 0 <= clock && clock <= 12;
bool const clock_is_even = (clock % 2) == 0;

bool const is_from_wye = winding_from == wye || winding_from == wye_n;
bool const is_to_wye = winding_to == wye || winding_to == wye_n;

// even clock number is only possible when both sides are wye winding or both sides aren't
// and conversely for odd clock number
bool const correct_clock_winding = (clock_is_even == (is_from_wye == is_to_wye));

return clock_in_range && correct_clock_winding;
return (clock_is_even == (is_from_wye == is_to_wye));
}

// add tap
Expand Down
2 changes: 1 addition & 1 deletion src/power_grid_model/validation/_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -607,7 +607,7 @@ def validate_transformer(data: SingleDataset) -> list[ValidationError]:
errors += _all_greater_than_or_equal_to_zero(data, ComponentType.transformer, "p0")
errors += _all_valid_enum_values(data, ComponentType.transformer, "winding_from", WindingType)
errors += _all_valid_enum_values(data, ComponentType.transformer, "winding_to", WindingType)
errors += _all_between_or_at(data, ComponentType.transformer, "clock", 0, 12)
errors += _all_between_or_at(data, ComponentType.transformer, "clock", -12, 12)
errors += _all_valid_clocks(data, ComponentType.transformer, "clock", "winding_from", "winding_to")
errors += _all_valid_enum_values(data, ComponentType.transformer, "tap_side", BranchSide)
errors += _all_between_or_at(
Expand Down
10 changes: 10 additions & 0 deletions tests/cpp_unit_tests/test_common.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,15 @@ static_assert(include_all(1));

// NOLINTNEXTLINE(performance-move-const-arg,hicpp-move-const-arg) // to test that rvalues work
static_assert(include_all(Idx{2}, std::move(Idx{3}))); // NOSONAR // to test that rvalues work

// periodic mapping
static_assert(map_to_cyclic_range(5, 3) == 2);
static_assert(map_to_cyclic_range(5.0, 3.0) == 2.0);
static_assert(map_to_cyclic_range(-1, 3) == 2);
static_assert(map_to_cyclic_range(-1.0, 3.0) == 2.0);
static_assert(map_to_cyclic_range(12, 12) == 0);
static_assert(map_to_cyclic_range(13, 12) == 1);
static_assert(map_to_cyclic_range(11, 12) == 11);
static_assert(map_to_cyclic_range(-1, -3) == -1);
} // namespace
} // namespace power_grid_model
22 changes: 22 additions & 0 deletions tests/cpp_unit_tests/test_three_winding_transformer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,28 @@ TEST_CASE("Test three winding transformer") {
input.node_2 = 3;
}

SUBCASE("Periodic clock input") {
input.clock_12 = 24;
input.clock_13 = 37;
ThreeWindingTransformer const trafo_24_36(input, 138e3, 69e3, 13.8e3);
CHECK(trafo_24_36.clock_12() == 0);
CHECK(trafo_24_36.clock_13() == 1);

input.clock_12 = -2;
input.clock_13 = -13;
ThreeWindingTransformer const trafo_m2_m13(input, 138e3, 69e3, 13.8e3);
CHECK(trafo_m2_m13.clock_12() == 10);
CHECK(trafo_m2_m13.clock_13() == 11);

input.winding_2 = WindingType::delta;
input.winding_3 = WindingType::delta;
input.clock_12 = 25;
input.clock_13 = 13;
ThreeWindingTransformer const trafo_25_13(input, 138e3, 69e3, 13.8e3);
CHECK(trafo_25_13.clock_12() == 1);
CHECK(trafo_25_13.clock_13() == 1);
}

SUBCASE("Test i base") {
CHECK(vec[0].base_i_1() == doctest::Approx(base_i_1));
CHECK(vec[0].base_i_2() == doctest::Approx(base_i_2));
Expand Down
17 changes: 17 additions & 0 deletions tests/cpp_unit_tests/test_transformer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,23 @@ TEST_CASE("Test transformer") {
CHECK(vec[0].tap_pos() == 9);
}

SUBCASE("periodic clock input") {
input.clock = 24;
Transformer const trafo_24(input, 150.0e3, 10.0e3);
input.clock = 36;
Transformer const trafo_36(input, 150.0e3, 10.0e3);
input.clock = -2;
Transformer const trafo_m2(input, 150.0e3, 10.0e3);
CHECK(trafo_24.clock() == 0);
CHECK(trafo_36.clock() == 0);
CHECK(trafo_m2.clock() == 10);

input.winding_to = WindingType::delta;
input.clock = 25;
Transformer const trafo_25(input, 150.0e3, 10.0e3);
CHECK(trafo_25.clock() == 1);
}

SUBCASE("symmetric parameters") {
for (size_t i = 0; i < 5; i++) {
auto changed =
Expand Down
10 changes: 2 additions & 8 deletions tests/unit/validation/test_input_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ def original_data() -> dict[ComponentType, np.ndarray]:
transformer["p0"] = [63000.0, 0.0, -10.0]
transformer["winding_from"] = [8, 0, 2]
transformer["winding_to"] = [5, 1, 2]
transformer["clock"] = [13, -1, 7]
transformer["clock"] = [13, -13, 7]
transformer["tap_side"] = [-1, 0, 1]
transformer["tap_pos"] = [-1, 6, -4]
transformer["tap_min"] = [-2, 4, 3]
Expand Down Expand Up @@ -496,7 +496,7 @@ def test_validate_input_data_sym_calculation(input_data):
assert NotGreaterOrEqualError(ComponentType.transformer, "i0", [1], "p0/sn") in validation_errors
assert NotLessThanError(ComponentType.transformer, "i0", [14], 1) in validation_errors
assert NotGreaterOrEqualError(ComponentType.transformer, "p0", [15], 0) in validation_errors
assert NotBetweenOrAtError(ComponentType.transformer, "clock", [1, 14], (0, 12)) in validation_errors
assert NotBetweenOrAtError(ComponentType.transformer, "clock", [1, 14], (-12, 12)) in validation_errors
assert (
NotBetweenOrAtError(ComponentType.transformer, "tap_pos", [14, 15], ("tap_min", "tap_max")) in validation_errors
)
Expand Down Expand Up @@ -822,12 +822,6 @@ def test_validate_three_winding_transformer(input_data):
assert NotGreaterOrEqualError(ComponentType.three_winding_transformer, "i0", [29], "p0/sn_1") in validation_errors
assert NotLessThanError(ComponentType.three_winding_transformer, "i0", [28], 1) in validation_errors
assert NotGreaterOrEqualError(ComponentType.three_winding_transformer, "p0", [1], 0) in validation_errors
assert (
NotBetweenOrAtError(ComponentType.three_winding_transformer, "clock_12", [1, 28], (0, 12)) in validation_errors
)
assert (
NotBetweenOrAtError(ComponentType.three_winding_transformer, "clock_13", [1, 28], (0, 12)) in validation_errors
)
assert (
NotBetweenOrAtError(ComponentType.three_winding_transformer, "tap_pos", [1, 28], ("tap_min", "tap_max"))
in validation_errors
Expand Down
Loading