diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/common/exception.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/common/exception.hpp index 0f43ca374..dfd21a4e4 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/common/exception.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/common/exception.hpp @@ -118,6 +118,14 @@ class InvalidRegulatedObject : public PowerGridError { } }; +class AutomaticTapCalculationError : public PowerGridError { + public: + AutomaticTapCalculationError(ID id) { + append_msg("Automatic tap changing regulator with tap_side at LV side is not supported. Found at id" + + std::to_string(id)); // NOSONAR + } +}; + class IDWrongType : public PowerGridError { public: explicit IDWrongType(ID id) { append_msg("Wrong type for object with id " + std::to_string(id) + '\n'); } diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/container.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/container.hpp index 8aee797f2..fd4d47e5d 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/container.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/container.hpp @@ -137,7 +137,9 @@ class Container, StorageableTypes...> { } // get size - template Idx size() const { + template + requires(std::same_as || ...) + Idx size() const { assert(construction_complete_); return size_[get_cls_pos_v]; } diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/main_core/input.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/main_core/input.hpp index aabef60d7..e2fc86ae0 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/main_core/input.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/main_core/input.hpp @@ -11,6 +11,9 @@ namespace power_grid_model::main_core { +constexpr std::array const branch3_sides = {Branch3Side::side_1, Branch3Side::side_2, + Branch3Side::side_3}; + // template to construct components // using forward interators // different selection based on component type @@ -124,6 +127,31 @@ inline void add_component(MainModelState& state, ForwardIter } }(); + if (regulated_object_idx.group == get_component_type_index(state)) { + auto const& regulated_object = get_component(state, regulated_object_idx); + + auto const non_tap_side = + regulated_object.tap_side() == BranchSide::from ? BranchSide::to : BranchSide::from; + if (get_component(state, regulated_object.node(regulated_object.tap_side())).u_rated() < + get_component(state, regulated_object.node(non_tap_side)).u_rated()) { + throw AutomaticTapCalculationError(id); + } + } else if (regulated_object_idx.group == get_component_type_index(state)) { + auto const& regulated_object = get_component(state, regulated_object_idx); + auto const tap_side_u_rated = + get_component(state, regulated_object.node(regulated_object.tap_side())).u_rated(); + for (auto const side : branch3_sides) { + if (side == regulated_object.tap_side()) { + continue; + } + if (tap_side_u_rated < get_component(state, regulated_object.node(side)).u_rated()) { + throw AutomaticTapCalculationError(id); + } + } + } else { + throw InvalidRegulatedObject(input.regulated_object, Component::name); + } + auto const regulated_object_type = get_component(state, regulated_object_idx).math_model_type(); double const u_rated = get_component(state, regulated_terminal).u_rated(); diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/optimizer/tap_position_optimizer.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/optimizer/tap_position_optimizer.hpp index 5e4ec497f..530841a30 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/optimizer/tap_position_optimizer.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/optimizer/tap_position_optimizer.hpp @@ -6,10 +6,14 @@ #include "base_optimizer.hpp" +#include "../all_components.hpp" #include "../auxiliary/dataset.hpp" #include "../common/enum.hpp" +#include "../common/exception.hpp" +#include "../main_core/state_queries.hpp" #include + #include #include #include @@ -21,114 +25,239 @@ namespace detail = power_grid_model::optimizer::detail; using TrafoGraphIdx = Idx; using EdgeWeight = int64_t; -using WeightedTrafo = std::pair; -using WeightedTrafoList = std::vector; -using RankedTransformerGroups = std::vector>; constexpr auto infty = std::numeric_limits::max(); +constexpr Idx2D unregulated_idx = {-1, -1}; struct TrafoGraphVertex { - bool is_source{}; // is_source = true if the vertex is a source + bool is_source{}; }; struct TrafoGraphEdge { - Idx2D pos{}; + Idx2D regulated_idx{}; EdgeWeight weight{}; + + bool operator==(const TrafoGraphEdge& other) const { + return regulated_idx == other.regulated_idx && weight == other.weight; + } // thanks boost + + auto operator<=>(const TrafoGraphEdge& other) const { + if (auto cmp = weight <=> other.weight; cmp != 0) { // NOLINT(modernize-use-nullptr) + return cmp; + } + if (auto cmp = regulated_idx.group <=> other.regulated_idx.group; cmp != 0) { // NOLINT(modernize-use-nullptr) + return cmp; + } + return regulated_idx.pos <=> other.regulated_idx.pos; + } +}; + +using TrafoGraphEdges = std::vector>; +using TrafoGraphEdgeProperties = std::vector; +using RankedTransformerGroups = std::vector; + +struct RegulatedObjects { + std::set transformers{}; + std::set transformers3w{}; }; -// TODO(mgovers): investigate whether this really is the correct graph structure using TransformerGraph = boost::compressed_sparse_row_graph; +inline void add_to_edge(TrafoGraphEdges& edges, TrafoGraphEdgeProperties& edge_props, Idx const& start, Idx const& end, + TrafoGraphEdge const& edge_prop) { + edges.emplace_back(start, end); + edge_props.emplace_back(edge_prop); +} + +inline void process_trafo3w_edge(ThreeWindingTransformer const& transformer3w, bool const& trafo3w_is_regulated, + Idx2D const& trafo3w_idx, TrafoGraphEdges& edges, + TrafoGraphEdgeProperties& edge_props) { + using enum Branch3Side; + + constexpr std::array, 3> const branch3_combinations{ + {{side_1, side_2}, {side_2, side_3}, {side_3, side_1}}}; + + for (auto const& [first_side, second_side] : branch3_combinations) { + if (!transformer3w.status(first_side) || !transformer3w.status(second_side)) { + continue; + } + auto const& from_node = transformer3w.node(first_side); + auto const& to_node = transformer3w.node(second_side); + + auto const tap_at_first_side = transformer3w.tap_side() == first_side; + auto const single_direction_condition = + trafo3w_is_regulated && (tap_at_first_side || transformer3w.tap_side() == second_side); + // ranking + if (single_direction_condition) { + auto const& tap_side_node = tap_at_first_side ? from_node : to_node; + auto const& non_tap_side_node = tap_at_first_side ? to_node : from_node; + // add regulated idx only when the first side node is tap side node. + // This is done to add only one directional edge with regulated idx. + Idx2D const regulated_idx = from_node == tap_side_node ? unregulated_idx : trafo3w_idx; + add_to_edge(edges, edge_props, tap_side_node, non_tap_side_node, {regulated_idx, 1}); + } else { + add_to_edge(edges, edge_props, from_node, to_node, {unregulated_idx, 1}); + add_to_edge(edges, edge_props, to_node, from_node, {unregulated_idx, 1}); + } + } +} + +template Component, class ComponentContainer> + requires main_core::model_component_state_c +constexpr void add_edge(main_core::MainModelState const& state, + RegulatedObjects const& regulated_objects, TrafoGraphEdges& edges, + TrafoGraphEdgeProperties& edge_props) { + + for (auto const& transformer3w : state.components.template citer()) { + bool const trafo3w_is_regulated = regulated_objects.transformers3w.contains(transformer3w.id()); + Idx2D const trafo3w_idx = main_core::get_component_idx_by_id(state, transformer3w.id()); + process_trafo3w_edge(transformer3w, trafo3w_is_regulated, trafo3w_idx, edges, edge_props); + } +} + +template Component, class ComponentContainer> + requires main_core::model_component_state_c +constexpr void add_edge(main_core::MainModelState const& state, + RegulatedObjects const& regulated_objects, TrafoGraphEdges& edges, + TrafoGraphEdgeProperties& edge_props) { + for (auto const& transformer : state.components.template citer()) { + if (!transformer.from_status() || !transformer.to_status()) { + continue; + } + auto const& from_node = transformer.from_node(); + auto const& to_node = transformer.to_node(); + + if (regulated_objects.transformers.contains(transformer.id())) { + auto const tap_at_from_side = transformer.tap_side() == BranchSide::from; + auto const& tap_side_node = tap_at_from_side ? from_node : to_node; + auto const& non_tap_side_node = tap_at_from_side ? to_node : from_node; + add_to_edge(edges, edge_props, tap_side_node, non_tap_side_node, + {main_core::get_component_idx_by_id(state, transformer.id()), 1}); + } else { + add_to_edge(edges, edge_props, from_node, to_node, {unregulated_idx, 1}); + add_to_edge(edges, edge_props, to_node, from_node, {unregulated_idx, 1}); + } + } +} + +template Component, class ComponentContainer> + requires main_core::model_component_state_c && + (!transformer_c) +constexpr void add_edge(main_core::MainModelState const& state, + RegulatedObjects const& /* regulated_objects */, TrafoGraphEdges& edges, + TrafoGraphEdgeProperties& edge_props) { + auto const& iter = state.components.template citer(); + edges.reserve(std::distance(iter.begin(), iter.end()) * 2); + edge_props.reserve(std::distance(iter.begin(), iter.end()) * 2); + for (auto const& branch : iter) { + if (!branch.from_status() || !branch.to_status()) { + continue; + } + add_to_edge(edges, edge_props, branch.from_node(), branch.to_node(), {unregulated_idx, 0}); + add_to_edge(edges, edge_props, branch.to_node(), branch.from_node(), {unregulated_idx, 0}); + } +} + +template +inline auto add_edges(State const& state, RegulatedObjects const& regulated_objects, TrafoGraphEdges& edges, + TrafoGraphEdgeProperties& edge_props) { + (add_edge(state, regulated_objects, edges, edge_props), ...); +} + +template +inline auto retrieve_regulator_info(State const& state) -> RegulatedObjects { + RegulatedObjects regulated_objects; + for (auto const& regulator : state.components.template citer()) { + if (!regulator.status()) { + continue; + } + if (regulator.regulated_object_type() == ComponentType::branch) { + regulated_objects.transformers.emplace(regulator.regulated_object()); + } else { + regulated_objects.transformers3w.emplace(regulator.regulated_object()); + } + } + return regulated_objects; +} + template -inline auto build_transformer_graph(State const& /*state*/) -> TransformerGraph { - // TODO(nbharambe): implement - return {}; +inline auto build_transformer_graph(State const& state) -> TransformerGraph { + TrafoGraphEdges edges; + TrafoGraphEdgeProperties edge_props; + + const RegulatedObjects regulated_objects = retrieve_regulator_info(state); + + add_edges(state, regulated_objects, edges, edge_props); + + // build graph + TransformerGraph trafo_graph{boost::edges_are_unsorted_multi_pass, edges.cbegin(), edges.cend(), + edge_props.cbegin(), + static_cast(state.components.template size())}; + + BGL_FORALL_VERTICES(v, trafo_graph, TransformerGraph) { trafo_graph[v].is_source = false; } + + // Mark sources + for (auto const& source : state.components.template citer()) { + // ignore disabled sources + trafo_graph[source.node()].is_source = source.status(); + } + + return trafo_graph; } -inline auto process_edges_dijkstra(Idx v, std::vector& edge_weight, std::vector& edge_pos, - TransformerGraph const& graph) -> void { +inline void process_edges_dijkstra(Idx v, std::vector& vertex_distances, TransformerGraph const& graph) { using TrafoGraphElement = std::pair; std::priority_queue, std::greater<>> pq; - edge_weight[v] = 0; - edge_pos[v] = {v, v}; + vertex_distances[v] = 0; pq.push({0, v}); while (!pq.empty()) { auto [dist, u] = pq.top(); pq.pop(); - if (dist != edge_weight[u]) { + if (dist != vertex_distances[u]) { continue; } - for (auto e : boost::make_iterator_range(boost::out_edges(u, graph))) { + BGL_FORALL_OUTEDGES(u, e, graph, TransformerGraph) { auto v = boost::target(e, graph); const EdgeWeight weight = graph[e].weight; - if (edge_weight[u] + weight < edge_weight[v]) { - edge_weight[v] = edge_weight[u] + weight; - edge_pos[v] = graph[e].pos; - pq.push({edge_weight[v], v}); + if (vertex_distances[u] + weight < vertex_distances[v]) { + vertex_distances[v] = vertex_distances[u] + weight; + pq.push({vertex_distances[v], v}); } } } } -// Step 2: Initialize the rank of all vertices (transformer nodes) as infinite (INT_MAX) -// Step 3: Loop all the connected sources (status == 1) -// a. Perform Dijkstra shortest path algorithm from the vertex with that source. -// This is to determine the shortest path of all vertices to this particular source. -inline auto get_edge_weights(TransformerGraph const& graph) -> WeightedTrafoList { - std::vector edge_weight(boost::num_vertices(graph), infty); - std::vector edge_pos(boost::num_vertices(graph)); - - for (auto v : boost::make_iterator_range(boost::vertices(graph))) { +inline auto get_edge_weights(TransformerGraph const& graph) -> TrafoGraphEdgeProperties { + std::vector vertex_distances(boost::num_vertices(graph), infty); + BGL_FORALL_VERTICES(v, graph, TransformerGraph) { if (graph[v].is_source) { - process_edges_dijkstra(v, edge_weight, edge_pos, graph); + process_edges_dijkstra(v, vertex_distances, graph); } } - WeightedTrafoList result; - for (size_t i = 0; i < edge_weight.size(); ++i) { - result.emplace_back(edge_pos[i], edge_weight[i]); + TrafoGraphEdgeProperties result; + BGL_FORALL_EDGES(e, graph, TransformerGraph) { + if (graph[e].regulated_idx == unregulated_idx) { + continue; + } + result.push_back({graph[e].regulated_idx, vertex_distances[boost::source(e, graph)]}); } return result; } -// Step 4: Loop all transformers with automatic tap changers, including the transformers which are not -// fully connected -// a.Rank of the transformer <- -// i. Infinity(INT_MAX), if tap side of the transformer is disconnected. -// The transformer regulation should be ignored -// ii.Rank of the vertex at the tap side of the transformer, if tap side of the transformer is connected -inline auto transformer_disconnected(Idx2D const& /*pos*/) -> bool { - // waiting for the functionalities in step 1 to be implemented - return false; -} - -inline auto rank_transformers(WeightedTrafoList const& w_trafo_list) -> RankedTransformerGroups { +inline auto rank_transformers(TrafoGraphEdgeProperties const& w_trafo_list) -> RankedTransformerGroups { auto sorted_trafos = w_trafo_list; - for (auto& trafo : sorted_trafos) { - if (transformer_disconnected(trafo.first)) { - trafo.second = infty; - } - } - std::sort(sorted_trafos.begin(), sorted_trafos.end(), - [](const WeightedTrafo& a, const WeightedTrafo& b) { return a.second < b.second; }); - - RankedTransformerGroups groups; - Idx last_weight = -1; - for (const auto& trafo : sorted_trafos) { - if (groups.empty() || last_weight != trafo.second) { - groups.push_back(std::vector{trafo.first}); - last_weight = trafo.second; - } else { - groups.back().push_back(trafo.first); - } - } + [](const TrafoGraphEdge& a, const TrafoGraphEdge& b) { return a.weight < b.weight; }); + + RankedTransformerGroups groups(sorted_trafos.size()); + std::ranges::transform(sorted_trafos, groups.begin(), [](const TrafoGraphEdge& x) { return x.regulated_idx; }); return groups; } diff --git a/src/power_grid_model/core/error_handling.py b/src/power_grid_model/core/error_handling.py index 23f5615ed..8f7016483 100644 --- a/src/power_grid_model/core/error_handling.py +++ b/src/power_grid_model/core/error_handling.py @@ -14,6 +14,7 @@ from power_grid_model.core.index_integer import IdxNp from power_grid_model.core.power_grid_core import power_grid_core as pgc from power_grid_model.errors import ( + AutomaticTapCalculationError, ConflictID, ConflictVoltage, IDNotFound, @@ -63,6 +64,9 @@ _INVALID_REGULATED_OBJECT_RE = re.compile( r"(\w+) regulator is not supported for object " ) # potentially multiple different flavors +_AUTOMATIC_TAP_CALCULATION_ERROR_RE = re.compile( + r"Automatic tap changing regulator with tap_side at LV side is not supported. Found at id (-?\d+)\n" +) _ID_WRONG_TYPE_RE = re.compile(r"Wrong type for object with id (-?\d+)\n") _INVALID_CALCULATION_METHOD_RE = re.compile(r"The calculation method is invalid for this calculation!") _INVALID_SHORT_CIRCUIT_PHASE_OR_TYPE_RE = re.compile(r"short circuit type") # multiple different flavors @@ -82,6 +86,7 @@ _ID_NOT_FOUND_RE: IDNotFound, _INVALID_MEASURED_OBJECT_RE: InvalidMeasuredObject, _INVALID_REGULATED_OBJECT_RE: InvalidRegulatedObject, + _AUTOMATIC_TAP_CALCULATION_ERROR_RE: AutomaticTapCalculationError, _ID_WRONG_TYPE_RE: IDWrongType, _INVALID_CALCULATION_METHOD_RE: InvalidCalculationMethod, _INVALID_SHORT_CIRCUIT_PHASE_OR_TYPE_RE: InvalidShortCircuitPhaseOrType, diff --git a/src/power_grid_model/errors.py b/src/power_grid_model/errors.py index 529189277..4243c66b2 100644 --- a/src/power_grid_model/errors.py +++ b/src/power_grid_model/errors.py @@ -86,6 +86,10 @@ class InvalidCalculationMethod(PowerGridError): """Invalid calculation method provided""" +class AutomaticTapCalculationError(PowerGridError): + """Automatic tap changer with tap at LV side is unsupported for automatic tap changing calculation.""" + + class InvalidShortCircuitPhaseOrType(PowerGridError): """Invalid (combination of) short circuit types and phase(s) provided""" diff --git a/tests/cpp_unit_tests/test_optimizer.cpp b/tests/cpp_unit_tests/test_optimizer.cpp index 9f1d79206..1c81685d5 100644 --- a/tests/cpp_unit_tests/test_optimizer.cpp +++ b/tests/cpp_unit_tests/test_optimizer.cpp @@ -8,7 +8,10 @@ namespace power_grid_model::optimizer { namespace { -struct StubComponentContainer {}; + +using StubComponentContainer = + Container, Line, Link, Node, Transformer, + ThreeWindingTransformer, TransformerTapRegulator, Source>; using StubState = main_core::MainModelState; static_assert(main_core::main_model_state_c); @@ -74,12 +77,15 @@ TEST_CASE("Test no-op optimizer") { } TEST_CASE("Test tap position optimizer") { + StubState empty_state{}; + empty_state.components.set_construction_complete(); + SUBCASE("symmetric") { for (auto strategy : strategies) { CAPTURE(strategy); auto optimizer = TapPositionOptimizer{ stub_steady_state_state_calculator, stub_const_dataset_update, strategy}; - CHECK_THROWS_AS(optimizer.optimize({}), PowerGridError); // TODO(mgovers): implement this check + CHECK_THROWS_AS(optimizer.optimize(empty_state), PowerGridError); // TODO(mgovers): implement this check } } SUBCASE("asymmetric") { @@ -87,7 +93,7 @@ TEST_CASE("Test tap position optimizer") { CAPTURE(strategy); auto optimizer = TapPositionOptimizer{ stub_steady_state_state_calculator, stub_const_dataset_update, strategy}; - CHECK_THROWS_AS(optimizer.optimize({}), PowerGridError); // TODO(mgovers): implement this check + CHECK_THROWS_AS(optimizer.optimize(empty_state), PowerGridError); // TODO(mgovers): implement this check } } } @@ -138,7 +144,10 @@ TEST_CASE("Test get optimizer") { REQUIRE(tap_optimizer != nullptr); CHECK(tap_optimizer->get_strategy() == strategy); - CHECK_THROWS_AS(optimizer->optimize({}), PowerGridError); // TODO(mgovers): implement this check + StubState empty_state{}; + empty_state.components.set_construction_complete(); + CHECK_THROWS_AS(optimizer->optimize(empty_state), + PowerGridError); // TODO(mgovers): implement this check } } } diff --git a/tests/cpp_unit_tests/test_tap_position_optimizer.cpp b/tests/cpp_unit_tests/test_tap_position_optimizer.cpp index e86fe8d86..f8f661735 100644 --- a/tests/cpp_unit_tests/test_tap_position_optimizer.cpp +++ b/tests/cpp_unit_tests/test_tap_position_optimizer.cpp @@ -2,67 +2,267 @@ // // SPDX-License-Identifier: MPL-2.0 +#include #include #include +#include +#include + namespace pgm_tap = power_grid_model::optimizer::tap_position_optimizer; +namespace main_core = power_grid_model::main_core; namespace power_grid_model { +namespace { +using TestComponentContainer = + Container, Line, Link, Node, Transformer, + ThreeWindingTransformer, TransformerTapRegulator, Source>; +using TestState = main_core::MainModelState; + +TransformerInput get_transformer(ID id, ID from, ID to, BranchSide tap_side) { + return TransformerInput{.id = id, + .from_node = from, + .to_node = to, + .from_status = 1, + .to_status = 1, + .u1 = nan, + .u2 = nan, + .sn = nan, + .uk = nan, + .pk = nan, + .i0 = nan, + .p0 = nan, + .winding_from = WindingType::wye_n, + .winding_to = WindingType::wye_n, + .clock = 0, + .tap_side = tap_side, + .tap_pos = na_IntS, + .tap_min = na_IntS, + .tap_max = na_IntS, + .tap_nom = na_IntS, + .tap_size = nan, + .uk_min = nan, + .uk_max = nan, + .pk_min = nan, + .pk_max = nan, + .r_grounding_from = nan, + .x_grounding_from = nan, + .r_grounding_to = nan, + .x_grounding_to = nan}; +} + +ThreeWindingTransformerInput get_transformer3w(ID id, ID node_1, ID node_2, ID node_3) { + return ThreeWindingTransformerInput{ + .id = id, + .node_1 = node_1, + .node_2 = node_2, + .node_3 = node_3, + .status_1 = 1, + .status_2 = 1, + .status_3 = 1, + .u1 = nan, + .u2 = nan, + .u3 = nan, + .sn_1 = nan, + .sn_2 = nan, + .sn_3 = nan, + .uk_12 = nan, + .uk_13 = nan, + .uk_23 = nan, + .pk_12 = nan, + .pk_13 = nan, + .pk_23 = nan, + .i0 = nan, + .p0 = nan, + .winding_1 = WindingType::wye_n, + .winding_2 = WindingType::wye_n, + .winding_3 = WindingType::wye_n, + .clock_12 = 0, + .clock_13 = 0, + .tap_side = Branch3Side::side_1, + .tap_pos = 0, + .tap_min = 0, + .tap_max = 0, + .tap_nom = 0, + .tap_size = nan, + .uk_12_min = nan, + .uk_12_max = nan, + .uk_13_min = nan, + .uk_13_max = nan, + .uk_23_min = nan, + .uk_23_max = nan, + .pk_12_min = nan, + .pk_12_max = nan, + .pk_13_min = nan, + .pk_13_max = nan, + .pk_23_min = nan, + .pk_23_max = nan, + .r_grounding_1 = nan, + .x_grounding_1 = nan, + .r_grounding_2 = nan, + .x_grounding_2 = nan, + .r_grounding_3 = nan, + .x_grounding_3 = nan, + }; +} + +LineInput get_line_input(ID id, ID from, ID to) { + return LineInput{.id = id, + .from_node = from, + .to_node = to, + .from_status = 1, + .to_status = 1, + .r1 = nan, + .x1 = nan, + .c1 = nan, + .tan1 = nan, + .r0 = nan, + .x0 = nan, + .c0 = nan, + .tan0 = nan, + .i_n = nan}; +} +TransformerTapRegulatorInput get_regulator(ID id, ID regulated_object, ControlSide control_side) { + return TransformerTapRegulatorInput{.id = id, + .regulated_object = regulated_object, + .status = 1, + .control_side = control_side, + .u_set = nan, + .u_band = nan, + .line_drop_compensation_r = nan, + .line_drop_compensation_x = nan}; +} + +} // namespace + +TEST_SUITE_BEGIN("Automatic Tap Changer"); TEST_CASE("Test Transformer ranking") { - // ToDo: The grid from OntNote page + // Minimum test grid + TestState state; + std::vector nodes{{0, 150e3}, {1, 10e3}, {2, 10e3}, {3, 10e3}, {4, 10e3}, + {5, 50e3}, {6, 10e3}, {7, 10e3}, {8, 10e3}, {9, 10e3}}; + main_core::add_component(state, nodes.begin(), nodes.end(), 50.0); + + std::vector transformers{ + get_transformer(11, 0, 1, BranchSide::from), get_transformer(12, 0, 1, BranchSide::from), + get_transformer(13, 5, 7, BranchSide::from), get_transformer(14, 2, 3, BranchSide::from), + get_transformer(15, 8, 9, BranchSide::from)}; + main_core::add_component(state, transformers.begin(), transformers.end(), 50.0); + + std::vector transformers3w{get_transformer3w(16, 0, 4, 5)}; + main_core::add_component(state, transformers3w.begin(), transformers3w.end(), 50.0); + + std::vector lines{get_line_input(17, 3, 6), get_line_input(18, 3, 9)}; + main_core::add_component(state, lines.begin(), lines.end(), 50.0); + + std::vector links{{19, 2, 1, 1, 1}, {20, 6, 4, 1, 1}, {21, 8, 7, 1, 1}}; + main_core::add_component(state, links.begin(), links.end(), 50.0); + + std::vector sources{{22, 0, 1, 1.0, 0, nan, nan, nan}}; + main_core::add_component(state, sources.begin(), sources.end(), 50.0); + + std::vector regulators{ + get_regulator(23, 11, ControlSide::from), get_regulator(24, 12, ControlSide::from), + get_regulator(25, 13, ControlSide::from), get_regulator(26, 14, ControlSide::from), + get_regulator(27, 15, ControlSide::from), get_regulator(28, 16, ControlSide::side_1)}; + main_core::add_component(state, regulators.begin(), regulators.end(), 50.0); + + state.components.set_construction_complete(); // Subcases SUBCASE("Building the graph") { - // ToDo: graph creation - } + using pgm_tap::unregulated_idx; + + // reference graph creation + // Inserted in order of transformer, transformer3w, line and link + std::vector> expected_edges; + expected_edges.insert(expected_edges.end(), {{0, 1}, {0, 1}, {5, 7}, {2, 3}, {8, 9}}); + expected_edges.insert(expected_edges.end(), {{0, 4}, {4, 5}, {5, 4}, {0, 5}}); + expected_edges.insert(expected_edges.end(), {{3, 6}, {6, 3}, {3, 9}, {9, 3}}); + expected_edges.insert(expected_edges.end(), {{2, 1}, {1, 2}, {6, 4}, {4, 6}, {8, 7}, {7, 8}}); + + pgm_tap::TrafoGraphEdgeProperties expected_edges_prop; + expected_edges_prop.insert(expected_edges_prop.end(), + {{{3, 0}, 1}, {{3, 1}, 1}, {{3, 2}, 1}, {{3, 3}, 1}, {{3, 4}, 1}}); + expected_edges_prop.insert(expected_edges_prop.end(), + {{{4, 0}, 1}, {unregulated_idx, 1}, {unregulated_idx, 1}, {unregulated_idx, 1}}); + expected_edges_prop.insert(expected_edges_prop.end(), 10, {unregulated_idx, 0}); + + const std::vector expected_vertex_props{{true}, {false}, {false}, {false}, {false}, + {false}, {false}, {false}, {false}, {false}}; - // Dummy graph - std::vector> edge_array = {{0, 1}, {0, 2}, {2, 3}}; + pgm_tap::TransformerGraph actual_graph = pgm_tap::build_transformer_graph(state); + pgm_tap::TrafoGraphEdgeProperties actual_edges_prop; + + boost::graph_traits::vertex_iterator vi, vi_end; + for (boost::tie(vi, vi_end) = vertices(actual_graph); vi != vi_end; ++vi) { + CHECK(actual_graph[*vi].is_source == expected_vertex_props[*vi].is_source); + } + + BGL_FORALL_EDGES(e, actual_graph, pgm_tap::TransformerGraph) { actual_edges_prop.push_back(actual_graph[e]); } + + std::sort(actual_edges_prop.begin(), actual_edges_prop.end()); + std::sort(expected_edges_prop.begin(), expected_edges_prop.end()); + CHECK(actual_edges_prop == expected_edges_prop); + } - std::vector edge_prop; - edge_prop.push_back(pgm_tap::TrafoGraphEdge({{0, 1}, 1})); - edge_prop.push_back(pgm_tap::TrafoGraphEdge({{0, 2}, 2})); - edge_prop.push_back(pgm_tap::TrafoGraphEdge({{2, 3}, 3})); + SUBCASE("Automatic tap unsupported tap side at LV") { + TestState bad_state; + std::vector bad_nodes{{0, 50e3}, {1, 10e3}}; + main_core::add_component(bad_state, bad_nodes.begin(), bad_nodes.end(), 50.0); - std::vector vertex_props{{true}, {false}, {false}, {false}}; + std::vector bad_trafo{get_transformer(2, 0, 1, BranchSide::to)}; + main_core::add_component(bad_state, bad_trafo.begin(), bad_trafo.end(), 50.0); - pgm_tap::TransformerGraph g{boost::edges_are_unsorted_multi_pass, edge_array.cbegin(), edge_array.cend(), - edge_prop.cbegin(), 4}; + std::vector bad_regulators{get_regulator(3, 2, ControlSide::from)}; - // Vertex properties can not be set during graph creation - boost::graph_traits::vertex_iterator vi, vi_end; - for (boost::tie(vi, vi_end) = vertices(g); vi != vi_end; ++vi) { - g[*vi].is_source = vertex_props[*vi].is_source; + CHECK_THROWS_AS(main_core::add_component(bad_state, bad_regulators.begin(), + bad_regulators.end(), 50.0), + AutomaticTapCalculationError); } SUBCASE("Process edge weights") { - auto const all_edge_weights = get_edge_weights(g); - const pgm_tap::WeightedTrafoList ref_edge_weights{{{0, 0}, 0}, {{0, 1}, 1}, {{0, 2}, 2}, {{2, 3}, 5}}; - CHECK(all_edge_weights == ref_edge_weights); + // Dummy graph + pgm_tap::TrafoGraphEdges edge_array = {{0, 1}, {0, 2}, {2, 3}}; + pgm_tap::TrafoGraphEdgeProperties edge_prop{{{0, 1}, 1}, {{-1, -1}, 2}, {{2, 3}, 3}}; + std::vector vertex_props{{true}, {false}, {false}, {false}}; + + pgm_tap::TransformerGraph g{boost::edges_are_unsorted_multi_pass, edge_array.cbegin(), edge_array.cend(), + edge_prop.cbegin(), 4}; + + // Vertex properties can not be set during graph creation + boost::graph_traits::vertex_iterator vi, vi_end; + for (boost::tie(vi, vi_end) = vertices(g); vi != vi_end; ++vi) { + g[*vi].is_source = vertex_props[*vi].is_source; + } + + const pgm_tap::TrafoGraphEdgeProperties regulated_edge_weights = get_edge_weights(g); + const pgm_tap::TrafoGraphEdgeProperties ref_regulated_edge_weights{{{0, 1}, 0}, {{2, 3}, 2}}; + CHECK(regulated_edge_weights == ref_regulated_edge_weights); } SUBCASE("Sorting transformer edges") { - pgm_tap::WeightedTrafoList trafoList{{Idx2D{1, 1}, pgm_tap::infty}, - {Idx2D{1, 2}, 5}, - {Idx2D{1, 3}, 4}, - {Idx2D{2, 1}, 4}, - {Idx2D{2, 2}, 3}, - {Idx2D{3, 1}, 2}, - {Idx2D{3, 2}, 1}, - {Idx2D{3, 3}, 1}}; - - const pgm_tap::RankedTransformerGroups referenceList{{Idx2D{3, 2}, Idx2D{3, 3}}, {Idx2D{3, 1}}, {Idx2D{2, 2}}, - {Idx2D{1, 3}, Idx2D{2, 1}}, {Idx2D{1, 2}}, {Idx2D{1, 1}}}; - - auto const sortedTrafoList = pgm_tap::rank_transformers(trafoList); + pgm_tap::TrafoGraphEdgeProperties trafoList{ + {Idx2D{1, 1}, pgm_tap::infty}, {Idx2D{1, 2}, 5}, {Idx2D{1, 3}, 4}, {Idx2D{2, 1}, 4}}; + + const pgm_tap::RankedTransformerGroups referenceList{Idx2D{1, 3}, Idx2D{2, 1}, Idx2D{1, 2}, Idx2D{1, 1}}; + + const pgm_tap::RankedTransformerGroups sortedTrafoList = pgm_tap::rank_transformers(trafoList); CHECK(sortedTrafoList == referenceList); } + + SUBCASE("Ranking complete the graph") { + pgm_tap::RankedTransformerGroups order = pgm_tap::rank_transformers(state); + pgm_tap::RankedTransformerGroups ref_order{Idx2D{3, 0}, Idx2D{3, 1}, Idx2D{4, 0}, + Idx2D{3, 3}, Idx2D{3, 2}, Idx2D{3, 4}}; + CHECK(order == ref_order); + } } TEST_CASE("Test Tap position optimizer" * doctest::skip(true)) { // TODO: Implement unit tests for the tap position optimizer } - +TEST_SUITE_END(); } // namespace power_grid_model \ No newline at end of file diff --git a/tests/unit/test_error_handling.py b/tests/unit/test_error_handling.py index 5d15f226f..501df75b0 100644 --- a/tests/unit/test_error_handling.py +++ b/tests/unit/test_error_handling.py @@ -10,6 +10,7 @@ from power_grid_model.core.power_grid_meta import initialize_array from power_grid_model.enum import CalculationMethod, LoadGenType, MeasuredTerminalType from power_grid_model.errors import ( + AutomaticTapCalculationError, ConflictID, ConflictVoltage, IDWrongType, @@ -205,6 +206,45 @@ def test_handle_invalid_calculation_method_error(): model.calculate_power_flow(calculation_method=CalculationMethod.iec60909) +@pytest.mark.xfail(reason="TODO: Automatic tap changer") +def test_transformer_tap_regulator_at_lv_tap_side(): + node_input = initialize_array("input", "node", 2) + node_input["id"] = [0, 1] + node_input["u_rated"] = [1e4, 4e2] + + source_input = initialize_array("input", "source", 1) + source_input["id"] = [2] + source_input["node"] = [0] + source_input["status"] = [1] + source_input["u_ref"] = [10.0e3] + + transformer_input = initialize_array("input", "transformer", 1) + transformer_input["id"] = [3] + transformer_input["from_node"] = [0] + transformer_input["to_node"] = [1] + transformer_input["from_status"] = [1] + transformer_input["to_status"] = [1] + transformer_input["u1"] = [1e4] + transformer_input["u2"] = [4e2] + transformer_input["sn"] = [1e5] + transformer_input["uk"] = [0.1] + transformer_input["pk"] = [1e3] + transformer_input["i0"] = [1.0e-6] + transformer_input["p0"] = [0.1] + transformer_input["winding_from"] = [2] + transformer_input["winding_to"] = [1] + transformer_input["clock"] = [5] + transformer_input["tap_side"] = [1] + transformer_input["tap_pos"] = [3] + transformer_input["tap_min"] = [-11] + transformer_input["tap_max"] = [9] + transformer_input["tap_size"] = [100] + + model = PowerGridModel(input_data={"node": node_input, "transformer": transformer_input, "source": source_input}) + with pytest.raises(AutomaticTapCalculationError): + model.calculate_power_flow() + + @pytest.mark.skip(reason="TODO") def test_handle_power_grid_dataset_error(): pass diff --git a/tests/unit/utils.py b/tests/unit/utils.py index 916524b61..486753393 100644 --- a/tests/unit/utils.py +++ b/tests/unit/utils.py @@ -14,6 +14,7 @@ from power_grid_model.core.power_grid_model import PowerGridModel from power_grid_model.data_types import Dataset, PythonDataset, SingleDataset from power_grid_model.errors import ( + AutomaticTapCalculationError, ConflictID, ConflictVoltage, IDWrongType, @@ -50,6 +51,7 @@ InvalidCalculationMethod, InvalidMeasuredObject, InvalidRegulatedObject, + AutomaticTapCalculationError, InvalidTransformerClock, NotObservableError, PowerGridSerializationError,