diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/auxiliary/dataset.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/auxiliary/dataset.hpp index 2344f291c..8ebf3e20d 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/auxiliary/dataset.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/auxiliary/dataset.hpp @@ -171,7 +171,7 @@ template class Dataset { template using DataStruct = std::conditional_t, StructType, StructType const>; - // for columnar buffers, Data* data is empty and attributes is filled + // for columnar buffers, Data* data is empty and attributes.data is filled // for uniform buffers, indptr is empty struct Buffer { using Data = Dataset::Data; @@ -227,10 +227,14 @@ template class Dataset { MetaDataset const& dataset() const { return *dataset_info_.dataset; } Idx n_components() const { return static_cast(buffers_.size()); } DatasetInfo const& get_description() const { return dataset_info_; } - ComponentInfo const& get_component_info(Idx i) const { return dataset_info_.component_info[i]; } Buffer const& get_buffer(std::string_view component) const { return get_buffer(find_component(component, true)); } Buffer const& get_buffer(Idx i) const { return buffers_[i]; } + ComponentInfo const& get_component_info(std::string_view component) const { + return get_component_info(find_component(component, true)); + } + ComponentInfo const& get_component_info(Idx i) const { return dataset_info_.component_info[i]; } + constexpr bool is_row_based(std::string_view component) const { Idx const idx = find_component(component, false); if (idx == invalid_index) { @@ -254,6 +258,63 @@ template class Dataset { return !is_row_based(buffer) && !(with_attribute_buffers && buffer.attributes.empty()); } + constexpr bool is_dense(std::string_view component) const { + Idx const idx = find_component(component, false); + if (idx == invalid_index) { + return true; // by definition + } + return is_dense(idx); + } + constexpr bool is_dense(Idx const i) const { return is_dense(buffers_[i]); } + constexpr bool is_dense(Buffer const& buffer) const { return buffer.indptr.empty(); } + constexpr bool is_sparse(std::string_view component, bool with_attribute_buffers = false) const { + Idx const idx = find_component(component, false); + if (idx == invalid_index) { + return false; + } + return is_sparse(idx, with_attribute_buffers); + } + constexpr bool is_sparse(Idx const i, bool with_attribute_buffers = false) const { + return is_sparse(buffers_[i], with_attribute_buffers); + } + constexpr bool is_sparse(Buffer const& buffer) const { return !is_dense(buffer); } + + constexpr bool is_uniform(std::string_view component) const { + Idx const idx = find_component(component, false); + if (idx == invalid_index) { + return true; // by definition + } + return is_uniform(idx); + } + constexpr bool is_uniform(Idx const i) const { return is_uniform(buffers_[i]); } + constexpr bool is_uniform(Buffer const& buffer) const { + if (is_dense(buffer)) { + return true; + } + assert(buffer.indptr.size() > 1); + auto const first_scenario_size = buffer.indptr[1] - buffer.indptr[0]; + return std::ranges::adjacent_find(buffer.indptr, [first_scenario_size](Idx start, Idx stop) { + return stop - start != first_scenario_size; + }) == buffer.indptr.end(); + } + + constexpr Idx uniform_elements_per_scenario(std::string_view component) const { + Idx const idx = find_component(component, false); + if (idx == invalid_index) { + return 0; + } + return uniform_elements_per_scenario(idx); + } + constexpr Idx uniform_elements_per_scenario(Idx const i) const { + assert(is_uniform(i)); + if (is_dense(i)) { + return get_component_info(i).elements_per_scenario; + } + auto const& indptr = buffers_[i].indptr; + assert(indptr.size() > 1); + return indptr[1] - indptr[0]; + } + Idx find_component(std::string_view component, bool required = false) const { auto const found = std::ranges::find_if(dataset_info_.component_info, [component](ComponentInfo const& x) { return x.component->name == component; @@ -269,10 +330,6 @@ template class Dataset { } bool contains_component(std::string_view component) const { return find_component(component) >= 0; } - ComponentInfo const& get_component_info(std::string_view component) const { - return dataset_info_.component_info[find_component(component, true)]; - } - void add_component_info(std::string_view component, Idx elements_per_scenario, Idx total_elements) requires is_indptr_mutable_v { diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/common/common.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/common/common.hpp index da783c1e8..43c6d78ce 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/common/common.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/common/common.hpp @@ -66,6 +66,7 @@ constexpr double numerical_tolerance = 1e-8; constexpr double nan = std::numeric_limits::quiet_NaN(); constexpr IntS na_IntS = std::numeric_limits::min(); constexpr ID na_IntID = std::numeric_limits::min(); +constexpr Idx na_Idx = std::numeric_limits::min(); // power grid constant constexpr double base_power_3p = 1e6; 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 e6dda1c51..34c55f53c 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 @@ -141,6 +141,13 @@ class IDNotFound : public PowerGridError { public: explicit IDNotFound(ID id) { append_msg("The id cannot be found: " + detail::to_string(id) + '\n'); } }; +class Idx2DNotFound : public PowerGridError { + public: + explicit Idx2DNotFound(Idx2D id) { + append_msg("The idx 2d cannot be found: {" + detail::to_string(id.group) + ", " + detail::to_string(id.pos) + + "}.\n"); + } +}; class InvalidMeasuredObject : public PowerGridError { public: diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/common/three_phase_tensor.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/common/three_phase_tensor.hpp index 17846e1d3..6523933ea 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/common/three_phase_tensor.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/common/three_phase_tensor.hpp @@ -297,6 +297,7 @@ template inline bool is_nan(Enum x) { return static_cast(x) == na_IntS; } +inline bool is_nan(Idx x) { return x == na_Idx; } // is normal inline auto is_normal(std::floating_point auto value) { return std::isnormal(value); } diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/component/branch.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/component/branch.hpp index 7af2b63f6..e07a584e7 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/component/branch.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/component/branch.hpp @@ -184,14 +184,14 @@ class Branch : public Base { // default update for branch, will be hidden for transformer UpdateChange update(BranchUpdate const& update_data) { - assert(update_data.id == id()); + assert(update_data.id == this->id() || is_nan(update_data.id)); bool const changed = set_status(update_data.from_status, update_data.to_status); // change branch connection will change both topo and param return {changed, changed}; } auto inverse(std::convertible_to auto update_data) const { - assert(update_data.id == id()); + assert(update_data.id == this->id() || is_nan(update_data.id)); set_if_not_nan(update_data.from_status, static_cast(from_status_)); set_if_not_nan(update_data.to_status, static_cast(to_status_)); diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/component/branch3.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/component/branch3.hpp index e486f4f2f..bce46ff42 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/component/branch3.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/component/branch3.hpp @@ -206,14 +206,14 @@ class Branch3 : public Base { // default update for branch3, will be hidden for three winding transformer UpdateChange update(Branch3Update const& update_data) { - assert(update_data.id == id()); + assert(update_data.id == this->id() || is_nan(update_data.id)); bool const changed = set_status(update_data.status_1, update_data.status_2, update_data.status_3); // change in branch3 connection will change both topo and param return {changed, changed}; } auto inverse(std::convertible_to auto update_data) const { - assert(update_data.id == id()); + assert(update_data.id == this->id() || is_nan(update_data.id)); set_if_not_nan(update_data.status_1, static_cast(status_1_)); set_if_not_nan(update_data.status_2, static_cast(status_2_)); diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/component/fault.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/component/fault.hpp index 5f33e6377..b55600a5a 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/component/fault.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/component/fault.hpp @@ -118,7 +118,7 @@ class Fault final : public Base { } FaultUpdate inverse(FaultUpdate update_data) const { - assert(update_data.id == id()); + assert(update_data.id == this->id() || is_nan(update_data.id)); set_if_not_nan(update_data.status, static_cast(status_)); set_if_not_nan(update_data.fault_type, fault_type_); diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/component/load_gen.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/component/load_gen.hpp index 6003b8f85..04c8b7262 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/component/load_gen.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/component/load_gen.hpp @@ -102,7 +102,7 @@ class LoadGen final : public std::conditional_t, // update for load_gen UpdateChange update(LoadGenUpdate const& update_data) { - assert(update_data.id == this->id()); + assert(update_data.id == this->id() || is_nan(update_data.id)); this->set_status(update_data.status); set_power(update_data.p_specified, update_data.q_specified); // change load connection and/or value will not change topology or parameters @@ -110,10 +110,9 @@ class LoadGen final : public std::conditional_t, } LoadGenUpdate inverse(LoadGenUpdate update_data) const { + assert(update_data.id == this->id() || is_nan(update_data.id)); double const scalar = direction_ * base_power; - assert(update_data.id == this->id()); - set_if_not_nan(update_data.status, static_cast(this->status())); set_if_not_nan(update_data.p_specified, real(s_specified_) * scalar); set_if_not_nan(update_data.q_specified, imag(s_specified_) * scalar); diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/component/power_sensor.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/component/power_sensor.hpp index 9179eb507..76f33be8c 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/component/power_sensor.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/component/power_sensor.hpp @@ -99,8 +99,7 @@ template class PowerSensor : public Generi } PowerSensorUpdate inverse(PowerSensorUpdate update_data) const { - assert(update_data.id == this->id()); - + assert(update_data.id == this->id() || is_nan(update_data.id)); auto const scalar = convert_direction() * base_power; set_if_not_nan(update_data.p_measured, real(s_measured_) * scalar); diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/component/regulator.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/component/regulator.hpp index 676f6eea8..49fbd24eb 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/component/regulator.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/component/regulator.hpp @@ -29,7 +29,7 @@ class Regulator : public Base { void set_status(IntS status) { status_ = static_cast(status); } auto inverse(std::convertible_to auto update_data) const { - assert(update_data.id == id()); + assert(update_data.id == this->id() || is_nan(update_data.id)); set_if_not_nan(update_data.status, static_cast(status_)); return update_data; } diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/component/shunt.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/component/shunt.hpp index 08a369d67..447c37b5a 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/component/shunt.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/component/shunt.hpp @@ -46,7 +46,7 @@ class Shunt : public Appliance { } UpdateChange update(ShuntUpdate const& update_data) { - assert(update_data.id == id()); + assert(update_data.id == this->id() || is_nan(update_data.id)); bool changed = set_status(update_data.status); changed = update_params(update_data) || changed; @@ -55,7 +55,7 @@ class Shunt : public Appliance { } ShuntUpdate inverse(ShuntUpdate update_data) const { - assert(update_data.id == id()); + assert(update_data.id == this->id() || is_nan(update_data.id)); set_if_not_nan(update_data.status, static_cast(this->status())); set_if_not_nan(update_data.g1, g1_); diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/component/source.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/component/source.hpp index 5eb007deb..ed0e54200 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/component/source.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/component/source.hpp @@ -88,7 +88,7 @@ class Source : public Appliance { // update for source UpdateChange update(SourceUpdate const& update_data) { - assert(update_data.id == id()); + assert(update_data.id == this->id() || is_nan(update_data.id)); bool const topo_changed = set_status(update_data.status); bool const param_changed = set_u_ref(update_data.u_ref, update_data.u_ref_angle); // change source connection will change both topo and param @@ -97,7 +97,7 @@ class Source : public Appliance { } SourceUpdate inverse(SourceUpdate update_data) const { - assert(update_data.id == id()); + assert(update_data.id == this->id() || is_nan(update_data.id)); set_if_not_nan(update_data.status, static_cast(this->status())); set_if_not_nan(update_data.u_ref, u_ref_); 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 0f644b3e6..b64a6510a 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 @@ -130,14 +130,14 @@ class ThreeWindingTransformer : public Branch3 { } UpdateChange update(ThreeWindingTransformerUpdate const& update_data) { - assert(update_data.id == id()); + assert(update_data.id == this->id() || is_nan(update_data.id)); bool const topo_changed = set_status(update_data.status_1, update_data.status_2, update_data.status_3); bool const param_changed = set_tap(update_data.tap_pos) || topo_changed; return {topo_changed, param_changed}; } ThreeWindingTransformerUpdate inverse(ThreeWindingTransformerUpdate update_data) const { - assert(update_data.id == id()); + assert(update_data.id == this->id() || is_nan(update_data.id)); update_data = Branch3::inverse(update_data); set_if_not_nan(update_data.tap_pos, tap_pos_); 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 d3aaddd25..10a50fdf0 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 @@ -94,14 +94,14 @@ class Transformer : public Branch { // update for transformer, hide default update for branch UpdateChange update(TransformerUpdate const& update_data) { - assert(update_data.id == id()); + assert(update_data.id == this->id() || is_nan(update_data.id)); bool const topo_changed = set_status(update_data.from_status, update_data.to_status); bool const param_changed = set_tap(update_data.tap_pos) || topo_changed; return {topo_changed, param_changed}; } TransformerUpdate inverse(TransformerUpdate update_data) const { - assert(update_data.id == id()); + assert(update_data.id == this->id() || is_nan(update_data.id)); update_data = Branch::inverse(update_data); set_if_not_nan(update_data.tap_pos, tap_pos_); diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/component/transformer_tap_regulator.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/component/transformer_tap_regulator.hpp index ba5405d85..760b2ba65 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/component/transformer_tap_regulator.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/component/transformer_tap_regulator.hpp @@ -35,7 +35,7 @@ class TransformerTapRegulator : public Regulator { // update for transformer tap regulator, hide default update for branch UpdateChange update(TransformerTapRegulatorUpdate const& update_data) { - assert(update_data.id == id()); + assert(update_data.id == this->id() || is_nan(update_data.id)); set_status(update_data.status); update_real_value(update_data.u_set, u_set_, 1.0); update_real_value(update_data.u_band, u_band_, 1.0); @@ -45,7 +45,7 @@ class TransformerTapRegulator : public Regulator { } TransformerTapRegulatorUpdate inverse(TransformerTapRegulatorUpdate update_data) const { - assert(update_data.id == id()); + assert(update_data.id == this->id() || is_nan(update_data.id)); update_data = Regulator::inverse(update_data); set_if_not_nan(update_data.u_set, u_set_); diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/component/voltage_sensor.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/component/voltage_sensor.hpp index 4b567dc0c..2fde9e57b 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/component/voltage_sensor.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/component/voltage_sensor.hpp @@ -80,8 +80,7 @@ template class VoltageSensor : public GenericVoltageSensor { u_angle_measured_{voltage_sensor_input.u_angle_measured} {}; UpdateChange update(VoltageSensorUpdate const& update_data) { - assert(update_data.id == this->id()); - + assert(update_data.id == this->id() || is_nan(update_data.id)); double const scalar = 1 / (u_rated_ * u_scale); update_real_value(update_data.u_measured, u_measured_, scalar); @@ -95,8 +94,7 @@ template class VoltageSensor : public GenericVoltageSensor { } VoltageSensorUpdate inverse(VoltageSensorUpdate update_data) const { - assert(update_data.id == this->id()); - + assert(update_data.id == this->id() || is_nan(update_data.id)); double const scalar = u_rated_ * u_scale; set_if_not_nan(update_data.u_measured, u_measured_ * scalar); 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 206d389b2..2aae59b48 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 @@ -110,6 +110,17 @@ class Container, StorageableTypes...> { assert(is_base[idx_2d.group]); return (this->*(func_arr[idx_2d.group]))(idx_2d.pos); } + +#ifndef NDEBUG + // get id by idx, only for debugging purpose + ID get_id_by_idx(Idx2D idx_2d) const { + if (auto it = std::ranges::find(map_, idx_2d, &std::pair::second); it != map_.end()) { + return it->first; + } + throw Idx2DNotFound{idx_2d}; + } +#endif // NDEBUG + // get idx by id Idx2D get_idx_by_id(ID id) const { auto const found = map_.find(id); @@ -125,6 +136,10 @@ class Container, StorageableTypes...> { } return result; } + template Storageable> constexpr Idx get_group_idx() const { + return static_cast(get_cls_pos_v); + } + // get item based on ID template Gettable> Gettable& get_item(ID id) { Idx2D const idx = get_idx_by_id(id); diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/main_core/state_queries.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/main_core/state_queries.hpp index 66751d7c9..04b998373 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/main_core/state_queries.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/main_core/state_queries.hpp @@ -33,6 +33,12 @@ inline Idx2D get_component_idx_by_id(MainModelState const& s return state.components.template get_idx_by_id(id); } +template + requires model_component_state_c +constexpr Idx get_component_group_idx(MainModelState const& state) { + return state.components.template get_group_idx(); +} + template requires model_component_state_c inline Idx get_component_sequence(MainModelState const& state, auto const& id_or_index) { diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/main_core/update.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/main_core/update.hpp index 71ee3f7c4..adffa30f2 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/main_core/update.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/main_core/update.hpp @@ -33,21 +33,31 @@ template OutputIterator> requires model_component_state_c inline void get_component_sequence(MainModelState const& state, ForwardIterator begin, - ForwardIterator end, OutputIterator destination) { + ForwardIterator end, OutputIterator destination, Idx n_comp_elements) { using UpdateType = typename Component::UpdateType; - std::transform(begin, end, destination, - [&state](UpdateType const& update) { return get_component_idx_by_id(state, update.id); }); + if (n_comp_elements == na_Idx) { + std::ranges::transform(begin, end, destination, [&state](UpdateType const& update) { + return get_component_idx_by_id(state, update.id); + }); + } else { + assert(std::distance(begin, end) <= n_comp_elements || begin == end); + std::ranges::transform( + begin, end, destination, + [group = get_component_group_idx(state), index = 0](auto const& /*update*/) mutable { + return Idx2D{group, index++}; // NOSONAR + }); + } } template ForwardIterator> requires model_component_state_c inline std::vector get_component_sequence(MainModelState const& state, ForwardIterator begin, - ForwardIterator end) { + ForwardIterator end, Idx n_comp_elements = na_Idx) { std::vector result; result.reserve(std::distance(begin, end)); - get_component_sequence(state, begin, end, std::back_inserter(result)); + get_component_sequence(state, begin, end, std::back_inserter(result), n_comp_elements); return result; } @@ -69,8 +79,8 @@ inline UpdateChange update_component(MainModelState& state, detail::iterate_component_sequence( [&state_changed, &changed_it, &state](UpdateType const& update_data, Idx2D const& sequence_single) { auto& comp = get_component(state, sequence_single); + assert(state.components.get_id_by_idx(sequence_single) == comp.id()); auto const comp_changed = comp.update(update_data); - state_changed = state_changed || comp_changed; if (comp_changed.param || comp_changed.topo) { diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/main_model.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/main_model.hpp index 54d1ca85e..642e8d5d2 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/main_model.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/main_model.hpp @@ -49,10 +49,6 @@ class MainModel { }; ~MainModel() { impl_.reset(); } - static bool is_update_independent(ConstDataset const& update_data) { - return Impl::is_update_independent(update_data); - } - std::map> all_component_count() const { return impl().all_component_count(); } void get_indexer(std::string_view component_type, ID const* id_begin, Idx size, Idx* indexer_begin) const { impl().get_indexer(component_type, id_begin, size, indexer_begin); diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/main_model_impl.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/main_model_impl.hpp index 34c294d26..80a6ed2e1 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/main_model_impl.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/main_model_impl.hpp @@ -145,7 +145,38 @@ class MainModelImpl, ComponentLis using OwnedUpdateDataset = std::tuple...>; + struct UpdateCompProperties { + std::string name{}; + bool has_any_elements{false}; // whether the component has any elements in the update data + bool ids_all_na{false}; // whether all ids are all NA + bool ids_part_na{false}; // whether some ids are NA but some are not + bool dense{false}; // whether the component is dense + bool uniform{false}; // whether the component is uniform + bool is_columnar{false}; // whether the component is columnar + bool update_ids_match{false}; // whether the ids match + Idx elements_ps_in_update{invalid_index}; // count of elements for this component per scenario in update + Idx elements_in_base{invalid_index}; // count of elements for this component per scenario in input + + constexpr bool no_id() const { return !has_any_elements || ids_all_na; } + constexpr bool qualify_for_optional_id() const { + return update_ids_match && ids_all_na && uniform && elements_ps_in_update == elements_in_base; + } + constexpr bool provided_ids_valid() const { + return is_empty_component() || (update_ids_match && !(ids_all_na || ids_part_na)); + } + constexpr bool is_empty_component() const { return !has_any_elements; } + constexpr bool is_independent() const { return qualify_for_optional_id() || provided_ids_valid(); } + constexpr Idx get_n_elements() const { + assert(uniform || elements_ps_in_update == invalid_index); + + return qualify_for_optional_id() ? elements_ps_in_update : invalid_index; + } + }; + using UpdateCompIndependence = std::vector; + using ComponentCountInBase = std::pair; + static constexpr Idx ignore_output{-1}; + static constexpr Idx invalid_index{-1}; protected: // run functors with all component types @@ -155,6 +186,9 @@ class MainModelImpl, ComponentLis template static constexpr auto run_functor_with_all_types_return_array(Functor functor) { return std::array { functor.template operator()()... }; } + template static constexpr auto run_functor_with_all_types_return_vector(Functor functor) { + return std::vector { functor.template operator()()... }; + } public: using Options = MainModelOptions; @@ -179,7 +213,7 @@ class MainModelImpl, ComponentLis // all component count std::map> all_component_count() const { - auto const get_comp_count = [this]() -> std::pair { + auto const get_comp_count = [this]() -> ComponentCountInBase { return make_pair(std::string{CT::name}, this->component_count()); }; auto const all_count = run_functor_with_all_types_return_array(get_comp_count); @@ -373,19 +407,29 @@ class MainModelImpl, ComponentLis } // get sequence idx map of a certain batch scenario - SequenceIdx get_sequence_idx_map(ConstDataset const& update_data, Idx scenario_idx) const { + SequenceIdx get_sequence_idx_map(ConstDataset const& update_data, Idx scenario_idx, + UpdateCompIndependence comp_independence = {}) const { auto const process_buffer_span = [](auto const& buffer_span, auto const& get_sequence) { auto const it_begin = buffer_span.begin(); auto const it_end = buffer_span.end(); return get_sequence(it_begin, it_end); }; - auto const get_seq_idx_func = [&state = this->state_, &update_data, scenario_idx, - &process_buffer_span]() -> std::vector { - auto const get_sequence = [&state](auto const& it_begin, auto const& it_end) { - return main_core::get_component_sequence(state, it_begin, it_end); - }; + auto const get_seq_idx_func = [&state = this->state_, &update_data, scenario_idx, &process_buffer_span, + &comp_independence]() -> std::vector { + // TODO: (jguo) this function could be encapsulated in UpdateCompIndependence in update.hpp + Idx const n_comp_elements = [&comp_independence]() { + if (auto const comp_idx = + std::ranges::find_if(comp_independence, [](auto const& comp) { return comp.name == CT::name; }); + comp_idx != comp_independence.end() && comp_idx->no_id()) { + return comp_idx->get_n_elements(); + } + return na_Idx; + }(); + auto const get_sequence = [&state, &n_comp_elements](auto const& it_begin, auto const& it_end) { + return main_core::get_component_sequence(state, it_begin, it_end, n_comp_elements); + }; if (update_data.is_columnar(CT::name)) { auto const buffer_span = update_data.get_columnar_buffer_span(scenario_idx); @@ -400,8 +444,15 @@ class MainModelImpl, ComponentLis // get sequence idx map of an entire batch for fast caching of component sequences // (only applicable for independent update dataset) SequenceIdx get_sequence_idx_map(ConstDataset const& update_data) const { - assert(is_update_independent(update_data)); - return get_sequence_idx_map(update_data, 0); + auto const update_components_independence = check_components_independence(update_data); + assert(std::ranges::all_of(update_components_independence, + [](auto const& comp) { return comp.is_independent(); })); + + // TODO: (jguo) this function could be encapsulated in UpdateCompIndependence in update.hpp + std::ranges::for_each(update_components_independence, + [this](auto& comp) { validate_update_data_independence(comp); }); + + return get_sequence_idx_map(update_data, 0, update_components_independence); } private: @@ -562,7 +613,9 @@ class MainModelImpl, ComponentLis MainModelImpl const& base_model = *this; // cache component update order if possible - bool const is_independent = MainModelImpl::is_update_independent(update_data); + bool const is_independent = std::ranges::all_of(is_update_independent(update_data), [](auto const& result) { + return result.second; // Check if all components are independent + }); if (is_independent) { all_scenarios_sequence = get_sequence_idx_map(update_data); } @@ -714,52 +767,119 @@ class MainModelImpl, ComponentLis public: template using UpdateType = typename Component::UpdateType; - static bool is_update_independent(ConstDataset const& update_data) { - // If the batch size is (0 or) 1, then the update data for this component is 'independent' - if (update_data.batch_size() <= 1) { - return true; - } + UpdateCompIndependence check_components_independence(ConstDataset const& update_data) const { + auto const all_comp_count_in_base = this->all_component_count(); - auto const process_buffer_span = [](auto const& all_spans) -> bool { - // Remember the first batch size, then loop over the remaining batches and check if they are of the same - // length - auto const elements_per_scenario = static_cast(all_spans.front().size()); - bool const uniform_batch = std::ranges::all_of(all_spans, [elements_per_scenario](auto const& span) { - return static_cast(span.size()) == elements_per_scenario; - }); - if (!uniform_batch) { - return false; + auto const check_id_na = [](auto const& obj) -> bool { + if constexpr (requires { obj.id; }) { + return is_nan(obj.id); + } else if constexpr (requires { obj.get().id; }) { + return is_nan(obj.get().id); + } else { + throw UnreachableHit{"check_components_independence", "Only components with id are supported"}; } - if (elements_per_scenario == 0) { - return true; + }; + + auto const process_buffer_span = [check_id_na](auto const& all_spans, + UpdateCompProperties& result) { + result.ids_all_na = std::ranges::all_of( + all_spans, [&check_id_na](auto const& vec) { return std::ranges::all_of(vec, check_id_na); }); + result.ids_part_na = + std::ranges::any_of( + all_spans, [&check_id_na](auto const& vec) { return std::ranges::any_of(vec, check_id_na); }) && + !result.ids_all_na; + + if (all_spans.empty()) { + result.update_ids_match = true; + } else { + // Remember the begin iterator of the first scenario, then loop over the remaining scenarios and + // check the ids + auto const first_span = all_spans[0]; + // check the subsequent scenarios + // only return true if all scenarios match the ids of the first batch + result.update_ids_match = std::ranges::all_of( + all_spans.cbegin() + 1, all_spans.cend(), [&first_span](auto const& current_span) { + return std::ranges::equal( + current_span, first_span, + [](UpdateType const& obj, UpdateType const& first) { return obj.id == first.id; }); + }); } - // Remember the begin iterator of the first scenario, then loop over the remaining scenarios and check the - // ids - auto const first_span = all_spans[0]; - // check the subsequent scenarios - // only return true if all scenarios match the ids of the first batch - return std::all_of(all_spans.cbegin() + 1, all_spans.cend(), [&first_span](auto const& current_span) { - return std::ranges::equal( - current_span, first_span, - [](UpdateType const& obj, UpdateType const& first) { return obj.id == first.id; }); - }); }; - auto const is_component_update_independent = [&update_data, &process_buffer_span]() -> bool { + auto const check_each_component = [&update_data, &process_buffer_span, + &all_comp_count_in_base]() -> UpdateCompProperties { // get span of all the update data - if (update_data.is_columnar(CT::name)) { - return process_buffer_span.template operator()( - update_data.get_columnar_buffer_span_all_scenarios()); + auto const comp_index = update_data.find_component(CT::name, false); + UpdateCompProperties result; + result.name = CT::name; + result.is_columnar = update_data.is_columnar(result.name); + result.dense = update_data.is_dense(result.name); + result.uniform = update_data.is_uniform(result.name); + result.has_any_elements = + comp_index != invalid_index && update_data.get_component_info(comp_index).total_elements > 0; + + result.elements_ps_in_update = + result.uniform ? update_data.uniform_elements_per_scenario(result.name) : invalid_index; + + if (auto it = all_comp_count_in_base.find(result.name); it != all_comp_count_in_base.end()) { + result.elements_in_base = it->second; + } else { + result.elements_in_base = 0; + } + + if (result.is_columnar) { + process_buffer_span.template operator()( + update_data.get_columnar_buffer_span_all_scenarios(), result); + } else { + process_buffer_span.template operator()( + update_data.get_buffer_span_all_scenarios(), result); } - return process_buffer_span.template operator()( - update_data.get_buffer_span_all_scenarios()); + return result; }; - // check all components - auto const update_independent = run_functor_with_all_types_return_array(is_component_update_independent); - return std::ranges::all_of(update_independent, [](bool const is_independent) { return is_independent; }); + // check and return indenpendence of all components + return run_functor_with_all_types_return_vector(check_each_component); + } + + std::unordered_map, std::equal_to<>> + is_update_independent(ConstDataset const& update_data) { + std::unordered_map, std::equal_to<>> result; + + // If the batch size is (0 or) 1, then the update data for this component is 'independent' + if (update_data.batch_size() <= 1) { + result["all component"] = true; + return result; + } + + auto const all_comp_update_independence = check_components_independence(update_data); + for (auto const& comp : all_comp_update_independence) { + result[comp.name] = comp.is_independent(); + } + + return result; + } + + void validate_update_data_independence(UpdateCompProperties const& comp) const { + if (comp.is_empty_component()) { + return; // empty dataset is still supported + } + auto const elements_ps = comp.get_n_elements(); + assert(comp.uniform || elements_ps < 0); + + if (elements_ps >= 0 && comp.elements_in_base < elements_ps) { + throw DatasetError("Update data has more elements per scenario than input data for component " + comp.name + + "!"); + } + if (comp.ids_part_na) { + throw DatasetError("Some IDs are not valid for component " + comp.name + " in update data!"); + } + if (comp.ids_all_na && comp.elements_in_base != elements_ps) { + throw DatasetError("Update data without IDs for component " + comp.name + + " has a different number of elements per scenario then input data!"); + } } + // Calculate with optimization, e.g., automatic tap changer template auto calculate(Options const& options) { auto const calculator = [this, &options] { if constexpr (std::derived_from) { diff --git a/src/power_grid_model/_utils.py b/src/power_grid_model/_utils.py index 23e230d4b..7f79eebe2 100644 --- a/src/power_grid_model/_utils.py +++ b/src/power_grid_model/_utils.py @@ -539,9 +539,28 @@ def is_columnar(component_data: ComponentData) -> bool: return not isinstance(component_data, np.ndarray) -def is_nan_or_equivalent(array): +def is_nan_or_default(x: np.ndarray) -> np.ndarray: + """ + Check if elements in the array are NaN or equal to the min of its dtype. + + Args: + x: A NumPy array to check. + + Returns: + A boolean NumPy array where each element is True if the corresponding element in x is NaN + or min of its dtype, and False otherwise. + """ + if x.dtype == np.float64: + return np.isnan(x) + if x.dtype in (np.int32, np.int8): + return x == np.iinfo(x.dtype).min + raise TypeError(f"Unsupported data type: {x.dtype}") + + +def is_nan_or_equivalent(array) -> bool: """ Check if the array contains only nan values or equivalent nan values for specific data types. + This is the aggregrated version of `is_nan_or_default` for the whole array. Args: array: The array to check. @@ -549,7 +568,7 @@ def is_nan_or_equivalent(array): Returns: bool: True if the array contains only nan or equivalent nan values, False otherwise. """ - return isinstance(array, np.ndarray) and ( + return isinstance(array, np.ndarray) and bool( (array.dtype == np.float64 and np.isnan(array).all()) or (array.dtype in (np.int32, np.int8) and np.all(array == np.iinfo(array.dtype).min)) ) @@ -749,3 +768,19 @@ def get_dataset_type(data: Dataset) -> DatasetType: raise ValueError("The dataset type could not be deduced because multiple dataset types match the data.") return next(iter(candidates)) + + +def get_comp_size(comp_data: SingleColumnarData | SingleArray) -> int: + """ + Get the number of elements in the comp_data of a single dataset. + + Args: + comp_data: Columnar or row based data of a single batch + + Returns: + Number of elements in the component + """ + if not is_columnar(comp_data): + return len(comp_data) + comp_data = cast(SingleColumnarData, comp_data) + return len(next(iter(comp_data.values()))) diff --git a/src/power_grid_model/validation/errors.py b/src/power_grid_model/validation/errors.py index 58bc84649..57c8488aa 100644 --- a/src/power_grid_model/validation/errors.py +++ b/src/power_grid_model/validation/errors.py @@ -129,9 +129,9 @@ class SingleFieldValidationError(ValidationError): _message = "Field {field} is not valid for {n} {objects}." component: ComponentType field: str - ids: list[int] + ids: Optional[list[int]] - def __init__(self, component: ComponentType, field: str, ids: Iterable[int]): + def __init__(self, component: ComponentType, field: str, ids: Optional[Iterable[int]]): """ Args: component: Component name @@ -140,7 +140,7 @@ def __init__(self, component: ComponentType, field: str, ids: Iterable[int]): """ self.component = component self.field = field - self.ids = sorted(ids) + self.ids = sorted(ids) if ids is not None else None class MultiFieldValidationError(ValidationError): @@ -325,12 +325,13 @@ def __init__( # pylint: disable=too-many-arguments self, component: ComponentType, field: str, - ids: list[int], - ref_components: ComponentType | list[ComponentType], + ids: Optional[list[int]] = None, + ref_components: Optional[ComponentType | list[ComponentType]] = None, filters: Optional[dict[str, Any]] = None, ): # pylint: disable=too-many-positional-arguments super().__init__(component=component, field=field, ids=ids) + ref_components = ref_components if ref_components is not None else [] self.ref_components = [ref_components] if isinstance(ref_components, (str, ComponentType)) else ref_components self.filters = filters if filters else None diff --git a/src/power_grid_model/validation/rules.py b/src/power_grid_model/validation/rules.py index fdb4df991..7046fb9f6 100644 --- a/src/power_grid_model/validation/rules.py +++ b/src/power_grid_model/validation/rules.py @@ -40,6 +40,7 @@ import numpy as np from power_grid_model import ComponentType +from power_grid_model._utils import get_comp_size, is_nan_or_default from power_grid_model.data_types import SingleDataset from power_grid_model.enum import FaultPhase, FaultType, WindingType from power_grid_model.validation.errors import ( @@ -678,26 +679,39 @@ def all_not_two_values_equal( return [] -def all_ids_exist_in_data_set( - data: SingleDataset, ref_data: SingleDataset, component: ComponentType, ref_name: str -) -> list[IdNotInDatasetError]: +def ids_valid_in_update_data_set( + update_data: SingleDataset, ref_data: SingleDataset, component: ComponentType, ref_name: str +) -> list[IdNotInDatasetError | InvalidIdError]: """ - Check that for all records of a particular type of component, the ids exist in the reference data set. + Check that for all records of a particular type of component, whether the ids: + - exist and match those in the reference data set + - are not present but qualifies for optional id Args: - data: The (update) data set for all components + update_data: The update data set for all components ref_data: The reference (input) data set for all components component: The component of interest - ref_name: The name of the reference data set, e.g. 'input_data' + ref_name: The name of the reference data set, e.g. 'update_data' Returns: A list containing zero or one IdNotInDatasetError, listing all ids of the objects in the data set which do not exist in the reference data set. """ - component_data = data[component] + component_data = update_data[component] component_ref_data = ref_data[component] - if not isinstance(component_data, np.ndarray) or not isinstance(component_ref_data, np.ndarray): - raise NotImplementedError() # TODO(mgovers): add support for columnar data - + if component_ref_data["id"].size == 0: + return [InvalidIdError(component=component, field="id", ids=None)] + id_field_is_nan = np.array(is_nan_or_default(component_data["id"])) + # check whether id qualify for optional + if component_data["id"].size == 0 or np.all(id_field_is_nan): + # check if the dimension of the component_data is the same as the component_ref_data + if get_comp_size(component_data) != get_comp_size(component_ref_data): + return [InvalidIdError(component=component, field="id", ids=None)] + return [] # supported optional id + + if np.all(id_field_is_nan) and not np.all(~id_field_is_nan): + return [InvalidIdError(component=component, field="id", ids=None)] + + # normal check: exist and match with input invalid = np.isin(component_data["id"], component_ref_data["id"], invert=True) if invalid.any(): ids = component_data["id"][invalid].flatten().tolist() diff --git a/src/power_grid_model/validation/utils.py b/src/power_grid_model/validation/utils.py index 7e421b04a..16fdee684 100644 --- a/src/power_grid_model/validation/utils.py +++ b/src/power_grid_model/validation/utils.py @@ -123,6 +123,13 @@ def _update_component_array_data( Update the data in a numpy array, with another numpy array, indexed on the "id" field and only non-NaN values are overwritten. """ + optional_ids_active = ( + "id" in update_data.dtype.names + and np.all(update_data["id"] == np.iinfo(update_data["id"].dtype).min) + and len(update_data["id"]) == len(input_data["id"]) + ) + update_data_ids = input_data["id"] if optional_ids_active else update_data["id"] + for field in update_data.dtype.names: if field == "id": continue @@ -136,12 +143,12 @@ def _update_component_array_data( for phase in range(mask.shape[1]): # find indexers of to-be-updated object sub_mask = mask[:, phase] - idx = get_indexer(input_data["id"], update_data["id"][sub_mask]) + idx = get_indexer(input_data["id"], update_data_ids[sub_mask]) # update input_data[field][idx, phase] = update_data[field][sub_mask, phase] else: # find indexers of to-be-updated object - idx = get_indexer(input_data["id"], update_data["id"][mask]) + idx = get_indexer(input_data["id"], update_data_ids[mask]) # update input_data[field][idx] = update_data[field][mask] diff --git a/src/power_grid_model/validation/validation.py b/src/power_grid_model/validation/validation.py index dbbbd7ad6..5e76af9a8 100644 --- a/src/power_grid_model/validation/validation.py +++ b/src/power_grid_model/validation/validation.py @@ -33,6 +33,7 @@ ) from power_grid_model.validation.errors import ( IdNotInDatasetError, + InvalidIdError, MissingValueError, MultiComponentNotUniqueError, ValidationError, @@ -47,7 +48,6 @@ all_greater_or_equal, all_greater_than_or_equal_to_zero, all_greater_than_zero, - all_ids_exist_in_data_set, all_less_than, all_not_two_values_equal, all_not_two_values_zero, @@ -58,6 +58,7 @@ all_valid_enum_values, all_valid_fault_phases, all_valid_ids, + ids_valid_in_update_data_set, none_missing, valid_p_q_sigma, ) @@ -149,10 +150,12 @@ def validate_batch_data( for batch, batch_update_data in enumerate(batch_data): row_update_data = compatibility_convert_row_columnar_dataset(batch_update_data, None, DatasetType.update) assert_valid_data_structure(row_update_data, DatasetType.update) - id_errors: list[ValidationError] = list(validate_ids_exist(row_update_data, input_data_copy)) + id_errors: list[IdNotInDatasetError | InvalidIdError] = validate_ids(row_update_data, input_data_copy) batch_errors = input_errors + id_errors + if not id_errors: + batch_errors = input_errors merged_data = update_input_data(input_data_copy, row_update_data) batch_errors += validate_required_values(merged_data, calculation_type, symmetric) batch_errors += validate_values(merged_data, calculation_type) @@ -216,23 +219,26 @@ def validate_unique_ids_across_components(data: SingleDataset) -> list[MultiComp return all_cross_unique(data, [(component, "id") for component in data]) -def validate_ids_exist(update_data: SingleDataset, input_data: SingleDataset) -> list[IdNotInDatasetError]: +def validate_ids(update_data: SingleDataset, input_data: SingleDataset) -> list[IdNotInDatasetError | InvalidIdError]: """ - Checks if all ids of the components in the update data exist in the input data. This needs to be true, because you - can only update existing components. + Checks if all ids of the components in the update data: + - exist and match those in the input data + - are not present but qualifies for optional id This function should be called for every update dataset in a batch set Args: update_data: A single update dataset - input_data: A power-grid-model input dataset + input_data: Input dataset Returns: - An empty list if all update data ids exist in the input dataset, or a list of IdNotInDatasetErrors for - all update components of which the id does not exist in the input dataset + An empty list if all update data ids are valid, or a list of IdNotInDatasetErrors or InvalidIdError for + all update components that have invalid ids """ - errors = (all_ids_exist_in_data_set(update_data, input_data, component, "input_data") for component in update_data) + errors = ( + ids_valid_in_update_data_set(update_data, input_data, component, "update_data") for component in update_data + ) return list(chain(*errors)) diff --git a/tests/cpp_integration_tests/CMakeLists.txt b/tests/cpp_integration_tests/CMakeLists.txt index d09ec1600..fe3322bd1 100644 --- a/tests/cpp_integration_tests/CMakeLists.txt +++ b/tests/cpp_integration_tests/CMakeLists.txt @@ -7,7 +7,6 @@ set(PROJECT_SOURCES "test_main_model_sc.cpp" "test_main_model_se.cpp" "test_main_model.cpp" - "test_main_model_static.cpp" "test_math_solver.cpp" ) diff --git a/tests/cpp_integration_tests/test_main_model.cpp b/tests/cpp_integration_tests/test_main_model.cpp index 62449ab21..667e3da21 100644 --- a/tests/cpp_integration_tests/test_main_model.cpp +++ b/tests/cpp_integration_tests/test_main_model.cpp @@ -1346,6 +1346,7 @@ TEST_CASE("Test main model - runtime dispatch") { CHECK(node_output_from_columnar[idx].u_pu == node_output_from_row_based[idx].u_pu); } } + SUBCASE("Columnar buffers in output data") { MainModel model{50.0, input_data}; @@ -1373,67 +1374,116 @@ TEST_CASE("Test main model - runtime dispatch") { CHECK(columnar_node_output_u_pu[idx] == doctest::Approx(row_based_node_output[idx].u_pu)); } } + SUBCASE("Columnar buffers in update data") { std::vector sym_load_ids; std::vector sym_load_p_specified; - std::ranges::transform(state.sym_load_update, std::back_inserter(sym_load_ids), - [](auto const& sym_load) { return sym_load.id; }); std::ranges::transform(state.sym_load_update, std::back_inserter(sym_load_p_specified), [](auto const& sym_load) { return sym_load.p_specified; }); - REQUIRE(sym_load_ids.size() == sym_load_p_specified.size()); - REQUIRE(sym_load_ids.size() == state.sym_load_update.size()); - - ConstDataset update_data_with_rows{false, 1, "update", meta_data::meta_data_gen::meta_data}; - update_data_with_rows.add_buffer("sym_load", state.sym_load_update.size(), state.sym_load_update.size(), - nullptr, state.sym_load_update.data()); - ConstDataset update_data_with_columns{false, 1, "update", meta_data::meta_data_gen::meta_data}; - update_data_with_columns.add_buffer("sym_load", sym_load_ids.size(), sym_load_ids.size(), nullptr, nullptr); - update_data_with_columns.add_attribute_buffer("sym_load", "id", sym_load_ids.data()); - update_data_with_columns.add_attribute_buffer("sym_load", "p_specified", sym_load_p_specified.data()); - - MainModel base_model{50.0, input_data}; - MainModel row_based_model{base_model}; - MainModel columnar_model{base_model}; - row_based_model.update_component(update_data_with_rows); - columnar_model.update_component(update_data_with_columns); - - std::vector node_output_from_base(state.node_input.size()); - std::vector node_output_from_row_based(state.node_input.size()); - std::vector node_output_from_columnar(state.node_input.size()); - - MutableDataset sym_output_from_base{true, 1, "sym_output", meta_data::meta_data_gen::meta_data}; - sym_output_from_base.add_buffer("node", node_output_from_base.size(), node_output_from_base.size(), nullptr, - node_output_from_base.data()); - MutableDataset sym_output_from_row_based{true, 1, "sym_output", meta_data::meta_data_gen::meta_data}; - sym_output_from_row_based.add_buffer("node", node_output_from_row_based.size(), - node_output_from_row_based.size(), nullptr, - node_output_from_row_based.data()); - MutableDataset sym_output_from_columnar{true, 1, "sym_output", meta_data::meta_data_gen::meta_data}; - sym_output_from_columnar.add_buffer("node", node_output_from_columnar.size(), - node_output_from_columnar.size(), nullptr, - node_output_from_columnar.data()); - - base_model.calculate(options, sym_output_from_base); - row_based_model.calculate(options, sym_output_from_row_based); - columnar_model.calculate(options, sym_output_from_columnar); - - REQUIRE(node_output_from_columnar.size() == node_output_from_base.size()); - REQUIRE(node_output_from_columnar.size() == node_output_from_row_based.size()); + std::ranges::transform(state.sym_load_update, std::back_inserter(sym_load_ids), + [](auto const& sym_load) { return sym_load.id; }); + REQUIRE(sym_load_ids.size() == sym_load_p_specified.size()); + REQUIRE(sym_load_p_specified.size() == state.sym_load_update.size()); + + auto const update_size = sym_load_ids.size(); + + SUBCASE("With IDs") { + ConstDataset update_data_with_rows{false, 1, "update", meta_data::meta_data_gen::meta_data}; + update_data_with_rows.add_buffer("sym_load", state.sym_load_update.size(), state.sym_load_update.size(), + nullptr, state.sym_load_update.data()); + + ConstDataset update_data_with_columns{false, 1, "update", meta_data::meta_data_gen::meta_data}; + update_data_with_columns.add_buffer("sym_load", update_size, update_size, nullptr, nullptr); + update_data_with_columns.add_attribute_buffer("sym_load", "id", sym_load_ids.data()); + update_data_with_columns.add_attribute_buffer("sym_load", "p_specified", sym_load_p_specified.data()); + + MainModel base_model{50.0, input_data}; + MainModel row_based_model{base_model}; + MainModel columnar_model{base_model}; + row_based_model.update_component(update_data_with_rows); + columnar_model.update_component(update_data_with_columns); + + std::vector node_output_from_base(state.node_input.size()); + std::vector node_output_from_row_based(state.node_input.size()); + std::vector node_output_from_columnar(state.node_input.size()); + + MutableDataset sym_output_from_base{true, 1, "sym_output", meta_data::meta_data_gen::meta_data}; + sym_output_from_base.add_buffer("node", node_output_from_base.size(), node_output_from_base.size(), + nullptr, node_output_from_base.data()); + MutableDataset sym_output_from_row_based{true, 1, "sym_output", meta_data::meta_data_gen::meta_data}; + sym_output_from_row_based.add_buffer("node", node_output_from_row_based.size(), + node_output_from_row_based.size(), nullptr, + node_output_from_row_based.data()); + MutableDataset sym_output_from_columnar{true, 1, "sym_output", meta_data::meta_data_gen::meta_data}; + sym_output_from_columnar.add_buffer("node", node_output_from_columnar.size(), + node_output_from_columnar.size(), nullptr, + node_output_from_columnar.data()); + + base_model.calculate(options, sym_output_from_base); + row_based_model.calculate(options, sym_output_from_row_based); + columnar_model.calculate(options, sym_output_from_columnar); + + REQUIRE(node_output_from_columnar.size() == node_output_from_base.size()); + REQUIRE(node_output_from_columnar.size() == node_output_from_row_based.size()); + + for (Idx idx = 0; idx < std::ssize(node_output_from_columnar); ++idx) { + // check columnar updates work same way as row-based updates + CHECK(node_output_from_columnar[idx].id == doctest::Approx(node_output_from_row_based[idx].id)); + CHECK(node_output_from_columnar[idx].u_pu == doctest::Approx(node_output_from_row_based[idx].u_pu)); + // check update actually changed something + CHECK(node_output_from_columnar[idx].id == doctest::Approx(node_output_from_base[idx].id)); + if (idx == 0) { // sym_load node + CHECK(node_output_from_columnar[idx].u_pu == doctest::Approx(node_output_from_base[idx].u_pu)); + } else { + CHECK(node_output_from_columnar[idx].u_pu != doctest::Approx(node_output_from_base[idx].u_pu)); + } + } + } - for (Idx idx = 0; idx < std::ssize(node_output_from_columnar); ++idx) { - // check columnar updates work same way as row-based updates - CHECK(node_output_from_columnar[idx].id == doctest::Approx(node_output_from_row_based[idx].id)); - CHECK(node_output_from_columnar[idx].u_pu == doctest::Approx(node_output_from_row_based[idx].u_pu)); - // check update actually changed something - CHECK(node_output_from_columnar[idx].id == doctest::Approx(node_output_from_base[idx].id)); - if (idx == 0) { // sym_load node - CHECK(node_output_from_columnar[idx].u_pu == doctest::Approx(node_output_from_base[idx].u_pu)); - } else { - CHECK(node_output_from_columnar[idx].u_pu != doctest::Approx(node_output_from_base[idx].u_pu)); + SUBCASE("Without IDs") { + ConstDataset update_data_with_ids{false, 1, "update", meta_data::meta_data_gen::meta_data}; + update_data_with_ids.add_buffer("sym_load", update_size, update_size, nullptr, nullptr); + update_data_with_ids.add_attribute_buffer("sym_load", "id", sym_load_ids.data()); + update_data_with_ids.add_attribute_buffer("sym_load", "p_specified", sym_load_p_specified.data()); + + ConstDataset update_data_without_ids{false, 1, "update", meta_data::meta_data_gen::meta_data}; + update_data_without_ids.add_buffer("sym_load", update_size, update_size, nullptr, nullptr); + update_data_without_ids.add_attribute_buffer("sym_load", "p_specified", sym_load_p_specified.data()); + + MainModel base_model{50.0, input_data}; + MainModel columnar_model_w_id{base_model}; + MainModel columnar_model_wo_id{base_model}; + + columnar_model_w_id.update_component(update_data_with_ids); + columnar_model_wo_id.update_component(update_data_without_ids); + + std::vector node_output_columnar_w_id(state.node_input.size()); + std::vector node_output_columnar_wo_id(state.node_input.size()); + + MutableDataset sym_output_columnar_w_id{true, 1, "sym_output", meta_data::meta_data_gen::meta_data}; + sym_output_columnar_w_id.add_buffer("node", node_output_columnar_w_id.size(), + node_output_columnar_w_id.size(), nullptr, + node_output_columnar_w_id.data()); + MutableDataset sym_output_columnar_wo_id{true, 1, "sym_output", meta_data::meta_data_gen::meta_data}; + sym_output_columnar_wo_id.add_buffer("node", node_output_columnar_wo_id.size(), + node_output_columnar_wo_id.size(), nullptr, + node_output_columnar_wo_id.data()); + + columnar_model_w_id.calculate(options, sym_output_columnar_w_id); + columnar_model_wo_id.calculate(options, sym_output_columnar_wo_id); + + REQUIRE(node_output_columnar_wo_id.size() == node_output_columnar_w_id.size()); + REQUIRE(node_output_columnar_wo_id.size() == node_output_columnar_w_id.size()); + + for (Idx idx = 0; idx < std::ssize(node_output_columnar_w_id); ++idx) { + // check columnar updates without ids work same way as ones with ids + CHECK(node_output_columnar_wo_id[idx].id == doctest::Approx(node_output_columnar_w_id[idx].id)); + CHECK(node_output_columnar_wo_id[idx].u_pu == doctest::Approx(node_output_columnar_w_id[idx].u_pu)); } } } + SUBCASE("Empty columnar update data") { std::vector sym_load_ids; std::vector sym_load_p_specified; diff --git a/tests/cpp_integration_tests/test_main_model_static.cpp b/tests/cpp_integration_tests/test_main_model_static.cpp deleted file mode 100644 index eb921c3b3..000000000 --- a/tests/cpp_integration_tests/test_main_model_static.cpp +++ /dev/null @@ -1,37 +0,0 @@ -// SPDX-FileCopyrightText: Contributors to the Power Grid Model project -// -// SPDX-License-Identifier: MPL-2.0 - -#include -#include - -#include - -namespace power_grid_model { - -TEST_CASE("Test main model static") { - // Cacheable and independent base update data - std::vector link{{0, na_IntS, na_IntS}, {1, na_IntS, na_IntS}, {0, na_IntS, na_IntS}, - {1, na_IntS, na_IntS}, {0, na_IntS, na_IntS}, {3, na_IntS, na_IntS}}; - - std::vector source{{0, na_IntS, 1.0, nan}, {0, na_IntS, 1.0, nan}, {0, na_IntS, 1.0, nan}}; - - Idx const batches = 3; - std::array const link_indptr = {0, 2, 4, 6}; - std::array const source_indptr = {0, 1, 2, 3}; - // dependent dataset - ConstDataset update_data_dependent{true, batches, "update", meta_data::meta_data_gen::meta_data}; - update_data_dependent.add_buffer("link", -1, link_indptr.back(), link_indptr.data(), link.data()); - update_data_dependent.add_buffer("source", -1, source_indptr.back(), source_indptr.data(), source.data()); - - // independent dataset - ConstDataset update_data_independent{true, batches - 1, "update", meta_data::meta_data_gen::meta_data}; - update_data_independent.add_buffer("link", -1, link_indptr.crbegin()[1], link_indptr.data(), source.data()); - update_data_independent.add_buffer("source", -1, source_indptr.crbegin()[1], source_indptr.data(), source.data()); - - SUBCASE("Independent update data") { CHECK(MainModel::is_update_independent(update_data_independent) == true); } - - SUBCASE("Dependent update data") { CHECK(MainModel::is_update_independent(update_data_dependent) == false); } -} - -} // namespace power_grid_model diff --git a/tests/cpp_unit_tests/test_container.cpp b/tests/cpp_unit_tests/test_container.cpp index 6442cf38f..15775c1c3 100644 --- a/tests/cpp_unit_tests/test_container.cpp +++ b/tests/cpp_unit_tests/test_container.cpp @@ -193,6 +193,23 @@ TEST_CASE("Test component container") { CHECK(const_container2.get_item_by_seq(1).a == 66); CHECK(const_container2.get_item_by_seq(2).a == 7); } + + SUBCASE("Test get group index") { + CHECK(const_container.get_group_idx() == 0); + CHECK(const_container.get_group_idx() == 1); + CHECK(const_container.get_group_idx() == 2); + } + +#ifndef NDEBUG + SUBCASE("Test get id by idx2d") { + CHECK(const_container.get_id_by_idx(Idx2D{0, 0}) == 1); + CHECK(const_container.get_id_by_idx(Idx2D{0, 1}) == 11); + CHECK(const_container.get_id_by_idx(Idx2D{0, 2}) == 111); + CHECK(const_container.get_id_by_idx(Idx2D{1, 0}) == 2); + CHECK(const_container.get_id_by_idx(Idx2D{1, 1}) == 22); + CHECK(const_container.get_id_by_idx(Idx2D{2, 0}) == 3); + } +#endif // NDEBUG } } // namespace power_grid_model diff --git a/tests/native_api_tests/test_api_model.cpp b/tests/native_api_tests/test_api_model.cpp index dd08523c9..21536ba90 100644 --- a/tests/native_api_tests/test_api_model.cpp +++ b/tests/native_api_tests/test_api_model.cpp @@ -70,7 +70,7 @@ void check_throws_with(Func&& func, PGM_ErrorCode const& reference_error, std::s try { std::forward(func)(std::forward(args)...); FAIL("Expected error not thrown."); - } catch (PowerGridRegularError const& e) { + } catch (PowerGridError const& e) { check_exception(e, reference_error, reference_err_msg); } } @@ -81,6 +81,7 @@ struct columnar_t {}; struct sparse_t {}; struct dense_t {}; struct with_id_t {}; +struct optional_id_t {}; struct invalid_id_t {}; template struct TypeCombo { @@ -321,7 +322,9 @@ TEST_CASE("API Model") { ID const good_source_update_id = 1; load_buffer.set_value(PGM_def_input_sym_load_id, &bad_load_id, -1); source_update_buffer.set_value(PGM_def_update_source_id, &good_source_update_id, 0, -1); + auto const wrong_model_lambda = [&input_dataset]() { Model const wrong_model{50.0, input_dataset}; }; + check_throws_with(wrong_model_lambda, PGM_regular_error, "Conflicting id detected:"s); } @@ -330,10 +333,32 @@ TEST_CASE("API Model") { ID const bad_source_update_id = 99; load_buffer.set_value(PGM_def_input_sym_load_id, &good_load_id, -1); source_update_buffer.set_value(PGM_def_update_source_id, &bad_source_update_id, 0, -1); + auto const bad_update_lambda = [&model, &single_update_dataset]() { model.update(single_update_dataset); }; + check_throws_with(bad_update_lambda, PGM_regular_error, "The id cannot be found:"s); } + SUBCASE("Update error in calculation") { + ID const bad_load_id = 2; + load_buffer.set_value(PGM_def_input_sym_load_id, &bad_load_id, -1); + DatasetConst bad_batch_update_dataset{"update", 1, 2}; + bad_batch_update_dataset.add_buffer("source", -1, 1, source_update_indptr.data(), + source_update_buffer.get()); + bad_batch_update_dataset.add_buffer("sym_load", 1, 2, nullptr, load_updates_buffer); + bad_batch_update_dataset.add_buffer("line", 2, 4, nullptr, nullptr); // columnar input for line + std::vector const bad_batch_line_id{99, 999, 9999, 99999}; + bad_batch_update_dataset.add_attribute_buffer("line", "id", bad_batch_line_id.data()); + bad_batch_update_dataset.add_attribute_buffer("line", "from_status", batch_line_from_status.data()); + bad_batch_update_dataset.add_attribute_buffer("line", "to_status", batch_line_to_status.data()); + + auto const bad_calc_with_update_lambda = [&model, &options, &batch_output_dataset, + &bad_batch_update_dataset]() { + model.calculate(options, batch_output_dataset, bad_batch_update_dataset); + }; + check_throws_with(bad_calc_with_update_lambda, PGM_batch_error, "The id cannot be found:"s); + } + SUBCASE("Invalid calculation type error") { auto const bad_calc_type_lambda = [&options, &model, &single_output_dataset]() { options.set_calculation_type(-128); @@ -376,37 +401,221 @@ TEST_CASE("API Model") { } SUBCASE("Batch calculation error") { - // wrong id - load_updates_id[1] = 999; - load_updates_buffer.set_value(PGM_def_update_sym_load_id, load_updates_id.data(), 1, -1); - // failed in batch 1 - try { - model.calculate(options, batch_output_dataset, batch_update_dataset); - FAIL("Expected batch calculation error not thrown."); - } catch (PowerGridBatchError const& e) { - CHECK(e.error_code() == PGM_batch_error); - auto const& failed_scenarios = e.failed_scenarios(); - CHECK(failed_scenarios.size() == 1); - CHECK(failed_scenarios[0].scenario == 1); - std::string const err_msg{failed_scenarios[0].error_message}; - CHECK(err_msg.find("The id cannot be found:"s) != std::string::npos); + SUBCASE("Line bad line id") { + // wrong id + load_updates_id[1] = 999; + load_updates_buffer.set_value(PGM_def_update_sym_load_id, load_updates_id.data(), 1, -1); + // failed in batch 1 + try { + model.calculate(options, batch_output_dataset, batch_update_dataset); + FAIL("Expected batch calculation error not thrown."); + } catch (PowerGridBatchError const& e) { + CHECK(e.error_code() == PGM_batch_error); + auto const& failed_scenarios = e.failed_scenarios(); + CHECK(failed_scenarios.size() == 1); + CHECK(failed_scenarios[0].scenario == 1); + std::string const err_msg{failed_scenarios[0].error_message}; + CHECK(err_msg.find("The id cannot be found:"s) != std::string::npos); + } + // valid results for batch 0 + node_batch_output.get_value(PGM_def_sym_output_node_id, batch_node_result_id.data(), -1); + node_batch_output.get_value(PGM_def_sym_output_node_energized, batch_node_result_energized.data(), -1); + node_batch_output.get_value(PGM_def_sym_output_node_u, batch_node_result_u.data(), -1); + node_batch_output.get_value(PGM_def_sym_output_node_u_pu, batch_node_result_u_pu.data(), -1); + node_batch_output.get_value(PGM_def_sym_output_node_u_angle, batch_node_result_u_angle.data(), -1); + CHECK(batch_node_result_id[0] == 0); + CHECK(batch_node_result_energized[0] == 1); + CHECK(batch_node_result_u[0] == doctest::Approx(40.0)); + CHECK(batch_node_result_u_pu[0] == doctest::Approx(0.4)); + CHECK(batch_node_result_u_angle[0] == doctest::Approx(0.0)); + CHECK(batch_node_result_id[1] == 4); + CHECK(batch_node_result_energized[1] == 0); + CHECK(batch_node_result_u[1] == doctest::Approx(0.0)); + CHECK(batch_node_result_u_pu[1] == doctest::Approx(0.0)); + CHECK(batch_node_result_u_angle[1] == doctest::Approx(0.0)); + } + } + } + + SUBCASE("Model update optional id") { + std::vector const input_node_id{0}; + std::vector const input_node_u_rated{100.0}; + Buffer input_node_buffer{PGM_def_input_node, 1}; + input_node_buffer.set_nan(); + input_node_buffer.set_value(PGM_def_input_node_id, input_node_id.data(), -1); + input_node_buffer.set_value(PGM_def_input_node_u_rated, input_node_u_rated.data(), -1); + + std::vector const input_source_id{1}; + std::vector const input_source_node{0}; + std::vector const input_source_status{1}; + std::vector const input_source_u_ref{1.0}; + std::vector const input_source_sk{1000.0}; + std::vector const input_source_rx_ratio{0.0}; + Buffer input_source_buffer{PGM_def_input_source, 1}; + input_source_buffer.set_nan(); + input_source_buffer.set_value(PGM_def_input_source_id, input_source_id.data(), -1); + input_source_buffer.set_value(PGM_def_input_source_node, input_source_node.data(), -1); + input_source_buffer.set_value(PGM_def_input_source_status, input_source_status.data(), -1); + input_source_buffer.set_value(PGM_def_input_source_u_ref, input_source_u_ref.data(), -1); + input_source_buffer.set_value(PGM_def_input_source_sk, input_source_sk.data(), -1); + input_source_buffer.set_value(PGM_def_input_source_rx_ratio, input_source_rx_ratio.data(), -1); + + std::vector const input_sym_load_id{2}; + std::vector const input_sym_load_node{0}; + std::vector const input_sym_load_status{1}; + std::vector const input_sym_load_type{2}; + std::vector const input_sym_load_p_specified{0.0}; + std::vector const input_sym_load_q_specified{500.0}; + Buffer input_sym_load_buffer{PGM_def_input_sym_load, 1}; + input_sym_load_buffer.set_nan(); + input_sym_load_buffer.set_value(PGM_def_input_sym_load_id, input_sym_load_id.data(), -1); + input_sym_load_buffer.set_value(PGM_def_input_sym_load_node, input_sym_load_node.data(), -1); + input_sym_load_buffer.set_value(PGM_def_input_sym_load_status, input_sym_load_status.data(), -1); + input_sym_load_buffer.set_value(PGM_def_input_sym_load_type, input_sym_load_type.data(), -1); + input_sym_load_buffer.set_value(PGM_def_input_sym_load_p_specified, input_sym_load_p_specified.data(), -1); + input_sym_load_buffer.set_value(PGM_def_input_sym_load_q_specified, input_sym_load_q_specified.data(), -1); + + // input dataset - row + DatasetConst input_dataset_row{"input", 0, 1}; + input_dataset_row.add_buffer("node", 1, 1, nullptr, input_node_buffer); + input_dataset_row.add_buffer("source", 1, 1, nullptr, input_source_buffer); + input_dataset_row.add_buffer("sym_load", 1, 1, nullptr, input_sym_load_buffer); + + // input dataset - col + DatasetConst input_dataset_col{"input", 0, 1}; + input_dataset_col.add_buffer("node", 1, 1, nullptr, nullptr); + input_dataset_col.add_attribute_buffer("node", "id", input_node_id.data()); + input_dataset_col.add_attribute_buffer("node", "u_rated", input_node_u_rated.data()); + + input_dataset_col.add_buffer("source", 1, 1, nullptr, nullptr); + input_dataset_col.add_attribute_buffer("source", "id", input_source_id.data()); + input_dataset_col.add_attribute_buffer("source", "node", input_source_node.data()); + input_dataset_col.add_attribute_buffer("source", "status", input_source_status.data()); + input_dataset_col.add_attribute_buffer("source", "u_ref", input_source_u_ref.data()); + input_dataset_col.add_attribute_buffer("source", "sk", input_source_sk.data()); + input_dataset_col.add_attribute_buffer("source", "rx_ratio", input_source_rx_ratio.data()); + + input_dataset_col.add_buffer("sym_load", 1, 1, nullptr, nullptr); + input_dataset_col.add_attribute_buffer("sym_load", "id", input_sym_load_id.data()); + input_dataset_col.add_attribute_buffer("sym_load", "node", input_sym_load_node.data()); + input_dataset_col.add_attribute_buffer("sym_load", "status", input_sym_load_status.data()); + input_dataset_col.add_attribute_buffer("sym_load", "type", input_sym_load_type.data()); + input_dataset_col.add_attribute_buffer("sym_load", "p_specified", input_sym_load_p_specified.data()); + input_dataset_col.add_attribute_buffer("sym_load", "q_specified", input_sym_load_q_specified.data()); + + // update dataset + std::vector update_source_indptr{0, 1, 2}; + std::vector const update_source_id{1, 1}; + std::vector const update_source_u_ref{0.5, 1.0}; + Buffer update_source_buffer{PGM_def_update_source, 2}; + update_source_buffer.set_nan(); + update_source_buffer.set_value(PGM_def_update_source_id, update_source_id.data(), -1); + update_source_buffer.set_value(PGM_def_update_source_u_ref, update_source_u_ref.data(), -1); + + std::vector update_sym_load_indptr{0, 1, 2}; + std::vector const update_sym_load_id{2, 5}; + std::vector const update_sym_load_q_specified{100.0, 300.0}; + Buffer update_sym_load_buffer{PGM_def_update_sym_load, 2}; + update_sym_load_buffer.set_nan(); + update_sym_load_buffer.set_value(PGM_def_update_sym_load_id, update_sym_load_id.data(), -1); + update_sym_load_buffer.set_value(PGM_def_update_sym_load_q_specified, update_sym_load_q_specified.data(), -1); + + // update dataset buffers - no ids + Buffer update_source_buffer_no_id{PGM_def_update_source, 2}; + update_source_buffer_no_id.set_nan(); + update_source_buffer_no_id.set_value(PGM_def_update_source_u_ref, update_source_u_ref.data(), -1); + + Buffer update_sym_load_buffer_no_id{PGM_def_update_sym_load, 2}; + update_sym_load_buffer_no_id.set_nan(); + update_sym_load_buffer_no_id.set_value(PGM_def_update_sym_load_q_specified, update_sym_load_q_specified.data(), + -1); + + // update dataset - row + DatasetConst update_dataset_row{"update", 1, 2}; + update_dataset_row.add_buffer("source", -1, 2, update_source_indptr.data(), update_source_buffer); + update_dataset_row.add_buffer("sym_load", -1, 2, update_sym_load_indptr.data(), update_sym_load_buffer); + + // update dataset - col + DatasetConst update_dataset_col{"update", 1, 2}; + + update_dataset_col.add_buffer("source", -1, 2, update_source_indptr.data(), nullptr); + update_dataset_col.add_attribute_buffer("source", "id", update_source_id.data()); + update_dataset_col.add_attribute_buffer("source", "u_ref", update_source_u_ref.data()); + + update_dataset_col.add_buffer("sym_load", -1, 2, update_sym_load_indptr.data(), nullptr); + update_dataset_col.add_attribute_buffer("sym_load", "id", update_sym_load_id.data()); + update_dataset_col.add_attribute_buffer("sym_load", "q_specified", update_sym_load_q_specified.data()); + + // update dataset - row no ids + DatasetConst update_dataset_row_no_id{"update", 1, 2}; + update_dataset_row_no_id.add_buffer("source", -1, 2, update_source_indptr.data(), update_source_buffer_no_id); + update_dataset_row_no_id.add_buffer("sym_load", -1, 2, update_sym_load_indptr.data(), + update_sym_load_buffer_no_id); + + // update dataset - col no ids + DatasetConst update_dataset_col_no_id{"update", 1, 2}; + update_dataset_col_no_id.add_buffer("source", -1, 2, update_source_indptr.data(), nullptr); + + update_dataset_col_no_id.add_attribute_buffer("source", "u_ref", update_source_u_ref.data()); + + update_dataset_col_no_id.add_buffer("sym_load", -1, 2, update_sym_load_indptr.data(), nullptr); + update_dataset_col_no_id.add_attribute_buffer("sym_load", "q_specified", update_sym_load_q_specified.data()); + + // output data + Buffer batch_node_output{PGM_def_sym_output_node, 2}; + batch_node_output.set_nan(); + DatasetMutable batch_output{"sym_output", 1, 2}; + batch_output.add_buffer("node", 1, 2, nullptr, batch_node_output); + + // options + Options const batch_options{}; + + SUBCASE("Row-based input dataset") { + Model row_model{50.0, input_dataset_row}; + + SUBCASE("Row-based update dataset error") { + CHECK_THROWS_AS(row_model.calculate(batch_options, batch_output, update_dataset_row), + PowerGridBatchError); + } + SUBCASE("Row-based update dataset wo id") { + row_model.calculate(batch_options, batch_output_dataset, update_dataset_row_no_id); + } + SUBCASE("Columnar update dataset error") { + CHECK_THROWS_AS(row_model.calculate(batch_options, batch_output, update_dataset_col), + PowerGridBatchError); + } + SUBCASE("Columnar update dataset wo id") { + row_model.calculate(batch_options, batch_output, update_dataset_col_no_id); + } + SUBCASE("Columnar update dataset wo id - non-uniform") { + update_source_indptr = {0, 1, 1}; + CHECK_THROWS_AS(row_model.calculate(batch_options, batch_output, update_dataset_col_no_id), + PowerGridBatchError); + } + } + + SUBCASE("Columnar input dataset") { + Model col_model{50.0, input_dataset_col}; + + SUBCASE("Row-based update dataset error") { + CHECK_THROWS_AS(col_model.calculate(batch_options, batch_output, update_dataset_row), + PowerGridBatchError); + } + SUBCASE("Row-based update dataset wo id") { + col_model.calculate(batch_options, batch_output_dataset, update_dataset_row_no_id); + } + SUBCASE("Columnar update dataset error") { + CHECK_THROWS_AS(col_model.calculate(batch_options, batch_output, update_dataset_col), + PowerGridBatchError); + } + SUBCASE("Columnar update dataset wo id") { + col_model.calculate(batch_options, batch_output, update_dataset_col_no_id); + } + SUBCASE("Columnar update dataset wo id - non-uniform") { + update_source_indptr = {0, 1, 1}; + CHECK_THROWS_AS(col_model.calculate(batch_options, batch_output, update_dataset_col_no_id), + PowerGridBatchError); } - // valid results for batch 0 - node_batch_output.get_value(PGM_def_sym_output_node_id, batch_node_result_id.data(), -1); - node_batch_output.get_value(PGM_def_sym_output_node_energized, batch_node_result_energized.data(), -1); - node_batch_output.get_value(PGM_def_sym_output_node_u, batch_node_result_u.data(), -1); - node_batch_output.get_value(PGM_def_sym_output_node_u_pu, batch_node_result_u_pu.data(), -1); - node_batch_output.get_value(PGM_def_sym_output_node_u_angle, batch_node_result_u_angle.data(), -1); - CHECK(batch_node_result_id[0] == 0); - CHECK(batch_node_result_energized[0] == 1); - CHECK(batch_node_result_u[0] == doctest::Approx(40.0)); - CHECK(batch_node_result_u_pu[0] == doctest::Approx(0.4)); - CHECK(batch_node_result_u_angle[0] == doctest::Approx(0.0)); - CHECK(batch_node_result_id[1] == 4); - CHECK(batch_node_result_energized[1] == 0); - CHECK(batch_node_result_u[1] == doctest::Approx(0.0)); - CHECK(batch_node_result_u_pu[1] == doctest::Approx(0.0)); - CHECK(batch_node_result_u_angle[1] == doctest::Approx(0.0)); } } @@ -555,10 +764,15 @@ TEST_CASE_TEMPLATE( TypeCombo, TypeCombo, TypeCombo, TypeCombo, TypeCombo, TypeCombo, - TypeCombo, TypeCombo, - TypeCombo, TypeCombo, - TypeCombo, TypeCombo, - TypeCombo, TypeCombo) { + TypeCombo, TypeCombo, + TypeCombo, + TypeCombo, TypeCombo, + TypeCombo, TypeCombo, + TypeCombo, TypeCombo, + TypeCombo, TypeCombo, + TypeCombo, TypeCombo, + TypeCombo, TypeCombo, + TypeCombo) { using namespace std::string_literals; using input_type = typename T::input_type; @@ -632,7 +846,9 @@ TEST_CASE_TEMPLATE( Buffer sym_load_update_buffer{PGM_def_update_sym_load, 2}; sym_load_update_buffer.set_nan(); - sym_load_update_buffer.set_value(PGM_def_update_sym_load_id, load_updates_id.data(), -1); + if constexpr (!std::is_same_v) { + sym_load_update_buffer.set_value(PGM_def_update_sym_load_id, load_updates_id.data(), -1); + } sym_load_update_buffer.set_value(PGM_def_update_sym_load_q_specified, load_updates_q_specified.data(), -1); if constexpr (std::is_same_v) { @@ -648,7 +864,9 @@ TEST_CASE_TEMPLATE( update_dataset.add_buffer("sym_load", -1, 2, sym_load_indptr.data(), nullptr); } - update_dataset.add_attribute_buffer("sym_load", "id", load_updates_id.data()); + if constexpr (!std::is_same_v) { + update_dataset.add_attribute_buffer("sym_load", "id", load_updates_id.data()); + } update_dataset.add_attribute_buffer("sym_load", "q_specified", load_updates_q_specified.data()); } diff --git a/tests/unit/test_power_grid_model.py b/tests/unit/test_power_grid_model.py index 8abc5049f..257ffecc1 100644 --- a/tests/unit/test_power_grid_model.py +++ b/tests/unit/test_power_grid_model.py @@ -13,6 +13,7 @@ DatasetType, PowerGridModel, initialize_array, + power_grid_meta_data, ) from power_grid_model._utils import compatibility_convert_row_columnar_dataset from power_grid_model.errors import InvalidCalculationMethod, IterationDiverge, PowerGridBatchError, PowerGridError @@ -204,7 +205,7 @@ def test_single_calculation_error(model: PowerGridModel): model.calculate_short_circuit(calculation_method=calculation_method) -def test_batch_calculation_error(model: PowerGridModel, update_batch): +def test_batch_calculation_error(model: PowerGridModel, update_batch, input): # wrong id update_batch[ComponentType.sym_load]["data"]["id"][1] = 5 # with error @@ -329,7 +330,6 @@ def test_update_ids_batch(minimal_update, minimal_input): np.testing.assert_almost_equal(output_data[ComponentType.node]["u"], np.array([[90.0], [70.0]])) -@pytest.mark.xfail(reason="The current implementation does not support optional ids.") @pytest.mark.parametrize( "minimal_update", [ diff --git a/tests/unit/validation/test_batch_validation.py b/tests/unit/validation/test_batch_validation.py index 26520dae9..064f9853d 100644 --- a/tests/unit/validation/test_batch_validation.py +++ b/tests/unit/validation/test_batch_validation.py @@ -119,6 +119,7 @@ def test_validate_batch_data_input_error(input_data, batch_data): def test_validate_batch_data_update_error(input_data, batch_data): batch_data["line"]["from_status"] = np.array([[12, 34], [0, -128], [56, 78]]) errors = validate_batch_data(input_data, batch_data) - assert len(errors) == 2 - assert [NotBooleanError("line", "from_status", [5, 6])] == errors[0] - assert [NotBooleanError("line", "from_status", [5, 7])] == errors[2] + assert len(errors) == 3 + assert NotBooleanError("line", "from_status", [5, 6]) == errors[0][0] + assert NotBooleanError("line", "from_status", [5, 7]) == errors[1][1] + assert NotBooleanError("line", "from_status", [5, 6]) == errors[2][0] diff --git a/tests/unit/validation/test_utils.py b/tests/unit/validation/test_utils.py index e472c30cd..d2879dbaf 100644 --- a/tests/unit/validation/test_utils.py +++ b/tests/unit/validation/test_utils.py @@ -76,6 +76,21 @@ def test_update_input_data(): np.testing.assert_array_equal(merged["sym_load"]["q_specified"], [4.1, 5.2, 6.1, np.nan, np.nan, 3.2]) +def test_update_input_data__without_ids(): + input_test = initialize_array("input", "sym_load", 6) + input_test["id"] = [4, 5, 6, 1, 2, 3] + input_test["p_specified"] = [4.0, 5.0, 6.0, 1.0, 2.0, 3.0] + input_test["q_specified"] = [4.1, 5.1, 6.1, np.nan, np.nan, np.nan] + + update_test = initialize_array("update", "sym_load", 6) + input_test["q_specified"] = [4.1, 5.2, np.nan, np.nan, np.nan, 3.2] + + merged = update_input_data(input_data={"sym_load": input_test}, update_data={"sym_load": update_test}) + np.testing.assert_array_equal(merged["sym_load"]["id"], [4, 5, 6, 1, 2, 3]) + np.testing.assert_array_equal(merged["sym_load"]["p_specified"], [4.0, 5.0, 6.0, 1.0, 2.0, 3.0]) + np.testing.assert_array_equal(merged["sym_load"]["q_specified"], [4.1, 5.2, np.nan, np.nan, np.nan, 3.2]) + + def test_update_input_data_int_nan(): input_line = initialize_array("input", "line", 3) input_line["id"] = [1, 2, 3] diff --git a/tests/unit/validation/test_validation_functions.py b/tests/unit/validation/test_validation_functions.py index 646ecf1f8..73c1102d3 100644 --- a/tests/unit/validation/test_validation_functions.py +++ b/tests/unit/validation/test_validation_functions.py @@ -9,8 +9,16 @@ import pytest from power_grid_model import CalculationType, LoadGenType, MeasuredTerminalType, initialize_array, power_grid_meta_data +from power_grid_model._utils import compatibility_convert_row_columnar_dataset from power_grid_model.core.dataset_definitions import ComponentType, DatasetType -from power_grid_model.enum import Branch3Side, BranchSide, CalculationType, FaultType, TapChangingStrategy +from power_grid_model.enum import ( + Branch3Side, + BranchSide, + CalculationType, + ComponentAttributeFilterOptions, + FaultType, + TapChangingStrategy, +) from power_grid_model.validation import assert_valid_input_data from power_grid_model.validation.errors import ( IdNotInDatasetError, @@ -27,7 +35,7 @@ from power_grid_model.validation.validation import ( assert_valid_data_structure, validate_generic_power_sensor, - validate_ids_exist, + validate_ids, validate_required_values, validate_unique_ids_across_components, validate_values, @@ -113,7 +121,7 @@ def test_validate_unique_ids_across_components(): assert len(unique_id_errors[0].ids) == 4 -def test_validate_ids_exist(): +def test_validate_ids(): source = initialize_array("input", "source", 3) source["id"] = [1, 2, 3] @@ -135,10 +143,47 @@ def test_validate_ids_exist(): update_data = {"source": source_update, "sym_load": sym_load_update} - invalid_ids = validate_ids_exist(update_data, input_data) + invalid_ids = validate_ids(update_data, input_data) + + assert IdNotInDatasetError("source", [4], "update_data") in invalid_ids + assert IdNotInDatasetError("sym_load", [7], "update_data") in invalid_ids + + source_update_no_id = initialize_array("update", "source", 3) + source_update_no_id["u_ref"] = [1.0, 2.0, 3.0] + + update_data_col = compatibility_convert_row_columnar_dataset( + data={"source": source_update_no_id, "sym_load": sym_load_update}, + data_filter=ComponentAttributeFilterOptions.relevant, + dataset_type=DatasetType.update, + ) + invalid_ids = validate_ids(update_data_col, input_data) + assert len(invalid_ids) == 1 + assert IdNotInDatasetError("sym_load", [7], "update_data") in invalid_ids + + source_update_less_no_id = initialize_array("update", "source", 2) + source_update_less_no_id["u_ref"] = [1.0, 2.0] - assert IdNotInDatasetError("source", [4], "input_data") in invalid_ids - assert IdNotInDatasetError("sym_load", [7], "input_data") in invalid_ids + update_data_col_less_no_id = compatibility_convert_row_columnar_dataset( + data={"source": source_update_less_no_id, "sym_load": sym_load_update}, + data_filter=ComponentAttributeFilterOptions.relevant, + dataset_type=DatasetType.update, + ) + invalid_ids = validate_ids(update_data_col_less_no_id, input_data) + assert len(invalid_ids) == 2 + assert IdNotInDatasetError("sym_load", [7], "update_data") in invalid_ids + + source_update_part_nan_id = initialize_array("update", "source", 3) + source_update_part_nan_id["id"] = [1, np.iinfo(np.int32).min, 4] + source_update_part_nan_id["u_ref"] = [1.0, 2.0, 3.0] + + update_data_col_part_nan_id = compatibility_convert_row_columnar_dataset( + data={"source": source_update_part_nan_id, "sym_load": sym_load_update}, + data_filter=ComponentAttributeFilterOptions.relevant, + dataset_type=DatasetType.update, + ) + invalid_ids = validate_ids(update_data_col_part_nan_id, input_data) + assert len(invalid_ids) == 2 + assert IdNotInDatasetError("sym_load", [7], "update_data") in invalid_ids @pytest.mark.parametrize(