diff --git a/code_generation/data/dataset_class_maps/dataset_definitions.json b/code_generation/data/dataset_class_maps/dataset_definitions.json index 9f38a8ee8..5574337d5 100644 --- a/code_generation/data/dataset_class_maps/dataset_definitions.json +++ b/code_generation/data/dataset_class_maps/dataset_definitions.json @@ -20,6 +20,10 @@ "names": ["transformer"], "class_name": "TransformerInput" }, + { + "names": ["transformer_tap_regulator"], + "class_name": "TransformerTapRegulatorInput" + }, { "names": ["three_winding_transformer"], "class_name": "ThreeWindingTransformerInput" @@ -62,6 +66,10 @@ "names": ["line", "link", "transformer"], "class_name": "BranchOutput" }, + { + "names": ["transformer_tap_regulator"], + "class_name": "TransformerTapRegulatorOutput" + }, { "names": ["three_winding_transformer"], "class_name": "Branch3Output" @@ -104,6 +112,10 @@ "names": ["transformer"], "class_name": "TransformerUpdate" }, + { + "names": ["transformer_tap_regulator"], + "class_name": "TransformerTapRegulatorUpdate" + }, { "names": ["three_winding_transformer"], "class_name": "ThreeWindingTransformerUpdate" diff --git a/docs/user_manual/components.md b/docs/user_manual/components.md index 61533469c..2f9a107d6 100644 --- a/docs/user_manual/components.md +++ b/docs/user_manual/components.md @@ -181,7 +181,7 @@ levels. An example of usage of transformer is given in [Transformer Examples](.. | `winding_to` | {py:class}`WindingType ` | - | to-side winding type | ✔ | ❌ | | | `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. | ✔ | ❌ | `>= 0` and `<= 12` | | `tap_side` | {py:class}`BranchSide ` | - | side of tap changer | ✔ | ❌ | | -| `tap_pos` | `int8_t` | - | current position of tap changer | ✔ | ✔ | `(tap_min <= tap_pos <= tap_max)` or `(tap_min >= tap_pos >= tap_max)` | +| `tap_pos` | `int8_t` | - | current position of tap changer | ❌ default `tap_nom`, if no `tap_nom` default `0` | ✔ | `(tap_min <= tap_pos <= tap_max)` or `(tap_min >= tap_pos >= tap_max)` | | `tap_min` | `int8_t` | - | position of tap changer at minimum voltage | ✔ | ❌ | | | `tap_max` | `int8_t` | - | position of tap changer at maximum voltage | ✔ | ❌ | | | `tap_nom` | `int8_t` | - | nominal position of tap changer | ❌ default `0` | ❌ | `(tap_min <= tap_nom <= tap_max)` or `(tap_min >= tap_nom >= tap_max)` | @@ -301,7 +301,7 @@ voltage levels. An example of usage of three-winding transformer is given in [Tr | `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. | ✔ | ❌ | `>= 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. | ✔ | ❌ | `>= 0` and `<= 12` | | `tap_side` | {py:class}`Branch3Side ` | - | side of tap changer | ✔ | ❌ | `side_1` or `side_2` or `side_3` | -| `tap_pos` | `int8_t` | - | current position of tap changer | ✔ | ✔ | `(tap_min <= tap_pos <= tap_max)` or `(tap_min >= tap_pos >= tap_max)` | +| `tap_pos` | `int8_t` | - | current position of tap changer | ❌ default `tap_nom`, if no `tap_nom` default `0` | ✔ | `(tap_min <= tap_pos <= tap_max)` or `(tap_min >= tap_pos >= tap_max)` | | `tap_min` | `int8_t` | - | position of tap changer at minimum voltage | ✔ | ❌ | | | `tap_max` | `int8_t` | - | position of tap changer at maximum voltage | ✔ | ❌ | | | `tap_nom` | `int8_t` | - | nominal position of tap changer | ❌ default `0` | ❌ | `(tap_min <= tap_nom <= tap_max)` or `(tap_min >= tap_nom >= tap_max)` | diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/component/three_winding_transformer.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/component/three_winding_transformer.hpp index 8284d9e7d..0f644b3e6 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/component/three_winding_transformer.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/component/three_winding_transformer.hpp @@ -42,12 +42,11 @@ class ThreeWindingTransformer : public Branch3 { clock_12_{three_winding_transformer_input.clock_12}, clock_13_{three_winding_transformer_input.clock_13}, tap_side_{three_winding_transformer_input.tap_side}, - tap_pos_{three_winding_transformer_input.tap_pos}, tap_min_{three_winding_transformer_input.tap_min}, tap_max_{three_winding_transformer_input.tap_max}, - tap_nom_{three_winding_transformer_input.tap_nom == na_IntS ? (IntS)0 + tap_nom_{three_winding_transformer_input.tap_nom == na_IntS ? IntS{0} : three_winding_transformer_input.tap_nom}, - tap_direction_{tap_max_ > tap_min_ ? (IntS)1 : (IntS)-1}, + tap_direction_{tap_max_ > tap_min_ ? IntS{1} : IntS{-1}}, tap_size_{three_winding_transformer_input.tap_size}, uk_12_min_{is_nan(three_winding_transformer_input.uk_12_min) ? uk_12_ : three_winding_transformer_input.uk_12_min}, @@ -79,6 +78,14 @@ class ThreeWindingTransformer : public Branch3 { z_grounding_1_{three_winding_transformer_input.r_grounding_1, three_winding_transformer_input.x_grounding_1}, z_grounding_2_{three_winding_transformer_input.r_grounding_2, three_winding_transformer_input.x_grounding_2}, z_grounding_3_{three_winding_transformer_input.r_grounding_3, three_winding_transformer_input.x_grounding_3} { + // init tap_pos_ linter smell + if (three_winding_transformer_input.tap_pos == na_IntS) { + tap_pos_ = + three_winding_transformer_input.tap_nom == na_IntS ? IntS{0} : three_winding_transformer_input.tap_nom; + } else { + tap_pos_ = three_winding_transformer_input.tap_pos; + } + if (!is_valid_clock(clock_12_, winding_1_, winding_2_)) { throw InvalidTransformerClock{id(), clock_12_}; } diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/component/transformer.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/component/transformer.hpp index 19c91a987..d3aaddd25 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/component/transformer.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/component/transformer.hpp @@ -37,11 +37,10 @@ class Transformer : public Branch { winding_to_{transformer_input.winding_to}, clock_{transformer_input.clock}, tap_side_{transformer_input.tap_side}, - tap_pos_{transformer_input.tap_pos}, tap_min_{transformer_input.tap_min}, tap_max_{transformer_input.tap_max}, - tap_nom_{transformer_input.tap_nom == na_IntS ? (IntS)0 : transformer_input.tap_nom}, - tap_direction_{tap_max_ > tap_min_ ? (IntS)1 : (IntS)-1}, + tap_nom_{transformer_input.tap_nom == na_IntS ? IntS{0} : transformer_input.tap_nom}, + tap_direction_{tap_max_ > tap_min_ ? IntS{1} : IntS{-1}}, uk_min_{is_nan(transformer_input.uk_min) ? uk_ : transformer_input.uk_min}, uk_max_{is_nan(transformer_input.uk_max) ? uk_ : transformer_input.uk_max}, pk_min_{is_nan(transformer_input.pk_min) ? pk_ : transformer_input.pk_min}, @@ -53,6 +52,13 @@ class Transformer : public Branch { calculate_z_pu(transformer_input.r_grounding_from, transformer_input.x_grounding_from, u1_rated)}, z_grounding_to_{ calculate_z_pu(transformer_input.r_grounding_to, transformer_input.x_grounding_to, u2_rated)} { + // init tap_pos_ linter smell + if (transformer_input.tap_pos == na_IntS) { + tap_pos_ = transformer_input.tap_nom == na_IntS ? IntS{0} : transformer_input.tap_nom; + } else { + tap_pos_ = transformer_input.tap_pos; + } + if (!is_valid_clock(clock_, winding_from_, winding_to_)) { throw InvalidTransformerClock{id(), clock_}; } diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/sparse_ordering.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/sparse_ordering.hpp new file mode 100644 index 000000000..41c9622fd --- /dev/null +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/sparse_ordering.hpp @@ -0,0 +1,221 @@ +// SPDX-FileCopyrightText: Contributors to the Power Grid Model project +// +// SPDX-License-Identifier: MPL-2.0 + +#pragma once + +#include "common/common.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace power_grid_model { + +namespace detail { +class DegreeLookup { + public: + void set(Idx u, Idx degree) { + if (auto degree_it = vertex_to_degree.find(u); degree_it != vertex_to_degree.end()) { + remove_degree(u, degree_it->second); + degree_it->second = degree; + } else { + vertex_to_degree.try_emplace(u, degree); + } + degrees_to_vertex[degree].insert(u); + } + + void erase(Idx u) { + auto vertex_it = vertex_to_degree.find(u); + if (vertex_it == vertex_to_degree.end()) { + return; + } + Idx const degree = vertex_it->second; + vertex_to_degree.erase(vertex_it); + remove_degree(u, degree); + } + + friend auto min_element(DegreeLookup const& dgd); + + private: + void remove_degree(Idx u, Idx degree) { + if (auto degree_it = degrees_to_vertex.find(degree); degree_it != degrees_to_vertex.end()) { + degree_it->second.erase(u); + if (degree_it->second.empty()) { + degrees_to_vertex.erase(degree_it); + } + } + } + + std::map vertex_to_degree; + std::map> degrees_to_vertex; +}; + +inline auto min_element(DegreeLookup const& dgd) { + Idx const vertex = *dgd.degrees_to_vertex.begin()->second.begin(); + return dgd.vertex_to_degree.find(vertex); +} + +inline void remove_element_degree(Idx u, DegreeLookup& dgd) { dgd.erase(u); } + +inline void set_element_degree(Idx u, Idx degree, DegreeLookup& dgd) { dgd.set(u, degree); } + +inline Idx num_adjacent(Idx const u, std::map const& d) { + if (auto it = d.find(u); it != d.end()) { + return static_cast(it->second.size()); + } + return 0; +} + +inline IdxVector const& adj(Idx const u, std::map const& d) { return d.at(u); } + +inline std::vector> comp_size_degrees_graph(std::map const& d) { + DegreeLookup dd; + IdxVector v; + + for (auto const& [k, adjacent] : d) { + v.push_back(k); + set_element_degree(k, static_cast(adjacent.size()), dd); + } + + return {{d.size(), dd}}; +} + +inline std::map make_clique(IdxVector& l) { + std::map d; + + for (Idx i = 0; i < static_cast(l.size()); i++) { + IdxVector sl(l.size() - 1); + std::copy(l.begin(), l.begin() + i, sl.begin()); + std::copy(l.begin() + i + 1, l.end(), sl.begin() + i); + d[l[i]] = std::move(sl); + } + + return d; +} + +inline std::vector> check_indistguishable(Idx const u, + std::map const& d) { + IdxVector rl; + + auto l = adj(u, d); + auto lu = l; + lu.push_back(u); + std::ranges::sort(lu); + + for (auto const& v : l) { + auto lv = adj(v, d); + lv.push_back(v); + std::ranges::sort(lv); + if (lu == lv) { + rl.push_back(v); + } + } + + return {{l, rl}}; +} + +inline bool in_graph(std::pair const& e, std::map const& d) { + if (auto edges_it = d.find(e.first); + edges_it != d.cend() && std::ranges::find(edges_it->second, e.second) != edges_it->second.cend()) { + return true; + } + return false; +} + +inline IdxVector remove_vertices_update_degrees(Idx const u, std::map& d, DegreeLookup& dgd, + std::vector>& fills) { + std::vector> nbsrl = check_indistguishable(u, d); + auto& [nbs, rl] = nbsrl[0]; + IdxVector alpha = rl; + std::map dd; + + rl.push_back(u); + + for (auto uu : rl) { + if (uu != u) { + std::erase(nbs, uu); + } + + remove_element_degree(uu, dgd); + IdxVector el; + for (auto e : d[uu]) { + auto& adjacents = d[e]; + std::erase(adjacents, uu); + if (adjacents.empty()) { + el.push_back(e); + } + } + + el.push_back(uu); + + for (auto const& it : el) { + d.erase(it); + } + } + + dd = make_clique(nbs); + + for (auto const& [k, adjacent] : dd) { + auto it = d.find(k); + for (Idx const e : adjacent) { + if (!in_graph(std::make_pair(k, e), d)) { + if (it == d.end()) { + std::tie(it, std::ignore) = d.try_emplace(k); + } + it->second.push_back(e); + d[e].push_back(k); + fills.emplace_back(k, e); + } + } + } + + for (auto const& e : nbs) { + set_element_degree(e, num_adjacent(e, d), dgd); + } + + return alpha; +} +} // namespace detail + +inline std::pair>> minimum_degree_ordering(std::map d) { + // make symmetric + for (auto const& [k, adjacent] : d) { + for (auto e : adjacent) { + d[e].push_back(k); + } + } + for (auto& [k, adjacent] : d) { + std::set const unique_sorted_adjacent{adjacent.begin(), adjacent.end()}; + adjacent = IdxVector{unique_sorted_adjacent.begin(), unique_sorted_adjacent.end()}; + } + + auto data = detail::comp_size_degrees_graph(d); + auto& [n, dgd] = data[0]; + + IdxVector alpha; + std::vector> fills; + + for (Idx k = 0; k < n; ++k) { + Idx const u = get<0>(*detail::min_element(dgd)); + alpha.push_back(u); + if (d.size() == 2) { + assert(d.begin()->second.size() == 1); + + Idx const from = d.begin()->first; + Idx const to = d.begin()->second[0]; + alpha.push_back(alpha.back() == from ? to : from); + return {alpha, fills}; + } + std::ranges::copy(detail::remove_vertices_update_degrees(u, d, dgd, fills), std::back_inserter(alpha)); + if (d.empty()) { + return {alpha, fills}; + } + } + return {alpha, fills}; +} +} // namespace power_grid_model diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/topology.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/topology.hpp index 6bcb7ea5a..f5d019796 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/topology.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/topology.hpp @@ -9,12 +9,12 @@ #include "common/enum.hpp" #include "common/exception.hpp" #include "index_mapping.hpp" +#include "sparse_ordering.hpp" #include #include #include #include -#include // build topology of the grid // divide grid into several math models @@ -284,6 +284,7 @@ class Topology { // loop all back edges assign all nodes before the back edges as inside cycle for (auto const& back_edge : back_edges) { GraphIdx node_in_cycle = back_edge.first; + // loop back from source in the predecessor tree // stop if it is already marked as in cycle while (node_status_[node_in_cycle] != -2) { @@ -300,105 +301,43 @@ class Topology { std::vector cyclic_node; std::copy_if(dfs_node_copy.cbegin(), dfs_node_copy.cend(), std::back_inserter(cyclic_node), [this](Idx x) { return node_status_[x] == -2; }); - GraphIdx const n_cycle_node = cyclic_node.size(); + // reorder does not make sense if number of cyclic nodes in a sub graph is smaller than 4 - if (n_cycle_node < 4) { + if (cyclic_node.size() < 4) { std::copy(cyclic_node.crbegin(), cyclic_node.crend(), std::back_inserter(dfs_node)); return fill_in; } - // assign temporary bus number as increasing from 0, 1, 2, ..., n_cycle_node - 1 - for (GraphIdx i = 0; i != n_cycle_node; ++i) { - node_status_[cyclic_node[i]] = static_cast(i); + std::map> unique_nearest_neighbours; + for (Idx const node_idx : cyclic_node) { + auto predecessor = static_cast(predecessors_[node_idx]); + if (predecessor != node_idx) { + unique_nearest_neighbours[node_idx] = {predecessor}; + } } - // build graph lambda - auto const build_graph = [&](ReorderGraph& g) { - // add edges - for (GraphIdx i = 0; i != n_cycle_node; ++i) { - // loop all edges of vertex i - auto const global_i = static_cast(cyclic_node[i]); - BGL_FORALL_ADJ(global_i, global_j, global_graph_, GlobalGraph) { - // skip if j is not part of cyclic sub graph - if (node_status_[global_j] == -1) { - continue; - } - auto const j = static_cast(node_status_[global_j]); - if (!boost::edge(i, j, g).second) { - boost::add_edge(i, j, g); - } - } + for (auto const& [from_node, to_node] : back_edges) { + auto const from{static_cast(from_node)}; + auto const to{static_cast(to_node)}; + if (!detail::in_graph(std::pair{from, to}, unique_nearest_neighbours)) { + unique_nearest_neighbours[from].push_back(to); } - }; - ReorderGraph meshed_graph{n_cycle_node}; - build_graph(meshed_graph); - // start minimum degree ordering - std::vector> perm(n_cycle_node); - std::vector> inverse_perm(n_cycle_node); - std::vector> degree(n_cycle_node); - std::vector> supernode_sizes(n_cycle_node, 1); - boost::vec_adj_list_vertex_id_map> const id{}; - int const delta = 0; - boost::minimum_degree_ordering(meshed_graph, boost::make_iterator_property_map(degree.begin(), id), - boost::make_iterator_property_map(inverse_perm.begin(), id), - boost::make_iterator_property_map(perm.begin(), id), - boost::make_iterator_property_map(supernode_sizes.begin(), id), delta, id); - // re-order cyclic node - std::vector const cyclic_node_copy{cyclic_node}; - for (GraphIdx i = 0; i != n_cycle_node; ++i) { - cyclic_node[i] = cyclic_node_copy[perm[i]]; } - // copy back to dfs node - std::copy(cyclic_node.cbegin(), cyclic_node.cend(), std::back_inserter(dfs_node)); - // analyze and record fill-ins - // re-assign temporary bus number as increasing from 0, 1, 2, ..., n_cycle_node - 1 - for (GraphIdx i = 0; i != n_cycle_node; ++i) { - node_status_[cyclic_node[i]] = static_cast(i); + auto [reordered, fills] = minimum_degree_ordering(std::move(unique_nearest_neighbours)); + + const auto n_non_cyclic_nodes = static_cast(dfs_node.size()); + std::map permuted_node_indices; + for (Idx idx = 0; idx < static_cast(reordered.size()); ++idx) { + permuted_node_indices[reordered[idx]] = n_non_cyclic_nodes + idx; } - // re-build graph with reordered cyclic node - meshed_graph.clear(); - meshed_graph = ReorderGraph{n_cycle_node}; - build_graph(meshed_graph); - // begin to remove vertices from graph, create fill-ins - BGL_FORALL_VERTICES(i, meshed_graph, ReorderGraph) { - // double loop to loop all pairs of adjacent vertices - BGL_FORALL_ADJ(i, j1, meshed_graph, ReorderGraph) { - // skip for already removed vertices - if (j1 < i) { - continue; - } - BGL_FORALL_ADJ(i, j2, meshed_graph, ReorderGraph) { - // no self edges - assert(i != j1); - assert(i != j2); - // skip for already removed vertices - if (j2 < i) { - continue; - } - // only keep pair with j1 < j2 - if (j1 >= j2) { - continue; - } - // if edge j1 -> j2 does not already exists - // it is a fill-in - if (!boost::edge(j1, j2, meshed_graph).second) { - // anti edge should also not exist - assert(!boost::edge(j2, j1, meshed_graph).second); - // add both edges to the graph - boost::add_edge(j1, j2, meshed_graph); - boost::add_edge(j2, j1, meshed_graph); - // add to fill-in - fill_in.push_back({static_cast(j1), static_cast(j2)}); - } - } - } + + std::ranges::copy(reordered, std::back_inserter(dfs_node)); + for (auto [from, to] : fills) { + auto from_reordered = permuted_node_indices[from]; + auto to_reordered = permuted_node_indices[to]; + fill_in.push_back({from_reordered, to_reordered}); } - // offset fill-in indices by n_node - n_cycle_node - auto const offset = static_cast(dfs_node.size() - n_cycle_node); - std::for_each(fill_in.begin(), fill_in.end(), [offset](BranchIdx& b) { - b[0] += offset; - b[1] += offset; - }); + return fill_in; } @@ -418,14 +357,13 @@ class Topology { Idx2D const i_math = comp_coup_.node[i]; Idx2D const j_math = comp_coup_.node[j]; Idx const math_group = [&]() { - Idx group = -1; if (i_status != 0 && i_math.group != -1) { - group = i_math.group; + return i_math.group; } if (j_status != 0 && j_math.group != -1) { - group = j_math.group; + return j_math.group; } - return group; + return Idx{-1}; }(); // skip if no math model connected if (math_group == -1) { @@ -687,6 +625,6 @@ class Topology { {comp_topo_.power_sensor_object_idx, comp_coup_.node}, comp_coup_.power_sensor, [this](Idx i) { return comp_topo_.power_sensor_terminal_type[i] == MeasuredTerminalType::node; }); } -}; // namespace power_grid_model +}; } // namespace power_grid_model diff --git a/power_grid_model_c/power_grid_model_c/include/power_grid_model_c/dataset_definitions.h b/power_grid_model_c/power_grid_model_c/include/power_grid_model_c/dataset_definitions.h index 10624ca82..4ebd40e85 100644 --- a/power_grid_model_c/power_grid_model_c/include/power_grid_model_c/dataset_definitions.h +++ b/power_grid_model_c/power_grid_model_c/include/power_grid_model_c/dataset_definitions.h @@ -87,6 +87,17 @@ PGM_API extern PGM_MetaAttribute const* const PGM_def_input_transformer_r_ground PGM_API extern PGM_MetaAttribute const* const PGM_def_input_transformer_x_grounding_from; PGM_API extern PGM_MetaAttribute const* const PGM_def_input_transformer_r_grounding_to; PGM_API extern PGM_MetaAttribute const* const PGM_def_input_transformer_x_grounding_to; +// component transformer_tap_regulator +PGM_API extern PGM_MetaComponent const* const PGM_def_input_transformer_tap_regulator; +// attributes of input transformer_tap_regulator +PGM_API extern PGM_MetaAttribute const* const PGM_def_input_transformer_tap_regulator_id; +PGM_API extern PGM_MetaAttribute const* const PGM_def_input_transformer_tap_regulator_regulated_object; +PGM_API extern PGM_MetaAttribute const* const PGM_def_input_transformer_tap_regulator_status; +PGM_API extern PGM_MetaAttribute const* const PGM_def_input_transformer_tap_regulator_control_side; +PGM_API extern PGM_MetaAttribute const* const PGM_def_input_transformer_tap_regulator_u_set; +PGM_API extern PGM_MetaAttribute const* const PGM_def_input_transformer_tap_regulator_u_band; +PGM_API extern PGM_MetaAttribute const* const PGM_def_input_transformer_tap_regulator_line_drop_compensation_r; +PGM_API extern PGM_MetaAttribute const* const PGM_def_input_transformer_tap_regulator_line_drop_compensation_x; // component three_winding_transformer PGM_API extern PGM_MetaComponent const* const PGM_def_input_three_winding_transformer; // attributes of input three_winding_transformer @@ -300,6 +311,12 @@ PGM_API extern PGM_MetaAttribute const* const PGM_def_sym_output_transformer_p_t PGM_API extern PGM_MetaAttribute const* const PGM_def_sym_output_transformer_q_to; PGM_API extern PGM_MetaAttribute const* const PGM_def_sym_output_transformer_i_to; PGM_API extern PGM_MetaAttribute const* const PGM_def_sym_output_transformer_s_to; +// component transformer_tap_regulator +PGM_API extern PGM_MetaComponent const* const PGM_def_sym_output_transformer_tap_regulator; +// attributes of sym_output transformer_tap_regulator +PGM_API extern PGM_MetaAttribute const* const PGM_def_sym_output_transformer_tap_regulator_id; +PGM_API extern PGM_MetaAttribute const* const PGM_def_sym_output_transformer_tap_regulator_energized; +PGM_API extern PGM_MetaAttribute const* const PGM_def_sym_output_transformer_tap_regulator_tap_pos; // component three_winding_transformer PGM_API extern PGM_MetaComponent const* const PGM_def_sym_output_three_winding_transformer; // attributes of sym_output three_winding_transformer @@ -466,6 +483,12 @@ PGM_API extern PGM_MetaAttribute const* const PGM_def_asym_output_transformer_p_ PGM_API extern PGM_MetaAttribute const* const PGM_def_asym_output_transformer_q_to; PGM_API extern PGM_MetaAttribute const* const PGM_def_asym_output_transformer_i_to; PGM_API extern PGM_MetaAttribute const* const PGM_def_asym_output_transformer_s_to; +// component transformer_tap_regulator +PGM_API extern PGM_MetaComponent const* const PGM_def_asym_output_transformer_tap_regulator; +// attributes of asym_output transformer_tap_regulator +PGM_API extern PGM_MetaAttribute const* const PGM_def_asym_output_transformer_tap_regulator_id; +PGM_API extern PGM_MetaAttribute const* const PGM_def_asym_output_transformer_tap_regulator_energized; +PGM_API extern PGM_MetaAttribute const* const PGM_def_asym_output_transformer_tap_regulator_tap_pos; // component three_winding_transformer PGM_API extern PGM_MetaComponent const* const PGM_def_asym_output_three_winding_transformer; // attributes of asym_output three_winding_transformer @@ -603,6 +626,15 @@ PGM_API extern PGM_MetaAttribute const* const PGM_def_update_transformer_id; PGM_API extern PGM_MetaAttribute const* const PGM_def_update_transformer_from_status; PGM_API extern PGM_MetaAttribute const* const PGM_def_update_transformer_to_status; PGM_API extern PGM_MetaAttribute const* const PGM_def_update_transformer_tap_pos; +// component transformer_tap_regulator +PGM_API extern PGM_MetaComponent const* const PGM_def_update_transformer_tap_regulator; +// attributes of update transformer_tap_regulator +PGM_API extern PGM_MetaAttribute const* const PGM_def_update_transformer_tap_regulator_id; +PGM_API extern PGM_MetaAttribute const* const PGM_def_update_transformer_tap_regulator_status; +PGM_API extern PGM_MetaAttribute const* const PGM_def_update_transformer_tap_regulator_u_set; +PGM_API extern PGM_MetaAttribute const* const PGM_def_update_transformer_tap_regulator_u_band; +PGM_API extern PGM_MetaAttribute const* const PGM_def_update_transformer_tap_regulator_line_drop_compensation_r; +PGM_API extern PGM_MetaAttribute const* const PGM_def_update_transformer_tap_regulator_line_drop_compensation_x; // component three_winding_transformer PGM_API extern PGM_MetaComponent const* const PGM_def_update_three_winding_transformer; // attributes of update three_winding_transformer diff --git a/power_grid_model_c/power_grid_model_c/src/dataset_definitions.cpp b/power_grid_model_c/power_grid_model_c/src/dataset_definitions.cpp index 250963b82..70bd343b9 100644 --- a/power_grid_model_c/power_grid_model_c/src/dataset_definitions.cpp +++ b/power_grid_model_c/power_grid_model_c/src/dataset_definitions.cpp @@ -76,6 +76,17 @@ PGM_MetaAttribute const* const PGM_def_input_transformer_r_grounding_from = PGM_ PGM_MetaAttribute const* const PGM_def_input_transformer_x_grounding_from = PGM_meta_get_attribute_by_name(nullptr, "input", "transformer", "x_grounding_from"); PGM_MetaAttribute const* const PGM_def_input_transformer_r_grounding_to = PGM_meta_get_attribute_by_name(nullptr, "input", "transformer", "r_grounding_to"); PGM_MetaAttribute const* const PGM_def_input_transformer_x_grounding_to = PGM_meta_get_attribute_by_name(nullptr, "input", "transformer", "x_grounding_to"); +// component transformer_tap_regulator +PGM_MetaComponent const* const PGM_def_input_transformer_tap_regulator = PGM_meta_get_component_by_name(nullptr, "input", "transformer_tap_regulator"); +// attributes of input transformer_tap_regulator +PGM_MetaAttribute const* const PGM_def_input_transformer_tap_regulator_id = PGM_meta_get_attribute_by_name(nullptr, "input", "transformer_tap_regulator", "id"); +PGM_MetaAttribute const* const PGM_def_input_transformer_tap_regulator_regulated_object = PGM_meta_get_attribute_by_name(nullptr, "input", "transformer_tap_regulator", "regulated_object"); +PGM_MetaAttribute const* const PGM_def_input_transformer_tap_regulator_status = PGM_meta_get_attribute_by_name(nullptr, "input", "transformer_tap_regulator", "status"); +PGM_MetaAttribute const* const PGM_def_input_transformer_tap_regulator_control_side = PGM_meta_get_attribute_by_name(nullptr, "input", "transformer_tap_regulator", "control_side"); +PGM_MetaAttribute const* const PGM_def_input_transformer_tap_regulator_u_set = PGM_meta_get_attribute_by_name(nullptr, "input", "transformer_tap_regulator", "u_set"); +PGM_MetaAttribute const* const PGM_def_input_transformer_tap_regulator_u_band = PGM_meta_get_attribute_by_name(nullptr, "input", "transformer_tap_regulator", "u_band"); +PGM_MetaAttribute const* const PGM_def_input_transformer_tap_regulator_line_drop_compensation_r = PGM_meta_get_attribute_by_name(nullptr, "input", "transformer_tap_regulator", "line_drop_compensation_r"); +PGM_MetaAttribute const* const PGM_def_input_transformer_tap_regulator_line_drop_compensation_x = PGM_meta_get_attribute_by_name(nullptr, "input", "transformer_tap_regulator", "line_drop_compensation_x"); // component three_winding_transformer PGM_MetaComponent const* const PGM_def_input_three_winding_transformer = PGM_meta_get_component_by_name(nullptr, "input", "three_winding_transformer"); // attributes of input three_winding_transformer @@ -289,6 +300,12 @@ PGM_MetaAttribute const* const PGM_def_sym_output_transformer_p_to = PGM_meta_ge PGM_MetaAttribute const* const PGM_def_sym_output_transformer_q_to = PGM_meta_get_attribute_by_name(nullptr, "sym_output", "transformer", "q_to"); PGM_MetaAttribute const* const PGM_def_sym_output_transformer_i_to = PGM_meta_get_attribute_by_name(nullptr, "sym_output", "transformer", "i_to"); PGM_MetaAttribute const* const PGM_def_sym_output_transformer_s_to = PGM_meta_get_attribute_by_name(nullptr, "sym_output", "transformer", "s_to"); +// component transformer_tap_regulator +PGM_MetaComponent const* const PGM_def_sym_output_transformer_tap_regulator = PGM_meta_get_component_by_name(nullptr, "sym_output", "transformer_tap_regulator"); +// attributes of sym_output transformer_tap_regulator +PGM_MetaAttribute const* const PGM_def_sym_output_transformer_tap_regulator_id = PGM_meta_get_attribute_by_name(nullptr, "sym_output", "transformer_tap_regulator", "id"); +PGM_MetaAttribute const* const PGM_def_sym_output_transformer_tap_regulator_energized = PGM_meta_get_attribute_by_name(nullptr, "sym_output", "transformer_tap_regulator", "energized"); +PGM_MetaAttribute const* const PGM_def_sym_output_transformer_tap_regulator_tap_pos = PGM_meta_get_attribute_by_name(nullptr, "sym_output", "transformer_tap_regulator", "tap_pos"); // component three_winding_transformer PGM_MetaComponent const* const PGM_def_sym_output_three_winding_transformer = PGM_meta_get_component_by_name(nullptr, "sym_output", "three_winding_transformer"); // attributes of sym_output three_winding_transformer @@ -455,6 +472,12 @@ PGM_MetaAttribute const* const PGM_def_asym_output_transformer_p_to = PGM_meta_g PGM_MetaAttribute const* const PGM_def_asym_output_transformer_q_to = PGM_meta_get_attribute_by_name(nullptr, "asym_output", "transformer", "q_to"); PGM_MetaAttribute const* const PGM_def_asym_output_transformer_i_to = PGM_meta_get_attribute_by_name(nullptr, "asym_output", "transformer", "i_to"); PGM_MetaAttribute const* const PGM_def_asym_output_transformer_s_to = PGM_meta_get_attribute_by_name(nullptr, "asym_output", "transformer", "s_to"); +// component transformer_tap_regulator +PGM_MetaComponent const* const PGM_def_asym_output_transformer_tap_regulator = PGM_meta_get_component_by_name(nullptr, "asym_output", "transformer_tap_regulator"); +// attributes of asym_output transformer_tap_regulator +PGM_MetaAttribute const* const PGM_def_asym_output_transformer_tap_regulator_id = PGM_meta_get_attribute_by_name(nullptr, "asym_output", "transformer_tap_regulator", "id"); +PGM_MetaAttribute const* const PGM_def_asym_output_transformer_tap_regulator_energized = PGM_meta_get_attribute_by_name(nullptr, "asym_output", "transformer_tap_regulator", "energized"); +PGM_MetaAttribute const* const PGM_def_asym_output_transformer_tap_regulator_tap_pos = PGM_meta_get_attribute_by_name(nullptr, "asym_output", "transformer_tap_regulator", "tap_pos"); // component three_winding_transformer PGM_MetaComponent const* const PGM_def_asym_output_three_winding_transformer = PGM_meta_get_component_by_name(nullptr, "asym_output", "three_winding_transformer"); // attributes of asym_output three_winding_transformer @@ -592,6 +615,15 @@ PGM_MetaAttribute const* const PGM_def_update_transformer_id = PGM_meta_get_attr PGM_MetaAttribute const* const PGM_def_update_transformer_from_status = PGM_meta_get_attribute_by_name(nullptr, "update", "transformer", "from_status"); PGM_MetaAttribute const* const PGM_def_update_transformer_to_status = PGM_meta_get_attribute_by_name(nullptr, "update", "transformer", "to_status"); PGM_MetaAttribute const* const PGM_def_update_transformer_tap_pos = PGM_meta_get_attribute_by_name(nullptr, "update", "transformer", "tap_pos"); +// component transformer_tap_regulator +PGM_MetaComponent const* const PGM_def_update_transformer_tap_regulator = PGM_meta_get_component_by_name(nullptr, "update", "transformer_tap_regulator"); +// attributes of update transformer_tap_regulator +PGM_MetaAttribute const* const PGM_def_update_transformer_tap_regulator_id = PGM_meta_get_attribute_by_name(nullptr, "update", "transformer_tap_regulator", "id"); +PGM_MetaAttribute const* const PGM_def_update_transformer_tap_regulator_status = PGM_meta_get_attribute_by_name(nullptr, "update", "transformer_tap_regulator", "status"); +PGM_MetaAttribute const* const PGM_def_update_transformer_tap_regulator_u_set = PGM_meta_get_attribute_by_name(nullptr, "update", "transformer_tap_regulator", "u_set"); +PGM_MetaAttribute const* const PGM_def_update_transformer_tap_regulator_u_band = PGM_meta_get_attribute_by_name(nullptr, "update", "transformer_tap_regulator", "u_band"); +PGM_MetaAttribute const* const PGM_def_update_transformer_tap_regulator_line_drop_compensation_r = PGM_meta_get_attribute_by_name(nullptr, "update", "transformer_tap_regulator", "line_drop_compensation_r"); +PGM_MetaAttribute const* const PGM_def_update_transformer_tap_regulator_line_drop_compensation_x = PGM_meta_get_attribute_by_name(nullptr, "update", "transformer_tap_regulator", "line_drop_compensation_x"); // component three_winding_transformer PGM_MetaComponent const* const PGM_def_update_three_winding_transformer = PGM_meta_get_component_by_name(nullptr, "update", "three_winding_transformer"); // attributes of update three_winding_transformer diff --git a/src/power_grid_model/validation/rules.py b/src/power_grid_model/validation/rules.py index 8033d660b..c15507a8c 100644 --- a/src/power_grid_model/validation/rules.py +++ b/src/power_grid_model/validation/rules.py @@ -286,7 +286,8 @@ def all_between_or_at( # pylint: disable=too-many-arguments field: str, ref_value_1: Union[int, float, str], ref_value_2: Union[int, float, str], - default_value: Optional[Union[np.ndarray, int, float]] = None, + default_value_1: Optional[Union[np.ndarray, int, float]] = None, + default_value_2: Optional[Union[np.ndarray, int, float]] = None, ) -> List[NotBetweenOrAtError]: """ Check that for all records of a particular type of component, the values in the 'field' column are inclusively @@ -306,6 +307,8 @@ def all_between_or_at( # pylint: disable=too-many-arguments default_value: Some values are not required, but will receive a default value in the C++ core. To do a proper input validation, these default values should be included in the validation. It can be a fixed value for the entire column (int/float) or be different for each element (np.ndarray). + default_value_2: Some values can have a double default: the default will be set to another attribute of the + component, but if that attribute is missing, the default will be set to a fixed value. Returns: A list containing zero or one NotBetweenOrAtErrors, listing all ids where the value in the field of interest was @@ -316,7 +319,14 @@ def outside(val: np.ndarray, *ref: np.ndarray) -> np.ndarray: return np.logical_or(np.less(val, np.minimum(*ref)), np.greater(val, np.maximum(*ref))) return none_match_comparison( - data, component, field, outside, (ref_value_1, ref_value_2), NotBetweenOrAtError, default_value + data, + component, + field, + outside, + (ref_value_1, ref_value_2), + NotBetweenOrAtError, + default_value_1, + default_value_2, ) @@ -327,7 +337,8 @@ def none_match_comparison( compare_fn: Callable, ref_value: ComparisonError.RefType, error: Type[CompError] = ComparisonError, # type: ignore - default_value: Optional[Union[np.ndarray, int, float]] = None, + default_value_1: Optional[Union[np.ndarray, int, float]] = None, + default_value_2: Optional[Union[np.ndarray, int, float]] = None, ) -> List[CompError]: # pylint: disable=too-many-arguments """ @@ -347,13 +358,17 @@ def none_match_comparison( default_value: Some values are not required, but will receive a default value in the C++ core. To do a proper input validation, these default values should be included in the validation. It can be a fixed value for the entire column (int/float) or be different for each element (np.ndarray). + default_value_2: Some values can have a double default: the default will be set to another attribute of the + component, but if that attribute is missing, the default will be set to a fixed value. Returns: A list containing zero or one comparison errors (should be a subclass of ComparisonError), listing all ids where the value in the field of interest matched the comparison. """ - if default_value is not None: - set_default_value(data=data, component=component, field=field, default_value=default_value) + if default_value_1 is not None: + set_default_value(data=data, component=component, field=field, default_value=default_value_1) + if default_value_2 is not None: + set_default_value(data=data, component=component, field=field, default_value=default_value_2) component_data = data[component] if isinstance(ref_value, tuple): ref = tuple(eval_expression(component_data, v) for v in ref_value) diff --git a/src/power_grid_model/validation/validation.py b/src/power_grid_model/validation/validation.py index b9a5e1166..12cbfee67 100644 --- a/src/power_grid_model/validation/validation.py +++ b/src/power_grid_model/validation/validation.py @@ -297,7 +297,6 @@ def validate_required_values( "winding_to", "clock", "tap_side", - "tap_pos", "tap_min", "tap_max", "tap_size", @@ -325,7 +324,6 @@ def validate_required_values( "clock_12", "clock_13", "tap_side", - "tap_pos", "tap_min", "tap_max", "tap_size", @@ -539,7 +537,7 @@ def validate_transformer(data: SingleDataset) -> List[ValidationError]: errors += all_between_or_at(data, "transformer", "clock", 0, 12) errors += all_valid_clocks(data, "transformer", "clock", "winding_from", "winding_to") errors += all_valid_enum_values(data, "transformer", "tap_side", BranchSide) - errors += all_between_or_at(data, "transformer", "tap_pos", "tap_min", "tap_max") + errors += all_between_or_at(data, "transformer", "tap_pos", "tap_min", "tap_max", data["transformer"]["tap_nom"], 0) errors += all_between_or_at(data, "transformer", "tap_nom", "tap_min", "tap_max", 0) errors += all_greater_than_or_equal_to_zero(data, "transformer", "tap_size") errors += all_greater_or_equal(data, "transformer", "uk_min", "pk_min/sn", data["transformer"]["uk"]) @@ -597,7 +595,15 @@ def validate_three_winding_transformer(data: SingleDataset) -> List[ValidationEr errors += all_valid_clocks(data, "three_winding_transformer", "clock_12", "winding_1", "winding_2") errors += all_valid_clocks(data, "three_winding_transformer", "clock_13", "winding_1", "winding_3") errors += all_valid_enum_values(data, "three_winding_transformer", "tap_side", Branch3Side) - errors += all_between_or_at(data, "three_winding_transformer", "tap_pos", "tap_min", "tap_max") + errors += all_between_or_at( + data, + "three_winding_transformer", + "tap_pos", + "tap_min", + "tap_max", + data["three_winding_transformer"]["tap_nom"], + 0, + ) errors += all_between_or_at(data, "three_winding_transformer", "tap_nom", "tap_min", "tap_max", 0) errors += all_greater_than_or_equal_to_zero(data, "three_winding_transformer", "tap_size") errors += all_greater_or_equal( diff --git a/tests/benchmark_cpp/fictional_grid_generator.hpp b/tests/benchmark_cpp/fictional_grid_generator.hpp index 25da2beb7..60393a010 100644 --- a/tests/benchmark_cpp/fictional_grid_generator.hpp +++ b/tests/benchmark_cpp/fictional_grid_generator.hpp @@ -116,7 +116,9 @@ class FictionalGridGenerator { option_.n_mv_feeder = option_.n_lv_grid / option_.n_node_per_mv_feeder + 1; } total_mv_connection = option_.n_mv_feeder * option_.n_node_per_mv_feeder; - option_.ratio_lv_grid = static_cast(option_.n_lv_grid) / static_cast(total_mv_connection); + option_.ratio_lv_grid = total_mv_connection > 0 + ? static_cast(option_.n_lv_grid) / static_cast(total_mv_connection) + : 1.0; // each mv feeder 10 MVA, each transformer 60 MVA, scaled up by 10% option_.n_parallel_hv_mv_transformer = static_cast(static_cast(option.n_mv_feeder) * 10.0 * 1.1 / 60.0) + 1; diff --git a/tests/cpp_unit_tests/CMakeLists.txt b/tests/cpp_unit_tests/CMakeLists.txt index 836fe4f5b..f75cad3c4 100644 --- a/tests/cpp_unit_tests/CMakeLists.txt +++ b/tests/cpp_unit_tests/CMakeLists.txt @@ -22,6 +22,7 @@ set(PROJECT_SOURCES "test_measured_values.cpp" "test_observability.cpp" "test_topology.cpp" + "test_sparse_ordering.cpp" "test_grouped_index_vector.cpp" "test_container.cpp" "test_index_mapping.cpp" diff --git a/tests/cpp_unit_tests/test_sparse_ordering.cpp b/tests/cpp_unit_tests/test_sparse_ordering.cpp new file mode 100644 index 000000000..99c15fd91 --- /dev/null +++ b/tests/cpp_unit_tests/test_sparse_ordering.cpp @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: Contributors to the Power Grid Model project +// +// SPDX-License-Identifier: MPL-2.0 + +#include + +#include + +#include +#include +#include + +namespace { +using power_grid_model::Idx; +} // namespace + +TEST_CASE("Test sparse ordering") { + SUBCASE("minimum_degree_ordering") { + std::map> graph{{0, {3, 5}}, {1, {4, 5, 8}}, {2, {4, 5, 6}}, {3, {6, 7}}, + {4, {6, 8}}, {6, {7, 8, 9}}, {7, {8, 9}}, {8, {9}}}; + + auto const start = std::chrono::high_resolution_clock::now(); + auto const [alpha, fills] = power_grid_model::minimum_degree_ordering(std::move(graph)); + auto const stop = std::chrono::high_resolution_clock::now(); + + auto const duration = duration_cast(stop - start); + std::cout << "Time taken by function: " << duration.count() << " microseconds\n"; + + CHECK(alpha == std::vector{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}); + CHECK(fills == std::vector>{{3, 5}, {4, 5}, {5, 8}, {5, 6}, {5, 7}}); + } +} diff --git a/tests/cpp_unit_tests/test_three_winding_transformer.cpp b/tests/cpp_unit_tests/test_three_winding_transformer.cpp index 75cdb1487..5d96e826a 100644 --- a/tests/cpp_unit_tests/test_three_winding_transformer.cpp +++ b/tests/cpp_unit_tests/test_three_winding_transformer.cpp @@ -583,6 +583,17 @@ TEST_CASE("Test three winding transformer") { CHECK(inv.status_3 == expected.status_3); CHECK(inv.tap_pos == expected.tap_pos); } -} // namespace power_grid_model + SUBCASE("Test optional tap pos/nom") { + input.tap_nom = 1; + input.tap_pos = na_IntS; + std::vector trafo_vec; + trafo_vec.emplace_back(input, 138e3, 69e3, 13.8e3); + input.tap_nom = na_IntS; + trafo_vec.emplace_back(input, 138e3, 69e3, 13.8e3); + CHECK(trafo_vec[0].tap_pos() == 1); + CHECK(trafo_vec[1].tap_pos() == 0); + CHECK(trafo_vec[1].tap_nom() == 0); + } +} } // namespace power_grid_model diff --git a/tests/cpp_unit_tests/test_topology.cpp b/tests/cpp_unit_tests/test_topology.cpp index 892cf4f32..6a5e8e3a6 100644 --- a/tests/cpp_unit_tests/test_topology.cpp +++ b/tests/cpp_unit_tests/test_topology.cpp @@ -6,8 +6,6 @@ #include -#include - /* * [0] = Node / Bus * --0--> = Branch (from --id--> to) @@ -178,10 +176,10 @@ TEST_CASE("Test topology") { // result TopologicalComponentToMathCoupling comp_coup_ref{}; comp_coup_ref.node = { // 0 1 2 3 - {0, 4}, // Topological node 0 has become node 4 in mathematical model (group) 0 + {0, 1}, // Topological node 0 has become node 4 in mathematical model (group) 0 {0, 2}, {0, 0}, - {0, 1}, + {0, 4}, // 4 5 6 {1, 2}, // Topological node 4 has become node 2 in mathematical model (group) 1 {1, 3}, @@ -218,9 +216,9 @@ TEST_CASE("Test topology") { {-1, {-1, -1, -1}}, // b1 {1, {2, 3, 4}}, // b2 }; - comp_coup_ref.load_gen = {{0, 1}, {-1, -1}, {1, 0}, {0, 0}}; + comp_coup_ref.load_gen = {{0, 0}, {-1, -1}, {1, 0}, {0, 1}}; comp_coup_ref.shunt = {{0, 0}, {1, 0}, {-1, -1}}; - comp_coup_ref.voltage_sensor = {{0, 0}, {0, 3}, {0, 1}, {0, 2}, {1, 0}, {-1, -1}}; + comp_coup_ref.voltage_sensor = {{0, 0}, {0, 2}, {0, 1}, {0, 3}, {1, 0}, {-1, -1}}; comp_coup_ref.power_sensor = { {0, 0}, // 0 branch_from {1, 0}, // 1 source @@ -244,51 +242,50 @@ TEST_CASE("Test topology") { // Sub graph / math model 0 MathModelTopology math0; - math0.slack_bus = 4; - math0.sources_per_bus = {from_sparse, {0, 0, 0, 0, 0, 1}}; - math0.branch_bus_idx = {{4, 2}, {4, 1}, {1, -1}, {-1, 0}, {2, 3}, {1, 3}, {0, 3}}; - math0.phase_shift = {0.0, -1.0, 0.0, 0.0, 0.0}; - math0.load_gens_per_bus = {from_sparse, {0, 0, 0, 1, 1, 2}}; - math0.load_gen_type = {LoadGenType::const_y, LoadGenType::const_pq}; - math0.shunts_per_bus = {from_sparse, {0, 0, 1, 1, 1, 1}}; - math0.voltage_sensors_per_bus = {from_sparse, {0, 2, 3, 4, 4, 4}}; - math0.power_sensors_per_bus = {from_sparse, {0, 0, 0, 0, 0, 0}}; - math0.power_sensors_per_source = {from_sparse, {0, 0}}; - math0.power_sensors_per_shunt = {from_sparse, {0, 0}}; - math0.power_sensors_per_load_gen = {from_sparse, {0, 1, 1}}; - math0.power_sensors_per_branch_from = {from_sparse, {0, 0, 2, 2, 2, 3, 4, 5}}; + math0.slack_bus = 1; + math0.sources_per_bus = {from_dense, {1}, 5}; + math0.branch_bus_idx = {{1, 2}, {1, 4}, {4, -1}, {-1, 0}, {2, 3}, {4, 3}, {0, 3}}; + math0.phase_shift = {0.0, 0.0, 0.0, 0.0, -1.0}; + math0.load_gens_per_bus = {from_dense, {1, 2}, 5}; + math0.load_gen_type = {LoadGenType::const_pq, LoadGenType::const_y}; + math0.shunts_per_bus = {from_dense, {4}, 5}; + math0.voltage_sensors_per_bus = {from_dense, {0, 0, 2, 4}, 5}; + math0.power_sensors_per_bus = {from_dense, {}, 5}; + math0.power_sensors_per_source = {from_dense, {}, 1}; + math0.power_sensors_per_shunt = {from_dense, {}, 1}; + math0.power_sensors_per_load_gen = {from_dense, {1}, 2}; + math0.power_sensors_per_branch_from = {from_dense, {1, 1, 4, 5, 6}, 7}; // 7 branches, 3 branch-to power sensors // sensor 0 is connected to branch 0 // sensor 1 and 2 are connected to branch 1 - math0.power_sensors_per_branch_to = {from_sparse, {0, 1, 3, 3, 3, 3, 3, 3}}; - math0.fill_in = {{3, 4}}; + math0.power_sensors_per_branch_to = {from_dense, {0, 1, 1}, 7}; + math0.fill_in = {{2, 4}}; // Sub graph / math model 1 MathModelTopology math1; math1.slack_bus = 3; - math1.sources_per_bus = {from_sparse, {0, 0, 0, 0, 1}}; - math1.branch_bus_idx = { - {3, 2}, {2, 3}, {-1, 1}, {0, 1}, {3, 1}, - }; + math1.sources_per_bus = {from_dense, {3}, 4}; + math1.branch_bus_idx = {{3, 2}, {2, 3}, {-1, 1}, {0, 1}, {3, 1}}; math1.phase_shift = {0, 0, 0, 0}; - math1.load_gens_per_bus = {from_sparse, {0, 0, 0, 0, 1}}; + math1.load_gens_per_bus = {from_dense, {3}, 4}; math1.load_gen_type = {LoadGenType::const_i}; - math1.shunts_per_bus = {from_sparse, {0, 1, 1, 1, 1}}; - math1.voltage_sensors_per_bus = {from_sparse, {0, 0, 0, 0, 1}}; - math1.power_sensors_per_bus = {from_sparse, {0, 0, 0, 0, 1}}; - math1.power_sensors_per_source = {from_sparse, {0, 2}}; - math1.power_sensors_per_shunt = {from_sparse, {0, 2}}; - math1.power_sensors_per_load_gen = {from_sparse, {0, 2}}; - math1.power_sensors_per_branch_from = {from_sparse, {0, 0, 0, 0, 0, 0}}; - math1.power_sensors_per_branch_to = {from_sparse, {0, 0, 0, 0, 0, 0}}; + math1.shunts_per_bus = {from_dense, {0}, 4}; + math1.voltage_sensors_per_bus = {from_dense, {3}, 4}; + math1.power_sensors_per_bus = {from_dense, {3}, 4}; + math1.power_sensors_per_source = {from_dense, {0, 0}, 1}; + math1.power_sensors_per_shunt = {from_dense, {0, 0}, 1}; + math1.power_sensors_per_load_gen = {from_dense, {0, 0}, 1}; + math1.power_sensors_per_branch_from = {from_dense, {}, 5}; + math1.power_sensors_per_branch_to = {from_dense, {}, 5}; std::vector math_topology_ref = {math0, math1}; SUBCASE("Test topology result") { Topology topo{comp_topo, comp_conn}; - auto pair = topo.build_topology(); - auto const& math_topology = pair.first; - auto const& topo_comp_coup = *pair.second; + auto const [math_topology, topo_comp_coup_ptr] = topo.build_topology(); + + REQUIRE(topo_comp_coup_ptr != nullptr); + auto const& topo_comp_coup = *topo_comp_coup_ptr; CHECK(math_topology.size() == 2); // test component coupling @@ -324,41 +321,89 @@ TEST_CASE("Test topology") { } TEST_CASE("Test cycle reorder") { - // component topology - ComponentTopology comp_topo{}; - comp_topo.n_node = 7; - comp_topo.branch_node_idx = { - {0, 1}, // 0 - {1, 2}, // 1 - {2, 3}, // 2 - {3, 4}, // 3 - {4, 5}, // 4 - {0, 5}, // 5 - {1, 4}, // 6 - {6, 0}, // 7 - {6, 2}, // 8 - {5, 1}, // 9 - {3, 1}, // 10 - {6, 1}, // 11 - {2, 1}, // 12 - }; - comp_topo.source_node_idx = {0}; - // component connection - ComponentConnections comp_conn{}; - comp_conn.branch_connected = std::vector(13, {1, 1}); - comp_conn.branch_phase_shift = std::vector(13, 0.0); - comp_conn.source_connected = {1}; - // result - TopologicalComponentToMathCoupling comp_coup_ref{}; - comp_coup_ref.node = {{0, 3}, {0, 5}, {0, 4}, {0, 2}, {0, 6}, {0, 1}, {0, 0}}; - std::vector const fill_in_ref{{3, 4}, {3, 6}, {4, 6}}; + SUBCASE("9 nodes") { + // { + // 0: [3, 5], + // 1: [4, 5, 8], + // 2: [4, 5, 6], + // 3: [6, 7], + // 4: [6, 8], + // 6: [7, 8, 9], + // 7: [8, 9], + // 8: [9] + // } - Topology topo{comp_topo, comp_conn}; - auto pair = topo.build_topology(); - auto const& topo_comp_coup = *pair.second; - auto const& math_topo = *pair.first[0]; - CHECK(topo_comp_coup.node == comp_coup_ref.node); - CHECK(math_topo.fill_in == fill_in_ref); + // component topology + ComponentTopology comp_topo{}; + comp_topo.n_node = 10; + comp_topo.branch_node_idx = { + {0, 3}, {0, 5}, {1, 4}, {1, 5}, {1, 8}, {2, 4}, {2, 5}, {2, 6}, {3, 6}, + {3, 7}, {4, 6}, {4, 8}, {6, 7}, {6, 8}, {6, 9}, {7, 8}, {7, 9}, {8, 9}, + }; + comp_topo.source_node_idx = {0}; + // component connection + ComponentConnections comp_conn{}; + comp_conn.branch_connected = std::vector(18, {1, 1}); + comp_conn.branch_phase_shift = std::vector(18, 0.0); + comp_conn.source_connected = {1}; + // result + TopologicalComponentToMathCoupling comp_coup_ref{}; + comp_coup_ref.node = {{0, 0}, {0, 1}, {0, 2}, {0, 3}, {0, 4}, {0, 5}, {0, 6}, {0, 7}, {0, 8}, {0, 9}}; + std::vector const fill_in_ref{{3, 5}, {4, 5}, {5, 8}, {5, 6}, {5, 7}}; + + Topology topo{comp_topo, comp_conn}; + auto pair = topo.build_topology(); + auto const& topo_comp_coup = *pair.second; + auto const& math_topo = *pair.first[0]; + CHECK(topo_comp_coup.node == comp_coup_ref.node); + CHECK(math_topo.fill_in == fill_in_ref); + } + + SUBCASE("7 nodes") { + // { + // 0: [1, 5, 6], + // 1: [2, 4, 5, 3, 6], + // 2: [3, 6] + // 3: [4], + // 4: [5], + // } + + // component topology + ComponentTopology comp_topo{}; + comp_topo.n_node = 7; + comp_topo.branch_node_idx = { + {0, 1}, // 0 + {1, 2}, // 1 + {2, 3}, // 2 + {3, 4}, // 3 + {4, 5}, // 4 + {0, 5}, // 5 + {1, 4}, // 6 + {6, 0}, // 7 + {6, 2}, // 8 + {5, 1}, // 9 + {3, 1}, // 10 + {6, 1}, // 11 + {2, 1}, // 12 + }; + comp_topo.source_node_idx = {0}; + // component connection + ComponentConnections comp_conn{}; + comp_conn.branch_connected = std::vector(13, {1, 1}); + comp_conn.branch_phase_shift = std::vector(13, 0.0); + comp_conn.source_connected = {1}; + // result + TopologicalComponentToMathCoupling comp_coup_ref{}; + comp_coup_ref.node = {{0, 0}, {0, 3}, {0, 1}, {0, 2}, {0, 4}, {0, 5}, {0, 6}}; + std::vector const fill_in_ref{{5, 6}, {2, 6}, {4, 6}}; + + Topology topo{comp_topo, comp_conn}; + auto pair = topo.build_topology(); + auto const& topo_comp_coup = *pair.second; + auto const& math_topo = *pair.first[0]; + CHECK(topo_comp_coup.node == comp_coup_ref.node); + CHECK(math_topo.fill_in == fill_in_ref); + } } } // namespace power_grid_model \ No newline at end of file diff --git a/tests/cpp_unit_tests/test_transformer.cpp b/tests/cpp_unit_tests/test_transformer.cpp index 833577f40..06c92d0bb 100644 --- a/tests/cpp_unit_tests/test_transformer.cpp +++ b/tests/cpp_unit_tests/test_transformer.cpp @@ -266,6 +266,17 @@ TEST_CASE("Test transformer") { CHECK(inv.to_status == expected.to_status); CHECK(inv.tap_pos == expected.tap_pos); } + SUBCASE("Test optional tap pos/nom") { + input.tap_nom = 1; + input.tap_pos = na_IntS; + std::vector trafo_vec; + trafo_vec.emplace_back(input, 150e3, 10e3); + input.tap_nom = na_IntS; + trafo_vec.emplace_back(input, 150e3, 10e3); + CHECK(trafo_vec[0].tap_pos() == 1); + CHECK(trafo_vec[1].tap_pos() == 0); + CHECK(trafo_vec[1].tap_nom() == 0); + } } TEST_CASE("Test Transfomer - Test 0 YNyn12") { diff --git a/tests/cpp_validation_tests/test_validation.cpp b/tests/cpp_validation_tests/test_validation.cpp index a45acbf48..98afca0a0 100644 --- a/tests/cpp_validation_tests/test_validation.cpp +++ b/tests/cpp_validation_tests/test_validation.cpp @@ -617,7 +617,9 @@ TEST_CASE("Validation test single") { try { validate_single_case(param); } catch (std::exception& e) { - auto const msg = std::string("Unexpected exception with message: ") + e.what(); + using namespace std::string_literals; + + auto const msg = "Unexpected exception with message: "s + e.what(); FAIL_CHECK(msg); } } @@ -631,7 +633,9 @@ TEST_CASE("Validation test batch") { try { validate_batch_case(param); } catch (std::exception& e) { - auto const msg = std::string("Unexpected exception with message: ") + e.what(); + using namespace std::string_literals; + + auto const msg = "Unexpected exception with message: "s + e.what(); FAIL_CHECK(msg); } } diff --git a/tests/unit/validation/test_rules.py b/tests/unit/validation/test_rules.py index fad1b9309..c21ee785f 100644 --- a/tests/unit/validation/test_rules.py +++ b/tests/unit/validation/test_rules.py @@ -7,7 +7,7 @@ import numpy as np import pytest -from power_grid_model import LoadGenType, initialize_array +from power_grid_model import LoadGenType, initialize_array, power_grid_meta_data from power_grid_model.enum import Branch3Side, BranchSide, FaultPhase, FaultType from power_grid_model.validation.errors import ( ComparisonError, @@ -143,6 +143,19 @@ def test_all_between_or_at(): assert len(errors) == 1 assert NotBetweenOrAtError("test", "value", [1, 4, 5, 6], (0.2, -0.2)) in errors + nan_value = power_grid_meta_data["input"]["transformer"].nans["tap_pos"] + transformer_array = initialize_array("input", "transformer", 3) + transformer_array["id"] = [1, 2, 3] + transformer_array["tap_pos"] = [nan_value, 1, nan_value] + transformer_array["tap_nom"] = [2, 1, nan_value] + valid = {"transformer": transformer_array} + errors = all_between_or_at(valid, "transformer", "tap_pos", 0, 2, transformer_array["tap_nom"], 0) + assert not errors + + errors = all_between_or_at(valid, "transformer", "tap_pos", 1, 2, transformer_array["tap_nom"], 0) + assert len(errors) == 1 + assert NotBetweenOrAtError("transformer", "tap_pos", [3], (1, 2)) in errors + def test_all_greater_than(): valid = {"test": np.array([(1, 0.0), (2, 0.2), (3, 0.5), (4, np.nan)], dtype=[("id", "i4"), ("value", "f8")])} @@ -217,6 +230,19 @@ def test_none_match_comparison(): assert len(errors) == 1 assert ComparisonError("test", "value", [2], 0.2) in errors + nan_value = power_grid_meta_data["input"]["transformer"].nans["tap_pos"] + transformer_array = initialize_array("input", "transformer", 3) + transformer_array["id"] = [1, 2, 3] + transformer_array["tap_pos"] = [nan_value, 0, nan_value] + transformer_array["tap_nom"] = [1, 1, nan_value] + valid = {"transformer": transformer_array} + + errors = none_match_comparison( + valid, "transformer", "tap_pos", np.equal, 0, ComparisonError, transformer_array["tap_nom"], 0 + ) + assert len(errors) == 1 + assert ComparisonError("transformer", "tap_pos", [2, 3], 0) in errors + def test_all_identical(): dtype = [("id", "i4"), ("foo", "i4")] diff --git a/tests/unit/validation/test_validation_functions.py b/tests/unit/validation/test_validation_functions.py index 7b2d4f86a..5cdd0f4d6 100644 --- a/tests/unit/validation/test_validation_functions.py +++ b/tests/unit/validation/test_validation_functions.py @@ -243,7 +243,6 @@ def test_validate_required_values_sym_calculation(calculation_type, symmetric): assert MissingValueError("transformer", "winding_to", [NaN]) in required_values_errors assert MissingValueError("transformer", "clock", [NaN]) in required_values_errors assert MissingValueError("transformer", "tap_side", [NaN]) in required_values_errors - assert MissingValueError("transformer", "tap_pos", [NaN]) in required_values_errors assert MissingValueError("transformer", "tap_min", [NaN]) in required_values_errors assert MissingValueError("transformer", "tap_max", [NaN]) in required_values_errors assert MissingValueError("transformer", "tap_size", [NaN]) in required_values_errors @@ -275,7 +274,6 @@ def test_validate_required_values_sym_calculation(calculation_type, symmetric): assert MissingValueError("three_winding_transformer", "clock_12", [NaN]) in required_values_errors assert MissingValueError("three_winding_transformer", "clock_13", [NaN]) in required_values_errors assert MissingValueError("three_winding_transformer", "tap_side", [NaN]) in required_values_errors - assert MissingValueError("three_winding_transformer", "tap_pos", [NaN]) in required_values_errors assert MissingValueError("three_winding_transformer", "tap_min", [NaN]) in required_values_errors assert MissingValueError("three_winding_transformer", "tap_max", [NaN]) in required_values_errors assert MissingValueError("three_winding_transformer", "tap_size", [NaN]) in required_values_errors @@ -722,6 +720,90 @@ def test_power_sigma_or_p_q_sigma(): assert_valid_input_data(input_data=input_data, calculation_type=CalculationType.state_estimation) +def test_all_default_values(): + """ + Initialize all components that have attributes that have default values, without setting values for + those attributes. + """ + node = initialize_array("input", "node", 3) + node["id"] = [0, 1, 2] + node["u_rated"] = [50.0e3, 20.0e3, 10.5e3] + + source = initialize_array("input", "source", 1) + source["id"] = [3] + source["node"] = [2] + source["status"] = [1] + source["u_ref"] = [1.0] + + transformer = initialize_array("input", "transformer", 1) + transformer["id"] = [4] + transformer["from_node"] = [0] + transformer["to_node"] = [2] + transformer["from_status"] = [1] + transformer["to_status"] = [1] + transformer["u1"] = [50e3] + transformer["u2"] = [10.5e3] + transformer["sn"] = [1e5] + transformer["uk"] = [0.1] + transformer["pk"] = [1e3] + transformer["i0"] = [1.0e-6] + transformer["p0"] = [0.1] + transformer["winding_from"] = [2] + transformer["winding_to"] = [1] + transformer["clock"] = [5] + transformer["tap_side"] = [0] + transformer["tap_min"] = [-11] + transformer["tap_max"] = [9] + transformer["tap_size"] = [100] + + three_winding_transformer = initialize_array("input", "three_winding_transformer", 1) + three_winding_transformer["id"] = [6] + three_winding_transformer["node_1"] = [0] + three_winding_transformer["node_2"] = [1] + three_winding_transformer["node_3"] = [2] + three_winding_transformer["status_1"] = [1] + three_winding_transformer["status_2"] = [1] + three_winding_transformer["status_3"] = [1] + three_winding_transformer["u1"] = [50.0e3] + three_winding_transformer["u2"] = [20.0e3] + three_winding_transformer["u3"] = [10.5e3] + three_winding_transformer["sn_1"] = [1e5] + three_winding_transformer["sn_2"] = [1e5] + three_winding_transformer["sn_3"] = [1e5] + three_winding_transformer["uk_12"] = [0.09] + three_winding_transformer["uk_13"] = [0.06] + three_winding_transformer["uk_23"] = [0.06] + three_winding_transformer["pk_12"] = [1e3] + three_winding_transformer["pk_13"] = [1e3] + three_winding_transformer["pk_23"] = [1e3] + three_winding_transformer["i0"] = [0] + three_winding_transformer["p0"] = [0] + three_winding_transformer["winding_1"] = [2] + three_winding_transformer["winding_2"] = [1] + three_winding_transformer["winding_3"] = [1] + three_winding_transformer["clock_12"] = [5] + three_winding_transformer["clock_13"] = [5] + three_winding_transformer["tap_side"] = [0] + three_winding_transformer["tap_min"] = [-10] + three_winding_transformer["tap_max"] = [10] + three_winding_transformer["tap_size"] = [1380] + + fault = initialize_array("input", "fault", 1) + fault["id"] = [5] + fault["status"] = [1] + fault["fault_object"] = [0] + + input_data = { + "node": node, + "transformer": transformer, + "three_winding_transformer": three_winding_transformer, + "source": source, + "fault": fault, + } + + assert_valid_input_data(input_data=input_data, calculation_type=CalculationType.power_flow) + + @patch("power_grid_model.validation.validation.validate_transformer", new=MagicMock(return_value=[])) @patch("power_grid_model.validation.validation.validate_three_winding_transformer", new=MagicMock(return_value=[])) def test_validate_values__tap_regulator_control_side():