diff --git a/code_generation/data/attribute_classes/input.json b/code_generation/data/attribute_classes/input.json index e621f65d0d..edaf6bb38c 100644 --- a/code_generation/data/attribute_classes/input.json +++ b/code_generation/data/attribute_classes/input.json @@ -176,7 +176,7 @@ "name": "GenericBranchInput", "base": "BranchInput", "attributes": [ - { + { "data_type": "double", "names": ["r1","x1","g1","b1"], "description": "positive sequence parameters" @@ -540,6 +540,28 @@ } ] }, + { + "name": "VoltageRegulatorInput", + "base": "RegulatorInput", + "is_template": false, + "attributes": [ + { + "data_type": "double", + "names": [ + "u_ref" + ], + "description": "reference voltage" + }, + { + "data_type": "double", + "names": [ + "q_min", + "q_max" + ], + "description": "reactive power limits" + } + ] + }, { "name": "GenericCurrentSensorInput", "base": "SensorInput", @@ -557,7 +579,7 @@ { "data_type": "double", "names": [ - "i_sigma", + "i_sigma", "i_angle_sigma" ], "description": "sigma of error margin of current (angle) measurement" diff --git a/code_generation/data/attribute_classes/output.json b/code_generation/data/attribute_classes/output.json index 0c5d6280e1..01158195f3 100644 --- a/code_generation/data/attribute_classes/output.json +++ b/code_generation/data/attribute_classes/output.json @@ -303,6 +303,25 @@ } ] }, + { + "name": "VoltageRegulatorOutput", + "base": "BaseOutput", + "is_template": true, + "attributes": [ + { + "data_type": "IntS", + "names": "limit_violated", + "description": "indicates whether voltage limits are violated" + }, + { + "data_type": "RealValue", + "names": [ + "q" + ], + "description": "additional reactive power injected by the voltage regulator" + } + ] + }, { "name": "RegulatorShortCircuitOutput", "base": "BaseOutput", diff --git a/code_generation/data/attribute_classes/update.json b/code_generation/data/attribute_classes/update.json index db87d7bd16..fadf101732 100644 --- a/code_generation/data/attribute_classes/update.json +++ b/code_generation/data/attribute_classes/update.json @@ -244,6 +244,25 @@ } ] }, + { + "name": "VoltageRegulatorUpdate", + "base": "RegulatorUpdate", + "attributes": [ + { + "data_type": "double", + "names": "u_ref", + "description": "reference voltage" + }, + { + "data_type": "double", + "names": [ + "q_min", + "q_max" + ], + "description": "reactive power limits" + } + ] + }, { "name": "CurrentSensorUpdate", "base": "BaseUpdate", @@ -252,7 +271,7 @@ { "data_type": "double", "names": [ - "i_sigma", + "i_sigma", "i_angle_sigma" ], "description": "sigma of error margin of current (angle) measurement" diff --git a/code_generation/data/dataset_class_maps/dataset_definitions.json b/code_generation/data/dataset_class_maps/dataset_definitions.json index ea6b2e894e..574b2d4764 100644 --- a/code_generation/data/dataset_class_maps/dataset_definitions.json +++ b/code_generation/data/dataset_class_maps/dataset_definitions.json @@ -40,6 +40,10 @@ "names": ["sym_load", "sym_gen", "asym_load", "asym_gen"], "class_name": "LoadGenInput" }, + { + "names": ["voltage_regulator"], + "class_name": "VoltageRegulatorInput" + }, { "names": ["shunt"], "class_name": "ShuntInput" @@ -90,6 +94,10 @@ "names": ["sym_load", "sym_gen", "asym_load", "asym_gen", "shunt", "source"], "class_name": "ApplianceOutput" }, + { + "names": ["voltage_regulator"], + "class_name": "VoltageRegulatorOutput" + }, { "names": ["sym_voltage_sensor", "asym_voltage_sensor"], "class_name": "VoltageSensorOutput" @@ -144,6 +152,10 @@ "names": ["sym_load", "sym_gen", "asym_load", "asym_gen"], "class_name": "LoadGenUpdate" }, + { + "names": ["voltage_regulator"], + "class_name": "VoltageRegulatorUpdate" + }, { "names": ["shunt"], "class_name": "ApplianceUpdate" diff --git a/docs/user_manual/components.md b/docs/user_manual/components.md index 79cd04d955..f17cfec941 100644 --- a/docs/user_manual/components.md +++ b/docs/user_manual/components.md @@ -62,7 +62,7 @@ Physically a node can be a busbar, a joint, or other similar component. | `q` | `RealValueOutput` | volt-ampere-reactive (var) | reactive power injection | ```{note} -The `p` and `q` output of injection follows the `generator` reference direction as mentioned in +The `p` and `q` output of injection follows the `generator` reference direction as mentioned in {hoverxreftooltip}`user_manual/data-model:Reference Direction` ``` @@ -256,7 +256,7 @@ $$ where $z_{\text{base,transformer}} = 1 / y_{\text{base,transformer}} = {u_{\text{2}}}^2 / s_{\text{n}}$. -### Generic Branch +### Generic Branch * type name: `generic_branch` @@ -992,7 +992,7 @@ per method on how the variances are taken into account for both the global and l individual phases. ```{note} -The combination of `i_measured=0` and `i_angle_measured=nπ/2` renders the current sensor invalid for PGM. +The combination of `i_measured=0` and `i_angle_measured=nπ/2` renders the current sensor invalid for PGM. See [State estimate sensor transformations](calculations.md#state-estimate-sensor-transformations). ``` @@ -1245,3 +1245,87 @@ node_1 --- transformer_4 --- node_2 --- line_5 --- node_3 source_6 | load_7 transformer_tap_regulator_8 ``` + +### Voltage Regulator + +* type name: `voltage_regulator` +* base: {hoverxreftooltip}`user_manual/components:regulator` + +`voltage_regulator` defines a regulator for voltage-controlled generators in the grid. +A voltage regulator adjusts the reactive power output of a generator to maintain the voltage at its connection node +at a specified setpoint. + +The voltage regulator changes the reactive power output of the generator it regulates to achieve the reference voltage +`u_ref` at the generator's node. +If `q_min` and `q_max` are provided, the reactive power is constrained within this range (i.e., `q_min <= q <= q_max` or +`q_min >= q >= q_max`). +If these limits are not provided, the reactive power can take any value needed to maintain the voltage setpoint. + +```{warning} +Voltage regulation is only supported by the [Newton-Raphson](#newton-raphson-power-flow) method as an experimental +feature. +``` + +```{note} +The `regulated_object` must reference a generator (`sym_gen` or `asym_gen`) or a load (`sym_load` or `asym_load`). +Each generator or load can have at most one voltage regulator. +When multiple voltage-regulated generators are connected to the same node, they should all specify the same `u_ref` +value to avoid conflicting voltage setpoints. +``` + +```{warning} +Reactive power limit checking is not yet fully implemented. When `q_min` and `q_max` are specified, +the intended behavior is that if the required reactive power to maintain `u_ref` exceeds these limits, +the voltage regulator should operate at the limit and the voltage may deviate from `u_ref`. +``` + +#### Input + +| name | data type | unit | description | required | update | valid values | +| -------- | --------- | -------------------------- | --------------------------------------------------- | :--------------------------: | :------: | :----------: | +| `u_ref` | `double` | - | reference voltage in per-unit at the generator node | ✨ only for power flow | ✔ | `> 0` | +| `q_min` | `double` | volt-ampere-reactive (var) | minimum reactive power limit of the generator | ❌ | ✔ | | +| `q_max` | `double` | volt-ampere-reactive (var) | maximum reactive power limit of the generator | ❌ | ✔ | | + +#### Steady state output + +| name | data type | unit | description | +| ----------------- | ----------------- | -------------------------- | -------------------------------------------------------------------- | +| `q` | `RealValueOutput` | volt-ampere-reactive (var) | reactive power provided by the voltage regulator | +| `limit_violated` | `int8_t` | - | reactive power limit violation indicator (not yet fully implemented) | + +#### Short circuit output + +A `voltage_regulator` has no short circuit output. + +#### Electric Model + +The voltage regulator controls the generator to behave as a **PV node** in power flow calculations: + +$$ + \begin{eqnarray} + & P_{\text{gen}} = P_{\text{specified}} \\ + & |U_{\text{node}}| = U_{\text{ref}} \\ + & Q_{\text{gen}} = \text{calculated to satisfy } U_{\text{ref}} + \end{eqnarray} +$$ + +When `q_min` and `q_max` are provided, the reactive power should be constrained: + +$$ + Q_{\text{min}} \leq Q_{\text{gen}} \leq Q_{\text{max}} +$$ + +When fully implemented, if the reactive power constraints are violated, the generator will operate at the limit and the +node becomes a PQ node: + +$$ + \begin{eqnarray} + & P_{\text{gen}} = P_{\text{specified}} \\ + & Q_{\text{gen}} = Q_{\text{min}} \text{ or } Q_{\text{max}} \\ + & |U_{\text{node}}| = \text{calculated from power flow} + \end{eqnarray} +$$ + +In this case, `limit_violated` will indicate which limit was exceeded, and the actual voltage at the node may differ +from `u_ref`. diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/all_components.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/all_components.hpp index c275535b54..63178043a1 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/all_components.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/all_components.hpp @@ -24,6 +24,7 @@ #include "component/three_winding_transformer.hpp" #include "component/transformer.hpp" #include "component/transformer_tap_regulator.hpp" +#include "component/voltage_regulator.hpp" #include "component/voltage_sensor.hpp" namespace power_grid_model { @@ -31,7 +32,7 @@ namespace power_grid_model { using AllComponents = ComponentList; + AsymVoltageSensor, SymCurrentSensor, AsymCurrentSensor, Fault, TransformerTapRegulator, VoltageRegulator>; using AllExtraRetrievableTypes = ExtraRetrievableTypes(*this); } }; +struct VoltageRegulatorInput { + ID id{na_IntID}; // ID of the object + ID regulated_object{na_IntID}; // ID of the regulated object + IntS status{na_IntS}; // regulator enabled + double u_ref{nan}; // reference voltage + double q_min{nan}; // reactive power limits + double q_max{nan}; // reactive power limits + + // implicit conversions to BaseInput + operator BaseInput&() { return reinterpret_cast(*this); } + operator BaseInput const&() const { return reinterpret_cast(*this); } + + // implicit conversions to RegulatorInput + operator RegulatorInput&() { return reinterpret_cast(*this); } + operator RegulatorInput const&() const { return reinterpret_cast(*this); } +}; + struct GenericCurrentSensorInput { ID id{na_IntID}; // ID of the object ID measured_object{na_IntID}; // ID of the measured object diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/auxiliary/meta_gen/input.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/auxiliary/meta_gen/input.hpp index a5fb7aef33..7c72b8208f 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/auxiliary/meta_gen/input.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/auxiliary/meta_gen/input.hpp @@ -440,6 +440,20 @@ struct get_attributes_list { }; }; +template<> +struct get_attributes_list { + static constexpr std::array value{ + // all attributes including base class + + meta_data_gen::get_meta_attribute<&VoltageRegulatorInput::id>(offsetof(VoltageRegulatorInput, id), "id"), + meta_data_gen::get_meta_attribute<&VoltageRegulatorInput::regulated_object>(offsetof(VoltageRegulatorInput, regulated_object), "regulated_object"), + meta_data_gen::get_meta_attribute<&VoltageRegulatorInput::status>(offsetof(VoltageRegulatorInput, status), "status"), + meta_data_gen::get_meta_attribute<&VoltageRegulatorInput::u_ref>(offsetof(VoltageRegulatorInput, u_ref), "u_ref"), + meta_data_gen::get_meta_attribute<&VoltageRegulatorInput::q_min>(offsetof(VoltageRegulatorInput, q_min), "q_min"), + meta_data_gen::get_meta_attribute<&VoltageRegulatorInput::q_max>(offsetof(VoltageRegulatorInput, q_max), "q_max"), + }; +}; + template<> struct get_attributes_list { static constexpr std::array value{ diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/auxiliary/meta_gen/output.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/auxiliary/meta_gen/output.hpp index 771d94303f..ea7c5269b6 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/auxiliary/meta_gen/output.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/auxiliary/meta_gen/output.hpp @@ -240,6 +240,20 @@ struct get_attributes_list { }; }; +template +struct get_attributes_list> { + using sym = sym_type; + + static constexpr std::array value{ + // all attributes including base class + + meta_data_gen::get_meta_attribute<&VoltageRegulatorOutput::id>(offsetof(VoltageRegulatorOutput, id), "id"), + meta_data_gen::get_meta_attribute<&VoltageRegulatorOutput::energized>(offsetof(VoltageRegulatorOutput, energized), "energized"), + meta_data_gen::get_meta_attribute<&VoltageRegulatorOutput::limit_violated>(offsetof(VoltageRegulatorOutput, limit_violated), "limit_violated"), + meta_data_gen::get_meta_attribute<&VoltageRegulatorOutput::q>(offsetof(VoltageRegulatorOutput, q), "q"), + }; +}; + template<> struct get_attributes_list { static constexpr std::array value{ diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/auxiliary/meta_gen/update.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/auxiliary/meta_gen/update.hpp index 0425c7468b..e93739edf4 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/auxiliary/meta_gen/update.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/auxiliary/meta_gen/update.hpp @@ -197,6 +197,19 @@ struct get_attributes_list { }; }; +template<> +struct get_attributes_list { + static constexpr std::array value{ + // all attributes including base class + + meta_data_gen::get_meta_attribute<&VoltageRegulatorUpdate::id>(offsetof(VoltageRegulatorUpdate, id), "id"), + meta_data_gen::get_meta_attribute<&VoltageRegulatorUpdate::status>(offsetof(VoltageRegulatorUpdate, status), "status"), + meta_data_gen::get_meta_attribute<&VoltageRegulatorUpdate::u_ref>(offsetof(VoltageRegulatorUpdate, u_ref), "u_ref"), + meta_data_gen::get_meta_attribute<&VoltageRegulatorUpdate::q_min>(offsetof(VoltageRegulatorUpdate, q_min), "q_min"), + meta_data_gen::get_meta_attribute<&VoltageRegulatorUpdate::q_max>(offsetof(VoltageRegulatorUpdate, q_max), "q_max"), + }; +}; + template struct get_attributes_list> { using sym = sym_type; diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/auxiliary/output.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/auxiliary/output.hpp index c5ad332db6..95c94f01b0 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/auxiliary/output.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/auxiliary/output.hpp @@ -239,6 +239,23 @@ struct TransformerTapRegulatorOutput { operator BaseOutput const&() const { return reinterpret_cast(*this); } }; +template +struct VoltageRegulatorOutput { + using sym = sym_type; + + ID id{na_IntID}; // ID of the object + IntS energized{na_IntS}; // whether the object is energized + IntS limit_violated{na_IntS}; // indicates whether voltage limits are violated + RealValue q{nan}; // additional reactive power injected by the voltage regulator + + // implicit conversions to BaseOutput + operator BaseOutput&() { return reinterpret_cast(*this); } + operator BaseOutput const&() const { return reinterpret_cast(*this); } +}; + +using SymVoltageRegulatorOutput = VoltageRegulatorOutput; +using AsymVoltageRegulatorOutput = VoltageRegulatorOutput; + struct RegulatorShortCircuitOutput { ID id{na_IntID}; // ID of the object IntS energized{na_IntS}; // whether the object is energized diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/auxiliary/static_asserts/input.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/auxiliary/static_asserts/input.hpp index 687894e1d5..c625bc59d6 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/auxiliary/static_asserts/input.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/auxiliary/static_asserts/input.hpp @@ -536,6 +536,21 @@ static_assert(offsetof(TransformerTapRegulatorInput, id) == offsetof(RegulatorIn static_assert(offsetof(TransformerTapRegulatorInput, regulated_object) == offsetof(RegulatorInput, regulated_object)); static_assert(offsetof(TransformerTapRegulatorInput, status) == offsetof(RegulatorInput, status)); +// static asserts for VoltageRegulatorInput +static_assert(std::is_standard_layout_v); +// static asserts for conversion of VoltageRegulatorInput to BaseInput +static_assert(std::alignment_of_v >= std::alignment_of_v); +static_assert(std::same_as); +static_assert(offsetof(VoltageRegulatorInput, id) == offsetof(BaseInput, id)); +// static asserts for conversion of VoltageRegulatorInput to RegulatorInput +static_assert(std::alignment_of_v >= std::alignment_of_v); +static_assert(std::same_as); +static_assert(std::same_as); +static_assert(std::same_as); +static_assert(offsetof(VoltageRegulatorInput, id) == offsetof(RegulatorInput, id)); +static_assert(offsetof(VoltageRegulatorInput, regulated_object) == offsetof(RegulatorInput, regulated_object)); +static_assert(offsetof(VoltageRegulatorInput, status) == offsetof(RegulatorInput, status)); + // static asserts for GenericCurrentSensorInput static_assert(std::is_standard_layout_v); // static asserts for conversion of GenericCurrentSensorInput to BaseInput diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/auxiliary/static_asserts/output.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/auxiliary/static_asserts/output.hpp index 812ca4faa9..3cbb3c5f7d 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/auxiliary/static_asserts/output.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/auxiliary/static_asserts/output.hpp @@ -286,6 +286,39 @@ static_assert(std::same_as +static_assert(std::is_standard_layout_v>); +// static asserts for conversion of VoltageRegulatorOutput to BaseOutput +static_assert(std::alignment_of_v> >= std::alignment_of_v); +static_assert(std::same_as::id), decltype(BaseOutput::id)>); +static_assert(std::same_as::energized), decltype(BaseOutput::energized)>); +static_assert(offsetof(VoltageRegulatorOutput, id) == offsetof(BaseOutput, id)); +static_assert(offsetof(VoltageRegulatorOutput, energized) == offsetof(BaseOutput, energized)); +// static asserts for VoltageRegulatorOutput +static_assert(std::is_standard_layout_v>); +// static asserts for conversion of VoltageRegulatorOutput to BaseOutput +static_assert(std::alignment_of_v> >= std::alignment_of_v); +static_assert(std::same_as::id), decltype(BaseOutput::id)>); +static_assert(std::same_as::energized), decltype(BaseOutput::energized)>); +static_assert(offsetof(VoltageRegulatorOutput, id) == offsetof(BaseOutput, id)); +static_assert(offsetof(VoltageRegulatorOutput, energized) == offsetof(BaseOutput, energized)); +// static asserts for SymVoltageRegulatorOutput +static_assert(std::is_standard_layout_v); +// static asserts for conversion of SymVoltageRegulatorOutput to BaseOutput +static_assert(std::alignment_of_v >= std::alignment_of_v); +static_assert(std::same_as); +static_assert(std::same_as); +static_assert(offsetof(SymVoltageRegulatorOutput, id) == offsetof(BaseOutput, id)); +static_assert(offsetof(SymVoltageRegulatorOutput, energized) == offsetof(BaseOutput, energized)); +// static asserts for AsymVoltageRegulatorOutput +static_assert(std::is_standard_layout_v); +// static asserts for conversion of AsymVoltageRegulatorOutput to BaseOutput +static_assert(std::alignment_of_v >= std::alignment_of_v); +static_assert(std::same_as); +static_assert(std::same_as); +static_assert(offsetof(AsymVoltageRegulatorOutput, id) == offsetof(BaseOutput, id)); +static_assert(offsetof(AsymVoltageRegulatorOutput, energized) == offsetof(BaseOutput, energized)); + // static asserts for RegulatorShortCircuitOutput static_assert(std::is_standard_layout_v); // static asserts for conversion of RegulatorShortCircuitOutput to BaseOutput diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/auxiliary/static_asserts/update.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/auxiliary/static_asserts/update.hpp index 75d2878566..f7a7a65bed 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/auxiliary/static_asserts/update.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/auxiliary/static_asserts/update.hpp @@ -221,6 +221,19 @@ static_assert(std::same_as); +// static asserts for conversion of VoltageRegulatorUpdate to BaseUpdate +static_assert(std::alignment_of_v >= std::alignment_of_v); +static_assert(std::same_as); +static_assert(offsetof(VoltageRegulatorUpdate, id) == offsetof(BaseUpdate, id)); +// static asserts for conversion of VoltageRegulatorUpdate to RegulatorUpdate +static_assert(std::alignment_of_v >= std::alignment_of_v); +static_assert(std::same_as); +static_assert(std::same_as); +static_assert(offsetof(VoltageRegulatorUpdate, id) == offsetof(RegulatorUpdate, id)); +static_assert(offsetof(VoltageRegulatorUpdate, status) == offsetof(RegulatorUpdate, status)); + // static asserts for CurrentSensorUpdate static_assert(std::is_standard_layout_v>); // static asserts for conversion of CurrentSensorUpdate to BaseUpdate diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/auxiliary/update.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/auxiliary/update.hpp index 8ec5e2f02e..c1314481de 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/auxiliary/update.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/auxiliary/update.hpp @@ -209,6 +209,22 @@ struct TransformerTapRegulatorUpdate { operator RegulatorUpdate const&() const { return reinterpret_cast(*this); } }; +struct VoltageRegulatorUpdate { + ID id{na_IntID}; // ID of the object + IntS status{na_IntS}; // regulator enables + double u_ref{nan}; // reference voltage + double q_min{nan}; // reactive power limits + double q_max{nan}; // reactive power limits + + // implicit conversions to BaseUpdate + operator BaseUpdate&() { return reinterpret_cast(*this); } + operator BaseUpdate const&() const { return reinterpret_cast(*this); } + + // implicit conversions to RegulatorUpdate + operator RegulatorUpdate&() { return reinterpret_cast(*this); } + operator RegulatorUpdate const&() const { return reinterpret_cast(*this); } +}; + template struct CurrentSensorUpdate { using sym = sym_type; diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/calculation_parameters.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/calculation_parameters.hpp index 82b553adce..d392fbc763 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/calculation_parameters.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/calculation_parameters.hpp @@ -79,6 +79,17 @@ template struct ApplianceShortCircuitSolverOutput { ComplexValue i{}; }; +template struct VoltageRegulatorSolverOutput { + using sym = sym_type; + + IntS limit_violated{}; + RealValue q{}; + + // provide generator info, as the regulator component has no other access to it + ID generator_id{}; + IntS generator_status{}; +}; + // voltage sensor calculation parameters for state estimation // The value is the complex voltage // If the imaginary part is NaN, it means the angle calculation is not correct @@ -157,6 +168,7 @@ struct MathModelTopology { DenseGroupedIdxVector current_sensors_per_branch_from; DenseGroupedIdxVector current_sensors_per_branch_to; DenseGroupedIdxVector tap_regulators_per_branch; + DenseGroupedIdxVector voltage_regulators_per_load_gen; Idx n_bus() const { return static_cast(phase_shift.size()); } @@ -187,6 +199,8 @@ struct MathModelTopology { Idx n_branch_to_current_sensor() const { return current_sensors_per_branch_to.element_size(); } Idx n_transformer_tap_regulator() const { return tap_regulators_per_branch.element_size(); } + + Idx n_load_gen_voltage_regulator() const { return voltage_regulators_per_load_gen.element_size(); } }; struct SourceCalcParam { @@ -202,6 +216,18 @@ struct SourceCalcParam { } }; +template struct VoltageRegulatorCalcParam { + using sym = sym_type; + + IntS status{}; + DoubleComplex u_ref; + RealValue q_min{}; + RealValue q_max{}; + + // add generator id for later lookup + ID generator_id{}; +}; + template struct MathModelParam { using sym = sym_type; @@ -220,6 +246,8 @@ template struct PowerFlowInput { ComplexVector source; // Complex u_ref of each source ComplexValueVector s_injection; // Specified injection power of each load_gen + std::vector> voltage_regulator; + IntSVector load_gen_status; }; template struct StateEstimationInput { @@ -291,6 +319,7 @@ template struct SolverOutput { std::vector> source; std::vector> shunt; std::vector> load_gen; + std::vector> voltage_regulator; }; template struct ShortCircuitSolverOutput { @@ -435,6 +464,7 @@ struct TopologicalComponentToMathCoupling { std::vector power_sensor; // can be coupled to branch-from/to, source, load_gen, or shunt sensor std::vector current_sensor; // can be coupled to branch-from/to std::vector regulator; + std::vector voltage_regulator; }; } // namespace power_grid_model diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/common/enum.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/common/enum.hpp index 391dd28987..b8bf7acf07 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/common/enum.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/common/enum.hpp @@ -8,6 +8,10 @@ namespace power_grid_model { +enum class BusType : IntS { + pq = 0, pv = 1, slack = 2 +}; + enum class LoadGenType : IntS { const_pq = 0, // constant power const_y = 1, // constant element_admittance (impedance) @@ -63,6 +67,7 @@ enum class ComponentType : IntS { fault = 10, regulator = 11, transformer_tap_regulator = 12, + voltage_regulator = 13, test = -128 // any stub or mock may use this. do not use this in production }; diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/component/appliance.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/component/appliance.hpp index 6f3cedce88..40c5ff89f4 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/component/appliance.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/component/appliance.hpp @@ -109,6 +109,8 @@ class Appliance : public Base { return get_sc_output(appliance_solver_output.i); } + virtual double injection_direction() const = 0; + private: ID node_; bool status_; @@ -117,8 +119,6 @@ class Appliance : public Base { // pure virtual functions for translate from u to s/i virtual ApplianceSolverOutput sym_u2si(ComplexValue const& u) const = 0; virtual ApplianceSolverOutput asym_u2si(ComplexValue const& u) const = 0; - - virtual double injection_direction() const = 0; }; } // namespace power_grid_model diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/component/voltage_regulator.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/component/voltage_regulator.hpp new file mode 100644 index 0000000000..915e52be84 --- /dev/null +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/component/voltage_regulator.hpp @@ -0,0 +1,113 @@ +// SPDX-FileCopyrightText: Contributors to the Power Grid Model project +// +// SPDX-License-Identifier: MPL-2.0 + +#pragma once + +#include "base.hpp" +#include "regulator.hpp" + +#include "../auxiliary/input.hpp" +#include "../auxiliary/output.hpp" +#include "../auxiliary/update.hpp" +#include "../calculation_parameters.hpp" +#include "../common/common.hpp" +#include + +namespace power_grid_model { + +class VoltageRegulator : public Regulator { + public: + using InputType = VoltageRegulatorInput; + using UpdateType = VoltageRegulatorUpdate; + template using OutputType = VoltageRegulatorOutput; + + static constexpr char const* name = "voltage_regulator"; + explicit VoltageRegulator(VoltageRegulatorInput const& voltage_regulator_input, + ComponentType regulated_object_type, + double injection_direction) + : Regulator{voltage_regulator_input, regulated_object_type}, + injection_direction_{injection_direction}, + u_ref_{voltage_regulator_input.u_ref}, + q_min_{voltage_regulator_input.q_min}, + q_max_{voltage_regulator_input.q_max} {} + + UpdateChange update(VoltageRegulatorUpdate const& update_data) { + assert(update_data.id == this->id() || is_nan(update_data.id)); + set_status(update_data.status); + set_u_ref(update_data.u_ref); + set_q_limits(update_data.q_min, update_data.q_max); + return {.topo = false, .param = false}; + } + + VoltageRegulatorUpdate inverse(VoltageRegulatorUpdate update_data) const { + assert(update_data.id == this->id() || is_nan(update_data.id)); + + update_data = Regulator::inverse(update_data); + set_if_not_nan(update_data.u_ref, u_ref_); + set_if_not_nan(update_data.q_min, q_min_); + set_if_not_nan(update_data.q_max, q_max_); + return update_data; + } + + constexpr RegulatorShortCircuitOutput get_null_sc_output() const { return {.id = id(), .energized = 0}; } + + template + constexpr VoltageRegulatorOutput get_null_output() const { + return {.id = id(), .energized = 0, .limit_violated = 0, .q = RealValue{0}}; + } + + bool is_energized(bool is_connected_to_source = true) const { + return is_connected_to_source && status(); + } + + template + VoltageRegulatorOutput get_output(VoltageRegulatorSolverOutput const& solver_output) const { + VoltageRegulatorOutput output{}; + static_cast(output) = base_output(is_energized(true) && solver_output.generator_status != 0); + output.limit_violated = solver_output.limit_violated; + output.q = base_power * solver_output.q * injection_direction(); + return output; + } + + template + VoltageRegulatorCalcParam calc_param() const { + return VoltageRegulatorCalcParam{ + .status = static_cast(status()), + .u_ref = {u_ref_, 0.0}, + .q_min = RealValue{q_min_ / base_power_3p}, + .q_max = RealValue{q_max_ / base_power_3p}, + .generator_id = this->regulated_object() + }; + } + + // setter + void set_u_ref(double new_u_ref) { + if (!is_nan(new_u_ref)) { + u_ref_ = new_u_ref; + } + } + + void set_q_limits(double new_q_min, double new_q_max) { + if (!is_nan(new_q_min)) { + q_min_ = new_q_min; + } + if (!is_nan(new_q_max)) { + q_max_ = new_q_max; + } + } + + // getter + constexpr double injection_direction() const { return injection_direction_; } + constexpr double u_ref() const { return u_ref_; } + constexpr double q_min() const { return q_min_; } + constexpr double q_max() const { return q_max_; } + + private: + double injection_direction_; + double u_ref_; + double q_min_; + double q_max_; +}; + +} // namespace power_grid_model diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/main_core/calculation_input_preparation.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/main_core/calculation_input_preparation.hpp index 4e6edd49da..95fd4a5ee3 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/main_core/calculation_input_preparation.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/main_core/calculation_input_preparation.hpp @@ -122,9 +122,10 @@ void prepare_input(main_model_state_c auto const& state, std::vector cons } } -template ::*component), class Component> +template + requires std::same_as> || std::same_as> void prepare_input_status(main_model_state_c auto const& state, std::vector const& objects, - std::vector>& input) { + std::vector& input) { for (Idx i = 0, n = narrow_cast(objects.size()); i != n; ++i) { Idx2D const math_idx = objects[i]; if (math_idx.group == isolated_component) { @@ -139,11 +140,14 @@ void prepare_input_status(main_model_state_c auto const& state, std::vector std::vector> prepare_power_flow_input(main_model_state_c auto const& state, Idx n_math_solvers) { using detail::prepare_input; + using detail::prepare_input_status; std::vector> pf_input(n_math_solvers); for (Idx i = 0; i != n_math_solvers; ++i) { pf_input[i].s_injection.resize(state.math_topology[i]->n_load_gen()); pf_input[i].source.resize(state.math_topology[i]->n_source()); + pf_input[i].voltage_regulator.resize(state.math_topology[i]->n_load_gen_voltage_regulator()); + pf_input[i].load_gen_status.resize(state.math_topology[i]->n_load_gen()); } prepare_input, DoubleComplex, &PowerFlowInput::source, Source>( state, state.topo_comp_coup->source, pf_input); @@ -151,6 +155,12 @@ std::vector> prepare_power_flow_input(main_model_state_c aut prepare_input, ComplexValue, &PowerFlowInput::s_injection, GenericLoadGen>( state, state.topo_comp_coup->load_gen, pf_input); + prepare_input, VoltageRegulatorCalcParam, &PowerFlowInput::voltage_regulator, VoltageRegulator>( + state, state.topo_comp_coup->voltage_regulator, pf_input); + + prepare_input_status, &PowerFlowInput::load_gen_status, GenericLoadGen>( + state, state.topo_comp_coup->load_gen, pf_input); + return pf_input; } @@ -177,12 +187,12 @@ std::vector> prepare_state_estimation_input(main_model se_input[i].measured_branch_to_current.resize(state.math_topology[i]->n_branch_to_current_sensor()); } - prepare_input_status::shunt_status, Shunt>(state, state.topo_comp_coup->shunt, - se_input); - prepare_input_status::load_gen_status, GenericLoadGen>( + prepare_input_status, &StateEstimationInput::shunt_status, Shunt>( + state, state.topo_comp_coup->shunt, se_input); + prepare_input_status, &StateEstimationInput::load_gen_status, GenericLoadGen>( state, state.topo_comp_coup->load_gen, se_input); - prepare_input_status::source_status, Source>(state, state.topo_comp_coup->source, - se_input); + prepare_input_status, &StateEstimationInput::source_status, Source>( + state, state.topo_comp_coup->source, se_input); prepare_input, VoltageSensorCalcParam, &StateEstimationInput::measured_voltage, GenericVoltageSensor>(state, state.topo_comp_coup->voltage_sensor, se_input); diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/main_core/input.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/main_core/input.hpp index 53cee3abe0..5cc999edfa 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/main_core/input.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/main_core/input.hpp @@ -184,6 +184,24 @@ inline void add_component(ComponentContainer& components, Inputs&& component_inp double const u_rated = get_component(components, regulated_terminal).u_rated(); emplace_component(components, id, input, regulated_object_type, u_rated); + } else if constexpr (std::derived_from) { + Idx2D const regulated_object_idx = + main_core::get_component_idx_by_id(components, input.regulated_object); + regulated_objects.push_back(regulated_object_idx); + + // regulate generators + // also allow loads, in order to have more flexibility when converting existing models + if (regulated_object_idx.group != get_component_type_index(components) && + regulated_object_idx.group != get_component_type_index(components) && + regulated_object_idx.group != get_component_type_index(components) && + regulated_object_idx.group != get_component_type_index(components)) { + throw InvalidRegulatedObject(input.regulated_object, Component::name); + } + + auto const& regulated_object = get_component(components, regulated_object_idx); + auto const regulated_object_type = regulated_object.math_model_type(); + auto const injection_direction = regulated_object.injection_direction(); + emplace_component(components, id, input, regulated_object_type, injection_direction); } } // Make sure that each regulated object has at most one regulator diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/main_core/main_model_type.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/main_core/main_model_type.hpp index 7aaaec2f25..9385eea018 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/main_core/main_model_type.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/main_core/main_model_type.hpp @@ -61,8 +61,8 @@ concept validate_component_types_c = dependent_type_check && // dependent_type_check && // - dependent_type_check; - + dependent_type_check && // + dependent_type_check; } // namespace detail template class MainModelType; diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/main_core/output.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/main_core/output.hpp index 330da5bebb..2f0c6eb354 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/main_core/output.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/main_core/output.hpp @@ -83,7 +83,7 @@ constexpr auto comp_base_sequence_cbegin(MainModelState cons return state.comp_coup.fault.cbegin(); } -template Component, class ComponentContainer> +template Component, class ComponentContainer> requires model_component_state_c constexpr auto comp_base_sequence_cbegin(MainModelState const& state) { return state.comp_topo->regulated_object_idx.cbegin() + @@ -451,6 +451,39 @@ output_result(Component const& transformer_tap_regulator, MainModelState Component, class ComponentContainer, + steady_state_solver_output_type SolverOutputType> + requires model_component_state_c +constexpr auto output_result(Component const& voltage_regulator, + MainModelState const& state, + MathOutput> const& math_output, + Idx const obj_seq) { + using sym = typename SolverOutputType::sym; + + Idx2D const load_gen_math_id = [&]() { + return state.topo_comp_coup->load_gen[obj_seq]; + }(); + if (load_gen_math_id.group != -1) { + // is voltage regulator always in same group as the generator it regulates? + for (auto const& vr_output: math_output.solver_output[load_gen_math_id.group].voltage_regulator) { + if (vr_output.generator_id == voltage_regulator.regulated_object()) { + return voltage_regulator.template get_output(vr_output); + } + } + } + return voltage_regulator.template get_null_output(); +} + +template Component, class ComponentContainer, + short_circuit_solver_output_type SolverOutputType> + requires model_component_state_c +constexpr auto +output_result(Component const& voltage_regulator, MainModelState const& /* state */, + MathOutput> const& /* math_output */, Idx const /* obj_seq */) { + return voltage_regulator.get_null_sc_output(); +} + // output base component template Component, class ComponentContainer, solver_output_type SolverOutputType, std::ranges::viewable_range ComponentOutput> diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/main_core/topology.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/main_core/topology.hpp index 6172babf2f..84bff621ea 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/main_core/topology.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/main_core/topology.hpp @@ -169,6 +169,8 @@ constexpr void register_topology_components(ComponentContainer const& components return get_component_sequence_idx(components, regulator.regulated_object()); case ComponentType::branch3: return get_component_sequence_idx(components, regulator.regulated_object()); + case ComponentType::generic_load_gen: + return get_component_sequence_idx(components, regulator.regulated_object()); default: throw MissingCaseForEnumError("Regulator idx to seq transformation", regulator.regulated_object_type()); } 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 d376683be1..05941f3729 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 @@ -486,7 +486,13 @@ class MainModelImpl { return *meta_data_; } - void check_no_experimental_features_used(Options const& /*options*/) const {} + void check_no_experimental_features_used(Options const& options) const { + if (options.calculation_type == CalculationType::power_flow && + options.calculation_method == CalculationMethod::newton_raphson && + state_.components.template size() > 0) { + throw ExperimentalFeature{"Voltage Regulator with Newton-Raphson Power Flow Method is an experimental feature."}; + } + } private: template diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/common_solver_functions.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/common_solver_functions.hpp index 5acaacf245..167ee5bb75 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/common_solver_functions.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/common_solver_functions.hpp @@ -10,6 +10,8 @@ #include "../calculation_parameters.hpp" #include "../common/exception.hpp" +#include + namespace power_grid_model::math_solver::detail { template @@ -141,6 +143,84 @@ inline void calculate_source_result(IdxRange const& sources, Idx const& bus_numb } } +template +inline void calculate_voltage_regulator_result(Idx const& bus_number, PowerFlowInput const& input, SolverOutput& output, + IdxRange const& load_gens, std::map const& loadgen_to_regulator) { + if (load_gens.empty()) { + return; + } + + ComplexValue const s_load_gen_bus = + std::transform_reduce(load_gens.begin(), load_gens.end(), ComplexValue{}, std::plus{}, + [&](Idx const load_gen) { return output.load_gen[load_gen].s; }); + + ComplexValue const s_remaining = output.bus_injection[bus_number] - s_load_gen_bus; + + auto regulating_gens = load_gens + | std::views::filter([&](Idx lg) { + return loadgen_to_regulator.contains(lg); + }) + | std::views::filter([&](Idx lg) { + auto const regulator = loadgen_to_regulator.at(lg); + return input.load_gen_status[lg] != 0 && + input.voltage_regulator[regulator].status != 0; + }); + double num_regulating_gens = 0.0; + for ([[maybe_unused]] auto const& _ : regulating_gens) { + ++num_regulating_gens; + } + if (num_regulating_gens == 0.0) { + return; + } + + auto const& q_remaining = imag(s_remaining); + for (Idx const load_gen : load_gens) { + if (!loadgen_to_regulator.contains(load_gen)) { + continue; + } + auto const regulator = loadgen_to_regulator.at(load_gen); + + auto const& input_regulator = input.voltage_regulator[regulator]; + auto& output_regulator = output.voltage_regulator[regulator]; + output_regulator.generator_id = input_regulator.generator_id; + output_regulator.generator_status = input.load_gen_status[load_gen]; + output_regulator.limit_violated = 0; + output_regulator.q = RealValue{0}; + + if(input.load_gen_status[load_gen] == 0 || input.voltage_regulator[regulator].status == 0) { + continue; + } + + // TODO(scud-soptim): #185 consider Q Limits (PV to PQ conversion) + // TODO(scud-soptim): #185 equal distribution for now, later consider proportional distribution based on Q limits + if constexpr (is_symmetric_v) { + auto const q_regulator = q_remaining / num_regulating_gens; + output_regulator.q = q_regulator; + output_regulator.limit_violated = 0; // no violation + + auto const& s_load_gen = output.load_gen[load_gen].s; + output.load_gen[load_gen].s = ComplexValue{real(s_load_gen), imag(s_load_gen) + q_regulator}; + } else { + output.voltage_regulator[regulator].q = RealValue{ + q_remaining[0] / num_regulating_gens, + q_remaining[1] / num_regulating_gens, + q_remaining[2] / num_regulating_gens + }; + output.voltage_regulator[regulator].limit_violated = 0; // no violation + + auto const& s_load_gen = output.load_gen[load_gen].s; + auto const& p_load_gen = real(s_load_gen); + auto const& q_load_gen = imag(s_load_gen); + output.load_gen[load_gen].s = ComplexValue{ + {p_load_gen[0], q_load_gen[0] + q_remaining[0] / num_regulating_gens}, + {p_load_gen[1], q_load_gen[1] + q_remaining[1] / num_regulating_gens}, + {p_load_gen[2], q_load_gen[2] + q_remaining[2] / num_regulating_gens} + }; + } + output.load_gen[load_gen].i = conj(output.load_gen[load_gen].s / output.u[bus_number]); + } +} + template requires std::invocable, Idx> && std::same_as, LoadGenType> @@ -178,6 +258,8 @@ inline void calculate_pf_result(YBus const& y_bus, PowerFlowInput cons LoadGenFunc load_gen_func) { assert(sources_per_bus.size() == load_gens_per_bus.size()); + auto const& voltage_regulators_per_load_gen = y_bus.math_topology().voltage_regulators_per_load_gen; + // call y bus output.branch = y_bus.template calculate_branch_flow>(output.u); output.shunt = y_bus.template calculate_shunt_flow>(output.u); @@ -185,11 +267,21 @@ inline void calculate_pf_result(YBus const& y_bus, PowerFlowInput cons // prepare source, load gen and node injection output.source.resize(sources_per_bus.element_size()); output.load_gen.resize(load_gens_per_bus.element_size()); + output.voltage_regulator.resize(voltage_regulators_per_load_gen.element_size()); output.bus_injection.resize(sources_per_bus.size()); output.bus_injection = y_bus.calculate_injection(output.u); + + std::map loadgen_to_regulator; // save mapping from generator to its regulator + for (auto const& [load_gen, regulators] : enumerated_zip_sequence(voltage_regulators_per_load_gen)) { + for (Idx const regulator : regulators) { + loadgen_to_regulator.insert({load_gen, regulator}); + } + } + for (auto const& [bus_number, sources, load_gens] : enumerated_zip_sequence(sources_per_bus, load_gens_per_bus)) { calculate_load_gen_result(load_gens, bus_number, input, output, [&load_gen_func](Idx idx) { return load_gen_func(idx); }); + calculate_voltage_regulator_result(bus_number, input, output, load_gens, loadgen_to_regulator); calculate_source_result(sources, bus_number, y_bus, input, output, load_gens); } } diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/newton_raphson_pf_solver.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/newton_raphson_pf_solver.hpp index a0f8790c34..055c09d7a0 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/newton_raphson_pf_solver.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/newton_raphson_pf_solver.hpp @@ -155,6 +155,8 @@ J.L -= -dQ_cal_m/dV #include "../common/three_phase_tensor.hpp" #include "../common/timer.hpp" +#include + namespace power_grid_model::math_solver { // hide implementation in inside namespace @@ -217,7 +219,9 @@ class NewtonRaphsonPFSolver : public IterativePFSolvervoltage_regulators_per_load_gen} {} // Initilize the unknown variable in polar form void initialize_derived_solver(YBus const& y_bus, PowerFlowInput const& input, @@ -234,6 +238,8 @@ class NewtonRaphsonPFSolver : public IterativePFSolvern_bus_; ++i) { x_[i].v() = cabs(output.u[i]); @@ -296,6 +302,46 @@ class NewtonRaphsonPFSolver : public IterativePFSolver bus_types_; + std::shared_ptr voltage_regulators_per_load_gen_; + + void set_u_ref_and_bus_types(PowerFlowInput const& input, ComplexValueVector& u) { + std::map loadgen_to_regulator = prepare_mapping_of_active_regulators(input); + + // expect that one load/gen is only regulated by one regulator and if multiple loads/gens with regulators + // are connected to the same bus, they all have the same u_ref + for (auto const& [bus_number, load_gens, sources] + : enumerated_zip_sequence(*this->load_gens_per_bus_, *this->sources_per_bus_)) { + if (!sources.empty()) { + bus_types_[bus_number] = BusType::slack; + continue; + } + + for (auto const load_number : load_gens) { + if (loadgen_to_regulator.find(load_number) != loadgen_to_regulator.end()) { + Idx const regulator_number = loadgen_to_regulator[load_number]; + auto const& regulator = input.voltage_regulator[regulator_number]; + u[bus_number] = ComplexValue{regulator.u_ref}; + bus_types_[bus_number] = BusType::pv; + break; + } + } + } + } + + auto prepare_mapping_of_active_regulators(PowerFlowInput const& input) { + std::map loadgen_to_regulator{}; + for (auto const& [load_gen, regulators] : enumerated_zip_sequence(*this->voltage_regulators_per_load_gen_)) { + if (input.load_gen_status[load_gen] == 0) { continue; } + for(Idx const regulator_number : regulators) { + if (input.voltage_regulator[regulator_number].status != 0) { + loadgen_to_regulator.insert({load_gen, regulator_number}); + } + } + } + return loadgen_to_regulator; + } + /// @brief power_flow_ij = (ui @* conj(uj)) .* conj(yij) /// Hij = diag(Vi) * ( Gij .* sin(theta_ij) - Bij .* cos(theta_ij) ) * diag(Vj) /// = imaginary(power_flow_ij) @@ -357,6 +403,39 @@ class NewtonRaphsonPFSolver : public IterativePFSolvern_bus_; ++row) { + if (bus_types_[row] == BusType::pv) { + for (Idx k = indptr[row]; k != indptr[row + 1]; ++k) { + Idx const j = indices[k]; + // If there are pv generators on a bus with type ::slack, then these generators + // will be considered as ::pq and the jacobian row will not be modified. + // TODO: #185 or should the slack behave like a pv bus too? + data_jac_[k].m() = RealTensor{0.0}; + if (row == j) { + auto const& v = x_[j].v(); + if constexpr (is_symmetric_v) { + data_jac_[k].l() = v; + } else { + data_jac_[k].l() = RealTensor{{v[0], v[1], v[2]}}; + } + + } else { + data_jac_[k].l() = RealTensor{0.0}; + } + } + } + } } void add_loads(IdxRange const& load_gens, Idx bus_number, Idx diagonal_position, PowerFlowInput const& input, @@ -379,6 +458,11 @@ class NewtonRaphsonPFSolver : public IterativePFSolver const& input) { 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 367792666a..2a316acc4a 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 @@ -121,6 +121,7 @@ class Topology { dfs_search(); couple_branch(); couple_all_appliance(); + couple_voltage_regulators(); couple_sensors(); // create return pair with shared pointer std::pair>, @@ -164,6 +165,7 @@ class Topology { comp_coup_.voltage_sensor.resize(comp_topo_.voltage_sensor_node_idx.size(), unknown_idx2d); comp_coup_.power_sensor.resize(comp_topo_.power_sensor_object_idx.size(), unknown_idx2d); comp_coup_.current_sensor.resize(comp_topo_.current_sensor_object_idx.size(), unknown_idx2d); + comp_coup_.voltage_regulator.resize(comp_topo_.regulated_object_idx.size(), unknown_idx2d); } template static void for_all_vertices(GlobalGraph const& graph, F&& func) { @@ -567,6 +569,14 @@ class Topology { [this](Idx i) { return comp_conn_.source_connected[i]; }); } + void couple_voltage_regulators() { + couple_object_components<&MathModelTopology::n_load_gen>( + [](MathModelTopology& math_topo) -> auto& { return math_topo.voltage_regulators_per_load_gen; }, + {.component_obj_idx = comp_topo_.regulated_object_idx, .objects_coupling = comp_coup_.load_gen}, + comp_coup_.voltage_regulator, + [this](Idx i) { return comp_topo_.regulated_object_type[i] == ComponentType::generic_load_gen; }); + } + void couple_sensors() { // voltage sensors couple_object_components<&MathModelTopology::n_bus>( 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 22510bbe8e..1d530a6802 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 @@ -241,6 +241,15 @@ PGM_API extern PGM_MetaAttribute const* const PGM_def_input_asym_gen_status; PGM_API extern PGM_MetaAttribute const* const PGM_def_input_asym_gen_type; PGM_API extern PGM_MetaAttribute const* const PGM_def_input_asym_gen_p_specified; PGM_API extern PGM_MetaAttribute const* const PGM_def_input_asym_gen_q_specified; +// component voltage_regulator +PGM_API extern PGM_MetaComponent const* const PGM_def_input_voltage_regulator; +// attributes of input voltage_regulator +PGM_API extern PGM_MetaAttribute const* const PGM_def_input_voltage_regulator_id; +PGM_API extern PGM_MetaAttribute const* const PGM_def_input_voltage_regulator_regulated_object; +PGM_API extern PGM_MetaAttribute const* const PGM_def_input_voltage_regulator_status; +PGM_API extern PGM_MetaAttribute const* const PGM_def_input_voltage_regulator_u_ref; +PGM_API extern PGM_MetaAttribute const* const PGM_def_input_voltage_regulator_q_min; +PGM_API extern PGM_MetaAttribute const* const PGM_def_input_voltage_regulator_q_max; // component shunt PGM_API extern PGM_MetaComponent const* const PGM_def_input_shunt; // attributes of input shunt @@ -502,6 +511,13 @@ PGM_API extern PGM_MetaAttribute const* const PGM_def_sym_output_source_q; PGM_API extern PGM_MetaAttribute const* const PGM_def_sym_output_source_i; PGM_API extern PGM_MetaAttribute const* const PGM_def_sym_output_source_s; PGM_API extern PGM_MetaAttribute const* const PGM_def_sym_output_source_pf; +// component voltage_regulator +PGM_API extern PGM_MetaComponent const* const PGM_def_sym_output_voltage_regulator; +// attributes of sym_output voltage_regulator +PGM_API extern PGM_MetaAttribute const* const PGM_def_sym_output_voltage_regulator_id; +PGM_API extern PGM_MetaAttribute const* const PGM_def_sym_output_voltage_regulator_energized; +PGM_API extern PGM_MetaAttribute const* const PGM_def_sym_output_voltage_regulator_limit_violated; +PGM_API extern PGM_MetaAttribute const* const PGM_def_sym_output_voltage_regulator_q; // component sym_voltage_sensor PGM_API extern PGM_MetaComponent const* const PGM_def_sym_output_sym_voltage_sensor; // attributes of sym_output sym_voltage_sensor @@ -719,6 +735,13 @@ PGM_API extern PGM_MetaAttribute const* const PGM_def_asym_output_source_q; PGM_API extern PGM_MetaAttribute const* const PGM_def_asym_output_source_i; PGM_API extern PGM_MetaAttribute const* const PGM_def_asym_output_source_s; PGM_API extern PGM_MetaAttribute const* const PGM_def_asym_output_source_pf; +// component voltage_regulator +PGM_API extern PGM_MetaComponent const* const PGM_def_asym_output_voltage_regulator; +// attributes of asym_output voltage_regulator +PGM_API extern PGM_MetaAttribute const* const PGM_def_asym_output_voltage_regulator_id; +PGM_API extern PGM_MetaAttribute const* const PGM_def_asym_output_voltage_regulator_energized; +PGM_API extern PGM_MetaAttribute const* const PGM_def_asym_output_voltage_regulator_limit_violated; +PGM_API extern PGM_MetaAttribute const* const PGM_def_asym_output_voltage_regulator_q; // component sym_voltage_sensor PGM_API extern PGM_MetaComponent const* const PGM_def_asym_output_sym_voltage_sensor; // attributes of asym_output sym_voltage_sensor @@ -843,6 +866,14 @@ PGM_API extern PGM_MetaAttribute const* const PGM_def_update_asym_gen_id; PGM_API extern PGM_MetaAttribute const* const PGM_def_update_asym_gen_status; PGM_API extern PGM_MetaAttribute const* const PGM_def_update_asym_gen_p_specified; PGM_API extern PGM_MetaAttribute const* const PGM_def_update_asym_gen_q_specified; +// component voltage_regulator +PGM_API extern PGM_MetaComponent const* const PGM_def_update_voltage_regulator; +// attributes of update voltage_regulator +PGM_API extern PGM_MetaAttribute const* const PGM_def_update_voltage_regulator_id; +PGM_API extern PGM_MetaAttribute const* const PGM_def_update_voltage_regulator_status; +PGM_API extern PGM_MetaAttribute const* const PGM_def_update_voltage_regulator_u_ref; +PGM_API extern PGM_MetaAttribute const* const PGM_def_update_voltage_regulator_q_min; +PGM_API extern PGM_MetaAttribute const* const PGM_def_update_voltage_regulator_q_max; // component shunt PGM_API extern PGM_MetaComponent const* const PGM_def_update_shunt; // attributes of update shunt 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 d293ce850b..70ee562c41 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 @@ -230,6 +230,15 @@ PGM_MetaAttribute const* const PGM_def_input_asym_gen_status = PGM_meta_get_attr PGM_MetaAttribute const* const PGM_def_input_asym_gen_type = PGM_meta_get_attribute_by_name(nullptr, "input", "asym_gen", "type"); PGM_MetaAttribute const* const PGM_def_input_asym_gen_p_specified = PGM_meta_get_attribute_by_name(nullptr, "input", "asym_gen", "p_specified"); PGM_MetaAttribute const* const PGM_def_input_asym_gen_q_specified = PGM_meta_get_attribute_by_name(nullptr, "input", "asym_gen", "q_specified"); +// component voltage_regulator +PGM_MetaComponent const* const PGM_def_input_voltage_regulator = PGM_meta_get_component_by_name(nullptr, "input", "voltage_regulator"); +// attributes of input voltage_regulator +PGM_MetaAttribute const* const PGM_def_input_voltage_regulator_id = PGM_meta_get_attribute_by_name(nullptr, "input", "voltage_regulator", "id"); +PGM_MetaAttribute const* const PGM_def_input_voltage_regulator_regulated_object = PGM_meta_get_attribute_by_name(nullptr, "input", "voltage_regulator", "regulated_object"); +PGM_MetaAttribute const* const PGM_def_input_voltage_regulator_status = PGM_meta_get_attribute_by_name(nullptr, "input", "voltage_regulator", "status"); +PGM_MetaAttribute const* const PGM_def_input_voltage_regulator_u_ref = PGM_meta_get_attribute_by_name(nullptr, "input", "voltage_regulator", "u_ref"); +PGM_MetaAttribute const* const PGM_def_input_voltage_regulator_q_min = PGM_meta_get_attribute_by_name(nullptr, "input", "voltage_regulator", "q_min"); +PGM_MetaAttribute const* const PGM_def_input_voltage_regulator_q_max = PGM_meta_get_attribute_by_name(nullptr, "input", "voltage_regulator", "q_max"); // component shunt PGM_MetaComponent const* const PGM_def_input_shunt = PGM_meta_get_component_by_name(nullptr, "input", "shunt"); // attributes of input shunt @@ -491,6 +500,13 @@ PGM_MetaAttribute const* const PGM_def_sym_output_source_q = PGM_meta_get_attrib PGM_MetaAttribute const* const PGM_def_sym_output_source_i = PGM_meta_get_attribute_by_name(nullptr, "sym_output", "source", "i"); PGM_MetaAttribute const* const PGM_def_sym_output_source_s = PGM_meta_get_attribute_by_name(nullptr, "sym_output", "source", "s"); PGM_MetaAttribute const* const PGM_def_sym_output_source_pf = PGM_meta_get_attribute_by_name(nullptr, "sym_output", "source", "pf"); +// component voltage_regulator +PGM_MetaComponent const* const PGM_def_sym_output_voltage_regulator = PGM_meta_get_component_by_name(nullptr, "sym_output", "voltage_regulator"); +// attributes of sym_output voltage_regulator +PGM_MetaAttribute const* const PGM_def_sym_output_voltage_regulator_id = PGM_meta_get_attribute_by_name(nullptr, "sym_output", "voltage_regulator", "id"); +PGM_MetaAttribute const* const PGM_def_sym_output_voltage_regulator_energized = PGM_meta_get_attribute_by_name(nullptr, "sym_output", "voltage_regulator", "energized"); +PGM_MetaAttribute const* const PGM_def_sym_output_voltage_regulator_limit_violated = PGM_meta_get_attribute_by_name(nullptr, "sym_output", "voltage_regulator", "limit_violated"); +PGM_MetaAttribute const* const PGM_def_sym_output_voltage_regulator_q = PGM_meta_get_attribute_by_name(nullptr, "sym_output", "voltage_regulator", "q"); // component sym_voltage_sensor PGM_MetaComponent const* const PGM_def_sym_output_sym_voltage_sensor = PGM_meta_get_component_by_name(nullptr, "sym_output", "sym_voltage_sensor"); // attributes of sym_output sym_voltage_sensor @@ -708,6 +724,13 @@ PGM_MetaAttribute const* const PGM_def_asym_output_source_q = PGM_meta_get_attri PGM_MetaAttribute const* const PGM_def_asym_output_source_i = PGM_meta_get_attribute_by_name(nullptr, "asym_output", "source", "i"); PGM_MetaAttribute const* const PGM_def_asym_output_source_s = PGM_meta_get_attribute_by_name(nullptr, "asym_output", "source", "s"); PGM_MetaAttribute const* const PGM_def_asym_output_source_pf = PGM_meta_get_attribute_by_name(nullptr, "asym_output", "source", "pf"); +// component voltage_regulator +PGM_MetaComponent const* const PGM_def_asym_output_voltage_regulator = PGM_meta_get_component_by_name(nullptr, "asym_output", "voltage_regulator"); +// attributes of asym_output voltage_regulator +PGM_MetaAttribute const* const PGM_def_asym_output_voltage_regulator_id = PGM_meta_get_attribute_by_name(nullptr, "asym_output", "voltage_regulator", "id"); +PGM_MetaAttribute const* const PGM_def_asym_output_voltage_regulator_energized = PGM_meta_get_attribute_by_name(nullptr, "asym_output", "voltage_regulator", "energized"); +PGM_MetaAttribute const* const PGM_def_asym_output_voltage_regulator_limit_violated = PGM_meta_get_attribute_by_name(nullptr, "asym_output", "voltage_regulator", "limit_violated"); +PGM_MetaAttribute const* const PGM_def_asym_output_voltage_regulator_q = PGM_meta_get_attribute_by_name(nullptr, "asym_output", "voltage_regulator", "q"); // component sym_voltage_sensor PGM_MetaComponent const* const PGM_def_asym_output_sym_voltage_sensor = PGM_meta_get_component_by_name(nullptr, "asym_output", "sym_voltage_sensor"); // attributes of asym_output sym_voltage_sensor @@ -832,6 +855,14 @@ PGM_MetaAttribute const* const PGM_def_update_asym_gen_id = PGM_meta_get_attribu PGM_MetaAttribute const* const PGM_def_update_asym_gen_status = PGM_meta_get_attribute_by_name(nullptr, "update", "asym_gen", "status"); PGM_MetaAttribute const* const PGM_def_update_asym_gen_p_specified = PGM_meta_get_attribute_by_name(nullptr, "update", "asym_gen", "p_specified"); PGM_MetaAttribute const* const PGM_def_update_asym_gen_q_specified = PGM_meta_get_attribute_by_name(nullptr, "update", "asym_gen", "q_specified"); +// component voltage_regulator +PGM_MetaComponent const* const PGM_def_update_voltage_regulator = PGM_meta_get_component_by_name(nullptr, "update", "voltage_regulator"); +// attributes of update voltage_regulator +PGM_MetaAttribute const* const PGM_def_update_voltage_regulator_id = PGM_meta_get_attribute_by_name(nullptr, "update", "voltage_regulator", "id"); +PGM_MetaAttribute const* const PGM_def_update_voltage_regulator_status = PGM_meta_get_attribute_by_name(nullptr, "update", "voltage_regulator", "status"); +PGM_MetaAttribute const* const PGM_def_update_voltage_regulator_u_ref = PGM_meta_get_attribute_by_name(nullptr, "update", "voltage_regulator", "u_ref"); +PGM_MetaAttribute const* const PGM_def_update_voltage_regulator_q_min = PGM_meta_get_attribute_by_name(nullptr, "update", "voltage_regulator", "q_min"); +PGM_MetaAttribute const* const PGM_def_update_voltage_regulator_q_max = PGM_meta_get_attribute_by_name(nullptr, "update", "voltage_regulator", "q_max"); // component shunt PGM_MetaComponent const* const PGM_def_update_shunt = PGM_meta_get_component_by_name(nullptr, "update", "shunt"); // attributes of update shunt diff --git a/src/power_grid_model/_core/dataset_definitions.py b/src/power_grid_model/_core/dataset_definitions.py index 38fc93f909..f00336dd5a 100644 --- a/src/power_grid_model/_core/dataset_definitions.py +++ b/src/power_grid_model/_core/dataset_definitions.py @@ -65,6 +65,7 @@ class ComponentType(str, Enum, metaclass=_MetaEnum): sym_gen = "sym_gen" asym_load = "asym_load" asym_gen = "asym_gen" + voltage_regulator = "voltage_regulator" shunt = "shunt" source = "source" sym_voltage_sensor = "sym_voltage_sensor" diff --git a/src/power_grid_model/validation/_validation.py b/src/power_grid_model/validation/_validation.py index 63753e2245..ea808b5bf3 100644 --- a/src/power_grid_model/validation/_validation.py +++ b/src/power_grid_model/validation/_validation.py @@ -66,6 +66,7 @@ from power_grid_model.validation.errors import ( IdNotInDatasetError, InvalidIdError, + InvalidVoltageRegulationError, MissingValueError, MultiComponentNotUniqueError, ValidationError, @@ -342,10 +343,14 @@ def validate_required_values( # noqa: PLR0915 # Regulators required["regulator"] = required["base"] + ["regulated_object", "status"] - required[ComponentType.transformer_tap_regulator] = required["regulator"] + required[ComponentType.transformer_tap_regulator] = required["regulator"].copy() if calculation_type is None or calculation_type == CalculationType.power_flow: required[ComponentType.transformer_tap_regulator] += ["control_side", "u_set", "u_band"] + required[ComponentType.voltage_regulator] = required["regulator"].copy() + if calculation_type is None or calculation_type == CalculationType.power_flow: + required[ComponentType.voltage_regulator] += ["u_ref"] + # Appliances required["appliance"] = required["base"] + ["node", "status"] required[ComponentType.source] = required["appliance"].copy() @@ -504,6 +509,7 @@ def validate_values(data: SingleDataset, calculation_type: CalculationType | Non ComponentType.asym_load: lambda d: validate_generic_load_gen(d, ComponentType.asym_load), ComponentType.asym_gen: lambda d: validate_generic_load_gen(d, ComponentType.asym_gen), ComponentType.shunt: validate_shunt, + ComponentType.voltage_regulator: validate_voltage_regulator, } for component, validator in component_validators.items(): @@ -885,6 +891,111 @@ def validate_shunt(data: SingleDataset) -> list[ValidationError]: return validate_appliance(data, ComponentType.shunt) +def validate_voltage_regulator(data: SingleDataset) -> list[ValidationError]: + errors = validate_base(data, ComponentType.voltage_regulator) + errors += _all_valid_ids(data, ComponentType.voltage_regulator, "regulated_object", [ + ComponentType.sym_gen, + ComponentType.asym_gen, + ComponentType.sym_load, + ComponentType.asym_load + ]) + errors += _all_boolean(data, ComponentType.voltage_regulator, "status") + errors += _all_unique(data, ComponentType.voltage_regulator, "regulated_object") + errors += _all_greater_than_zero(data, ComponentType.voltage_regulator, "u_ref") + errors += validate_same_u_ref_per_node_voltage_regulator(data) + return errors + + +def validate_same_u_ref_per_node_voltage_regulator( + data: SingleDataset, +) -> list[ValidationError]: + """Ensure that all voltage regulators and sources connected to the same node have the same u_ref + - collect u_ref defined by sources + - if there are multiple sources on the same node, check they have the same u_ref + - if they have different u_ref, then remove them from consideration and later only check + that two voltage regulators connected to the same node have the same u_ref, because + usually the source has priority in defining the node type (Slack vs. PV) + """ + + errors: list[ValidationError] = [] + if ComponentType.voltage_regulator in data: + node_u_refs = _init_node_u_ref_from_sources(data) + vr_data = data[ComponentType.voltage_regulator] + if vr_data.size != 0: + appliance_to_node : dict[int, int] = _init_appliance_to_node_mapping(data) + + regulator_ids = vr_data["id"] + regulator_status = vr_data["status"] + appliance_ids = vr_data["regulated_object"] + u_refs = vr_data["u_ref"] + + # update node_u_refs with voltage regulator u_ref + node_regulators: dict[int, list[int]] = {} + for appliance_id, regulator_id, status, u_ref in zip( + appliance_ids, regulator_ids, regulator_status, u_refs + ): + if status == 0: + continue # skip disabled voltage regulators + node_id = appliance_to_node.get(appliance_id) + if node_id is not None: + node_u_refs.setdefault(node_id, set()).add(u_ref) + node_regulators.setdefault(node_id, []).append(regulator_id) + + # get nodes with different u_refs + error_node_ids = set( + [ + node_id + for node_id, u_refs in node_u_refs.items() + if node_id is not None and u_refs is not None and len(u_refs) > 1 + ] + ) + + # collect voltage regulator ids connected to those nodes + error_regulator_ids = [] + for node_id in error_node_ids: + error_regulator_ids.extend(node_regulators[node_id]) + + if len(error_regulator_ids) > 0: + errors.append( + InvalidVoltageRegulationError( + ComponentType.voltage_regulator, "u_ref", error_regulator_ids + ) + ) + + return errors + + +def _init_node_u_ref_from_sources(data: SingleDataset): + """ Initialize a mapping of node IDs to u_ref values defined by sources connected to those nodes. + Multiple sources connected to the same node are possible and the resulting u_ref is effectively + determined relative to the sk value of those sources. In that case, a voltage regulator at the same node + probably won't reference the same u_ref value as the sources, so we remove the u_ref of the sources again + and only check the voltage regulators among each other. + """ + node_u_refs: dict[int, set[float]] = {} + if ComponentType.source in data: + source_data = data[ComponentType.source] + for idx, node_id in enumerate(source_data["node"]): + u_ref = source_data["u_ref"][idx] + node_u_refs.setdefault(node_id, set()).add(u_ref) + + # clear nodes with different source u_refs from consideration + for node_id, u_refs in node_u_refs.items(): + if len(u_refs) > 1: + u_refs.clear() + + return node_u_refs + +def _init_appliance_to_node_mapping(data: SingleDataset): + appliance_to_node: dict[int, int] = {} + for component_type in [ComponentType.sym_gen, ComponentType.asym_gen, + ComponentType.sym_load, ComponentType.asym_load]: + if component_type in data: + for appliance in data[component_type]: + appliance_to_node[appliance["id"]] = appliance["node"] + + return appliance_to_node + def validate_generic_voltage_sensor(data: SingleDataset, component: ComponentType) -> list[ValidationError]: errors = validate_base(data, component) errors += _all_greater_than_zero(data, component, "u_sigma") diff --git a/src/power_grid_model/validation/errors.py b/src/power_grid_model/validation/errors.py index cd1b883042..ea8850835f 100644 --- a/src/power_grid_model/validation/errors.py +++ b/src/power_grid_model/validation/errors.py @@ -557,6 +557,12 @@ def enum_str(self) -> str: def __eq__(self, other): return super().__eq__(other) and self.enum == other.enum +class InvalidVoltageRegulationError(SingleFieldValidationError): + """ + U_ref values for voltage regulators connected to the same node are not identical. + """ + + _message = "Voltage regulators connected to the same node have different u_ref values for {n} {objects}." class UnsupportedMeasuredTerminalType(InvalidValueError): """ diff --git a/tests/cpp_unit_tests/CMakeLists.txt b/tests/cpp_unit_tests/CMakeLists.txt index 16592f36f0..c44a70b168 100644 --- a/tests/cpp_unit_tests/CMakeLists.txt +++ b/tests/cpp_unit_tests/CMakeLists.txt @@ -61,6 +61,7 @@ add_executable( "test_fault.cpp" "test_transformer_tap_regulator.cpp" "test_current_sensor.cpp" + "test_voltage_regulator.cpp" ) target_link_libraries( diff --git a/tests/cpp_unit_tests/test_voltage_regulator.cpp b/tests/cpp_unit_tests/test_voltage_regulator.cpp new file mode 100644 index 0000000000..da0b2ad9f5 --- /dev/null +++ b/tests/cpp_unit_tests/test_voltage_regulator.cpp @@ -0,0 +1,236 @@ +// SPDX-FileCopyrightText: Contributors to the Power Grid Model project +// +// SPDX-License-Identifier: MPL-2.0 + +#include + +#include + +namespace power_grid_model { + +namespace { +void check_nan_preserving_equality(std::floating_point auto actual, std::floating_point auto expected) { + if (is_nan(expected)) { + is_nan(actual); + } else { + CHECK(actual == doctest::Approx(expected)); + } +} +} // namespace + + +TEST_CASE("Test voltage regulator") { + VoltageRegulatorInput const input{.id = 1, + .regulated_object = 2, + .status = 1, + .u_ref = 1.05, + .q_min = 1e6, + .q_max = 100e6}; + + double const injection_direction{1.0}; + + VoltageRegulator voltage_regulator{input, ComponentType::generic_load_gen, injection_direction}; + + SUBCASE("Test energized") { + CHECK(voltage_regulator.is_energized(true)); + CHECK_FALSE(voltage_regulator.is_energized(false)); + } + + SUBCASE("Test regulated object") { CHECK(voltage_regulator.regulated_object() == ID{2}); } + + SUBCASE("Test regulated object type") { + CHECK(voltage_regulator.regulated_object_type() == ComponentType::generic_load_gen); + } + + SUBCASE("Test status") { CHECK(voltage_regulator.status()); } + + SUBCASE("Test u_ref") { CHECK(voltage_regulator.u_ref() == 1.05); } + + SUBCASE("Test injection direction") { + CHECK(voltage_regulator.injection_direction() == injection_direction); + } + + SUBCASE("Test q limits") { + CHECK(voltage_regulator.q_min() == 1e6); + CHECK(voltage_regulator.q_max() == 100e6); + } + + SUBCASE("Test get_output") { + SUBCASE("symmetric") { + VoltageRegulatorOutput const output = voltage_regulator.get_output({ + .limit_violated = 0, + .q = 50.0 /*Mvar*/, + .generator_id = 2, + .generator_status = 1 + }); + CHECK(output.id == 1); + CHECK(output.energized); + CHECK(output.q == 50.0 * base_power); + CHECK(output.limit_violated == 0); + } + SUBCASE("asymmetric") { + VoltageRegulatorOutput const output = voltage_regulator.get_output({ + .limit_violated = 0, + .q = {30.0, 40.0, 50.0} /*Mvar*/, + .generator_id = 2, + .generator_status = 1 + }); + CHECK(output.id == 1); + CHECK(output.energized); + CHECK(output.q[0] == doctest::Approx(30.0 * base_power)); + CHECK(output.q[1] == doctest::Approx(40.0 * base_power)); + CHECK(output.q[2] == doctest::Approx(50.0 * base_power)); + CHECK(output.limit_violated == 0); + } + } + + SUBCASE("Test calc param") { + SUBCASE("symmetric") { + VoltageRegulatorCalcParam const param = voltage_regulator.calc_param(); + + CHECK(param.u_ref == 1.05); + CHECK(param.q_min == 1); + CHECK(param.q_max == 100); + CHECK(param.status); + } + SUBCASE("asymmetric") { + VoltageRegulatorCalcParam const param = voltage_regulator.calc_param(); + + CHECK(param.u_ref == 1.05); + for (size_t i = 0; i != 3; i++) { + CHECK(param.q_min(i) == 1); + CHECK(param.q_max(i) == 100); + } + CHECK(param.status); + } + } + + SUBCASE("Test short circuit output") { + RegulatorShortCircuitOutput const sc_output = voltage_regulator.get_null_sc_output(); + CHECK(sc_output.id == 1); + CHECK(sc_output.energized == 0); + } + + SUBCASE("Test update") { + SUBCASE("Set all values") { + VoltageRegulatorUpdate const update{.id = 1, + .status = 0, + .u_ref = 0.97, + .q_min = 10e6, + .q_max = 110e6}; + voltage_regulator.update(update); + + SUBCASE("symmetric") { + VoltageRegulatorCalcParam const param = voltage_regulator.calc_param(); + + CHECK(param.u_ref == 0.97); + CHECK(param.q_min == 10.0); + CHECK(param.q_max == 110.0); + CHECK_FALSE(param.status); + CHECK_FALSE(voltage_regulator.is_energized(true)); + CHECK_FALSE(voltage_regulator.is_energized(false)); + } + SUBCASE("asymmetric") { + VoltageRegulatorCalcParam const param = voltage_regulator.calc_param(); + + CHECK(param.u_ref == 0.97); + for (size_t i = 0; i != 3; i++) { + CHECK(param.q_min(i) == 10.0); + CHECK(param.q_max(i) == 110.0); + } + CHECK_FALSE(param.status); + CHECK_FALSE(voltage_regulator.is_energized(true)); + CHECK_FALSE(voltage_regulator.is_energized(false)); + } + } + SUBCASE("Set nan values") { + SUBCASE("symmetric") { + VoltageRegulatorCalcParam const before_param = voltage_regulator.calc_param(); + + VoltageRegulatorUpdate const update{.id = 1}; + voltage_regulator.update(update); + + VoltageRegulatorCalcParam const param = voltage_regulator.calc_param(); + + CHECK(cabs(param.u_ref - before_param.u_ref) < numerical_tolerance); + CHECK(param.q_min == doctest::Approx(before_param.q_min)); + CHECK(param.q_max == doctest::Approx(before_param.q_max)); + CHECK(param.status == before_param.status); + } + SUBCASE("asymmetric") { + VoltageRegulatorCalcParam const before_param = voltage_regulator.calc_param(); + + VoltageRegulatorUpdate const update{.id = 1}; + voltage_regulator.update(update); + + VoltageRegulatorCalcParam const param = voltage_regulator.calc_param(); + + CHECK(cabs(param.u_ref - before_param.u_ref) < numerical_tolerance); + for (size_t i = 0; i != 3; i++) { + CHECK(param.q_min(i) == doctest::Approx(before_param.q_min(i))); + CHECK(param.q_max(i) == doctest::Approx(before_param.q_max(i))); + } + CHECK(param.status == before_param.status); + } + } + } + + SUBCASE("Test update inverse") { + VoltageRegulatorUpdate update{.id = 1, + .status = na_IntS, + .u_ref = nan, + .q_min = nan, + .q_max = nan}; + auto expected = update; + + SUBCASE("Identical") { + // default values + } + + SUBCASE("Status") { + SUBCASE("same") { update.status = status_to_int(voltage_regulator.status()); } + SUBCASE("different") { update.status = IntS{0}; } + expected.status = status_to_int(voltage_regulator.status()); + } + + SUBCASE("u_ref") { + SUBCASE("same") { update.u_ref = input.u_ref; } + SUBCASE("different") { update.u_ref = 1.1; } + expected.u_ref = input.u_ref; + } + + SUBCASE("q_min") { + SUBCASE("same") { update.q_min = input.q_min; } + SUBCASE("different") { update.q_min = 30e6; } + expected.q_min = input.q_min; + } + + SUBCASE("q_max") { + SUBCASE("same") { update.q_max = input.q_max; } + SUBCASE("different") { update.q_max = 300e6; } + expected.q_max = input.q_max; + } + + SUBCASE("multiple") { + update.id = 1; + update.status = 0; + update.u_ref = 1.025; + update.q_min = 40e6; + update.q_max = 400e6; + expected.status = status_to_int(voltage_regulator.status()); + expected.u_ref = input.u_ref; + expected.q_min = input.q_min; + expected.q_max = input.q_max; + } + + auto const inv = voltage_regulator.inverse(update); + + CHECK(inv.id == expected.id); + CHECK(inv.status == expected.status); + check_nan_preserving_equality(inv.u_ref, expected.u_ref); + check_nan_preserving_equality(inv.q_min, expected.q_min); + check_nan_preserving_equality(inv.q_max, expected.q_max); + } +} + +} // namespace power_grid_model diff --git a/tests/data/power_flow/pv-node/pv-node3/input.json b/tests/data/power_flow/pv-node/pv-node3/input.json new file mode 100644 index 0000000000..ab2dca6107 --- /dev/null +++ b/tests/data/power_flow/pv-node/pv-node3/input.json @@ -0,0 +1,97 @@ +{ + "version": "1.0", + "type": "input", + "is_batch": false, + "attributes": {}, + "data": { + "node": [ + { + "id": 1, + "u_rated": 110e3 + }, + { + "id": 2, + "u_rated": 110e3 + }, + { + "id": 3, + "u_rated": 110e3 + } + ], + "line": [ + { + "id": 4, + "from_node": 1, + "to_node": 2, + "from_status": 1, + "to_status": 1, + "r1": 0.0, + "x1": 10.0, + "c1": 238.78e-9, + "tan1": 0.0, + "_length": 25.0 + }, + { + "id": 5, + "from_node": 1, + "to_node": 3, + "from_status": 1, + "to_status": 1, + "r1": 0.0, + "x1": 10.0, + "c1": 238.78e-9, + "tan1": 0.0, + "_length": 25.0 + }, + { + "id": 6, + "from_node": 3, + "to_node": 2, + "from_status": 1, + "to_status": 1, + "r1": 0.0, + "x1": 10.0, + "c1": 238.78e-9, + "tan1": 0.0, + "_length": 25.0 + } + ], + "source": [ + { + "id": 7, + "node": 3, + "status": 1, + "u_ref": 1.018182, + "sk": 1e40 + } + ], + "sym_gen": [ + { + "id": 8, + "node": 2, + "status": 1, + "type": 0, + "p_specified": 70e6, + "q_specified": 0e6 + } + ], + "voltage_regulator": [ + { + "id": 9, + "regulated_object": 8, + "status": 1, + "u_ref": 1.027273 + } + ], + "sym_load": [ + { + "id": 10, + "node": 1, + "status": 1, + "type": 0, + "p_specified": 100e6, + "q_specified": 30e6 + } + ] + } +} diff --git a/tests/data/power_flow/pv-node/pv-node3/input.json.license b/tests/data/power_flow/pv-node/pv-node3/input.json.license new file mode 100644 index 0000000000..7601059167 --- /dev/null +++ b/tests/data/power_flow/pv-node/pv-node3/input.json.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: Contributors to the Power Grid Model project + +SPDX-License-Identifier: MPL-2.0 diff --git a/tests/data/power_flow/pv-node/pv-node3/params.json b/tests/data/power_flow/pv-node/pv-node3/params.json new file mode 100644 index 0000000000..4c39b1e6db --- /dev/null +++ b/tests/data/power_flow/pv-node/pv-node3/params.json @@ -0,0 +1,10 @@ +{ + "calculation_method": "newton_raphson", + "rtol": 1e-5, + "atol": 1e-5, + "extra_params": { + "newton_raphson": { + "experimental_features": "enabled" + } + } +} \ No newline at end of file diff --git a/tests/data/power_flow/pv-node/pv-node3/params.json.license b/tests/data/power_flow/pv-node/pv-node3/params.json.license new file mode 100644 index 0000000000..7601059167 --- /dev/null +++ b/tests/data/power_flow/pv-node/pv-node3/params.json.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: Contributors to the Power Grid Model project + +SPDX-License-Identifier: MPL-2.0 diff --git a/tests/data/power_flow/pv-node/pv-node3/sym_output.json b/tests/data/power_flow/pv-node/pv-node3/sym_output.json new file mode 100644 index 0000000000..6392d9425c --- /dev/null +++ b/tests/data/power_flow/pv-node/pv-node3/sym_output.json @@ -0,0 +1,110 @@ +{ + "version": "1.0", + "type": "sym_output", + "is_batch": false, + "attributes": {}, + "data": { + "node": [ + { + "id": 1, + "energized": 1, + "u_pu": 1.01, + "u": 111099.965168, + "u_angle": -0.03476 + }, + { + "id": 2, + "energized": 1, + "u_pu": 1.027273, + "u": 113000.03, + "u_angle": 0.010464 + }, + { + "id": 3, + "energized": 1, + "u_pu": 1.018182, + "u": 112000.02, + "u_angle": -0.0 + } + ], + "sym_load": [ + { + "id": 10, + "energized": 1, + "p": 100000000.0, + "q": 30000000.0, + "i": 542.548665, + "s": 104403065.089106, + "pf": 0.957826 + } + ], + "sym_gen": [ + { + "id": 8, + "energized": 1, + "p": 70000000.0, + "q": 33165882.712756, + "i": 395.76334, + "s": 77459510.559493, + "pf": 0.903698 + } + ], + "source": [ + { + "id": 7, + "energized": 1, + "p": 30000000.0, + "q": -1239548.855064, + "i": 154.779317, + "s": 30025597.102541, + "pf": 0.999147 + } + ], + "line": [ + { + "id": 4, + "energized": 1, + "p_from": -56756451.555671, + "q_from": -20289074.948068, + "i_from": 313.22372, + "s_from": 60273886.181659, + "p_to": 56756451.555671, + "q_to": 22275406.177503, + "i_to": 311.519783, + "s_to": 60971210.530577 + }, + { + "id": 5, + "energized": 1, + "p_from": -43243548.444329, + "q_from": -9710925.051933, + "i_from": 230.319167, + "s_from": 44320498.05024, + "p_to": 43243548.444329, + "q_to": 10361765.341661, + "i_to": 229.22676, + "s_to": 44467636.130704 + }, + { + "id": 6, + "energized": 1, + "p_from": -13243548.444329, + "q_from": -11601314.196725, + "i_from": 90.75897, + "s_from": 17606307.576787, + "p_to": 13243548.444329, + "q_to": 10890476.535252, + "i_to": 87.605241, + "s_to": 17146254.826118 + } + ], + "voltage_regulator": [ + { + "id": 9, + "energized": 1, + "q": 33165882.712756, + "limit_violated": 0 + } + ] + } +} \ No newline at end of file diff --git a/tests/data/power_flow/pv-node/pv-node3/sym_output.json.license b/tests/data/power_flow/pv-node/pv-node3/sym_output.json.license new file mode 100644 index 0000000000..7601059167 --- /dev/null +++ b/tests/data/power_flow/pv-node/pv-node3/sym_output.json.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: Contributors to the Power Grid Model project + +SPDX-License-Identifier: MPL-2.0 diff --git a/tests/data/power_flow/pv-node/pv-node5/asym_output.json b/tests/data/power_flow/pv-node/pv-node5/asym_output.json new file mode 100644 index 0000000000..6009ec78cb --- /dev/null +++ b/tests/data/power_flow/pv-node/pv-node5/asym_output.json @@ -0,0 +1,707 @@ +{ + "version": "1.0", + "type": "asym_output", + "is_batch": false, + "attributes": {}, + "data": { + "node": [ + { + "id": 1, + "energized": 1, + "u_pu": [ + 1.0, + 1.0, + 1.0 + ], + "u": [ + 63508.529611, + 63508.529611, + 63508.529611 + ], + "u_angle": [ + -0.0, + -2.094395, + 2.094395 + ] + }, + { + "id": 2, + "energized": 1, + "u_pu": [ + 0.981047, + 0.981051, + 0.981057 + ], + "u": [ + 62304.878037, + 62305.135128, + 62305.476376 + ], + "u_angle": [ + -0.052691, + -2.147056, + 2.041773 + ] + }, + { + "id": 3, + "energized": 1, + "u_pu": [ + 1.03, + 1.03, + 1.03 + ], + "u": [ + 65413.785499, + 65413.785499, + 65413.785499 + ], + "u_angle": [ + -0.007676, + -2.101404, + 2.088276 + ] + }, + { + "id": 4, + "energized": 1, + "u_pu": [ + 0.969962, + 0.969969, + 0.969979 + ], + "u": [ + 61600.843694, + 61601.305226, + 61601.914364 + ], + "u_angle": [ + -0.11265, + -2.206863, + 1.98217 + ] + }, + { + "id": 5, + "energized": 1, + "u_pu": [ + 1.0, + 1.0, + 1.0 + ], + "u": [ + 63508.529611, + 63508.529611, + 63508.529611 + ], + "u_angle": [ + -0.074935, + -2.16882, + 2.020651 + ] + } + ], + "sym_load": [ + { + "id": 15, + "energized": 1, + "p": [ + 8333333.333333, + 8333333.333333, + 8333333.333333 + ], + "q": [ + 2666666.666667, + 2666666.666667, + 2666666.666667 + ], + "i": [ + 140.432073, + 140.431493, + 140.430724 + ], + "s": [ + 8749603.165604, + 8749603.165604, + 8749603.165604 + ], + "pf": [ + 0.952424, + 0.952424, + 0.952424 + ] + }, + { + "id": 16, + "energized": 1, + "p": [ + 8333333.333333, + 8333333.333333, + 8333333.333333 + ], + "q": [ + 2333333.333333, + 2333333.333333, + 2333333.333333 + ], + "i": [ + 138.89501, + 138.894437, + 138.893676 + ], + "s": [ + 8653836.657165, + 8653836.657165, + 8653836.657165 + ], + "pf": [ + 0.962964, + 0.962964, + 0.962964 + ] + }, + { + "id": 17, + "energized": 1, + "p": [ + 8333333.333333, + 8333333.333333, + 8333333.333333 + ], + "q": [ + 2333333.333333, + 2333333.333333, + 2333333.333333 + ], + "i": [ + 140.482437, + 140.481385, + 140.479996 + ], + "s": [ + 8653836.657165, + 8653836.657165, + 8653836.657165 + ], + "pf": [ + 0.962964, + 0.962964, + 0.962964 + ] + }, + { + "id": 18, + "energized": 1, + "p": [ + 8333333.333333, + 8333333.333333, + 8333333.333333 + ], + "q": [ + 2666666.666667, + 2666666.666667, + 2666666.666667 + ], + "i": [ + 142.037067, + 142.036003, + 142.034598 + ], + "s": [ + 8749603.165604, + 8749603.165604, + 8749603.165604 + ], + "pf": [ + 0.952424, + 0.952424, + 0.952424 + ] + }, + { + "id": 19, + "energized": 1, + "p": [ + 3333333.333333, + 3333333.333333, + 3333333.333333 + ], + "q": [ + 1666666.666667, + 1666666.666667, + 1666666.666667 + ], + "i": [ + 58.681566, + 58.681566, + 58.681566 + ], + "s": [ + 3726779.9625, + 3726779.9625, + 3726779.9625 + ], + "pf": [ + 0.894427, + 0.894427, + 0.894427 + ] + }, + { + "id": 20, + "energized": 1, + "p": [ + 5000000.0, + 5000000.0, + 5000000.0 + ], + "q": [ + 1666666.666667, + 1666666.666667, + 1666666.666667 + ], + "i": [ + 82.988266, + 82.988266, + 82.988266 + ], + "s": [ + 5270462.766947, + 5270462.766947, + 5270462.766947 + ], + "pf": [ + 0.948683, + 0.948683, + 0.948683 + ] + } + ], + "sym_gen": [ + { + "id": 21, + "energized": 1, + "p": [ + 6666666.666667, + 6666666.666667, + 6666666.666667 + ], + "q": [ + 8849502.888601, + 9034359.946627, + 9164316.37254 + ], + "i": [ + 169.377562, + 171.643135, + 173.245716 + ], + "s": [ + 11079627.512683, + 11227827.220333, + 11332658.073923 + ], + "pf": [ + 0.601705, + 0.593763, + 0.58827 + ] + }, + { + "id": 23, + "energized": 1, + "p": [ + 0.0, + 0.0, + 0.0 + ], + "q": [ + 1230793.524641, + 1232930.022781, + 1235805.971135 + ], + "i": [ + 19.379972, + 19.413613, + 19.458898 + ], + "s": [ + 1230793.524641, + 1232930.022781, + 1235805.971135 + ], + "pf": [ + 0.0, + 0.0, + 0.0 + ] + } + ], + "asym_gen": [ + { + "id": 22, + "energized": 1, + "p": [ + 3000000.0, + 3300000.0, + 3700000.0 + ], + "q": [ + 7549502.888601, + 7334359.946627, + 7164316.37254 + ], + "i": [ + 124.189882, + 122.949069, + 123.266656 + ], + "s": [ + 8123730.292483, + 8042564.008243, + 8063338.581869 + ], + "pf": [ + 0.369288, + 0.410317, + 0.458867 + ] + } + ], + "source": [ + { + "id": 12, + "energized": 1, + "p": [ + 32402161.684234, + 32101708.087941, + 31701165.791323 + ], + "q": [ + -5102567.578867, + -5078986.990092, + -5047242.137306 + ], + "i": [ + 516.489168, + 511.758216, + 505.450924 + ], + "s": [ + 32801467.615156, + 32501011.845924, + 32100444.94585 + ], + "pf": [ + 0.987827, + 0.987714, + 0.987562 + ] + } + ], + "line": [ + { + "id": 6, + "energized": 1, + "p_from": [ + 1960254.201611, + 1685696.315558, + 1319676.469707 + ], + "q_from": [ + -12410499.065243, + -12385077.780867, + -12350900.756639 + ], + "i_from": [ + 197.837326, + 196.812444, + 195.583232 + ], + "s_from": [ + 12564357.666962, + 12499268.926878, + 12421203.463637 + ], + "p_to": [ + -1921890.388025, + -1647735.374096, + -1282195.718415 + ], + "q_to": [ + 12532998.699685, + 12503548.694063, + 12464569.768138 + ], + "i_to": [ + 193.835288, + 192.798074, + 191.555099 + ], + "s_to": [ + 12679499.953464, + 12611651.827095, + 12530344.183824 + ], + "loading": 0.387916 + }, + { + "id": 7, + "energized": 1, + "p_from": [ + 11588557.054691, + 11614402.040763, + 11648862.385082 + ], + "q_from": [ + 3866007.077518, + 3865171.19919, + 3864062.976943 + ], + "i_from": [ + 186.755889, + 187.12669, + 187.621282 + ], + "s_from": [ + 12216409.67442, + 12240665.143836, + 12273018.274098 + ], + "p_to": [ + -11499778.484927, + -11525277.209557, + -11559274.62077 + ], + "q_to": [ + -3631067.633433, + -3626769.140691, + -3621031.587384 + ], + "i_to": [ + 189.886585, + 190.249164, + 190.73284 + ], + "s_to": [ + 12059417.786981, + 12082444.668071, + 12113162.242586 + ], + "loading": 0.373986 + }, + { + "id": 8, + "energized": 1, + "p_from": [ + 3166445.151593, + 3191943.876224, + 3225941.287436 + ], + "q_from": [ + 1528527.824741, + 1526365.830138, + 1523504.225186 + ], + "i_from": [ + 55.363798, + 55.710953, + 56.175115 + ], + "s_from": [ + 3516073.407803, + 3538120.737959, + 3567598.956462 + ], + "p_to": [ + -3148220.802473, + -3173530.122148, + -3207272.362295 + ], + "q_to": [ + -2575903.270448, + -2571856.158104, + -2566454.63098 + ], + "i_to": [ + 66.033962, + 66.310574, + 66.681539 + ], + "s_to": [ + 4067747.765021, + 4084817.93158, + 4107710.478944 + ], + "loading": 0.130748 + }, + { + "id": 9, + "energized": 1, + "p_from": [ + -8892025.302568, + -8878180.596139, + -8859722.746279 + ], + "q_from": [ + -1561231.58136, + -1563670.131191, + -1566924.394844 + ], + "i_from": [ + 146.55713, + 146.341538, + 146.054202 + ], + "s_from": [ + 9028042.868315, + 9014829.725338, + 8997218.414604 + ], + "p_to": [ + 8997436.035664, + 8983271.771901, + 8964388.740278 + ], + "q_to": [ + 1385719.975412, + 1384954.020085, + 1383944.677639 + ], + "i_to": [ + 143.343264, + 143.121, + 142.824722 + ], + "s_to": [ + 9103519.927265, + 9089404.236009, + 9070588.09326 + ], + "loading": 0.287367 + }, + { + "id": 10, + "energized": 1, + "p_from": [ + -4626420.561626, + -4614955.94838, + -4599671.558093 + ], + "q_from": [ + -862865.148192, + -864473.710705, + -866620.974176 + ], + "i_from": [ + 76.398278, + 76.219562, + 75.981395 + ], + "s_from": [ + 4706198.410288, + 4695224.510286, + 4680599.358545 + ], + "p_to": [ + 4654716.808883, + 4643113.299413, + 4627644.29879 + ], + "q_to": [ + -60003.882474, + -59798.24623, + -59515.554118 + ], + "i_to": [ + 74.714913, + 74.528341, + 74.279618 + ], + "s_to": [ + 4655103.547378, + 4643498.351614, + 4628026.994012 + ], + "loading": 0.149801 + }, + { + "id": 11, + "energized": 1, + "p_from": [ + -21321383.47555, + -21309779.96608, + -21294310.965456 + ], + "q_from": [ + -4939996.117526, + -4940201.75377, + -4940484.445882 + ], + "i_from": [ + 351.275574, + 351.093442, + 350.850687 + ], + "s_from": [ + 21886181.826728, + 21874924.355771, + 21859919.168505 + ], + "p_to": [ + 21444471.446959, + 21432740.000482, + 21417100.581338 + ], + "q_to": [ + 5922211.510964, + 5921136.77069, + 5919713.941695 + ], + "i_to": [ + 350.302565, + 350.120005, + 349.876676 + ], + "s_to": [ + 22247200.831117, + 22235606.683549, + 22220153.250206 + ], + "loading": 0.688776 + } + ], + "voltage_regulator": [ + { + "id": 30, + "energized": 1, + "q": [ + 3849502.888601, + 4034359.946627, + 4164316.37254 + ], + "limit_violated": 0 + }, + { + "id": 31, + "energized": 1, + "q": [ + 3849502.888601, + 4034359.946627, + 4164316.37254 + ], + "limit_violated": 0 + }, + { + "id": 32, + "energized": 1, + "q": [ + 1230793.524641, + 1232930.022781, + 1235805.971135 + ], + "limit_violated": 0 + } + ] + } +} \ No newline at end of file diff --git a/tests/data/power_flow/pv-node/pv-node5/asym_output.json.license b/tests/data/power_flow/pv-node/pv-node5/asym_output.json.license new file mode 100644 index 0000000000..7601059167 --- /dev/null +++ b/tests/data/power_flow/pv-node/pv-node5/asym_output.json.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: Contributors to the Power Grid Model project + +SPDX-License-Identifier: MPL-2.0 diff --git a/tests/data/power_flow/pv-node/pv-node5/input.json b/tests/data/power_flow/pv-node/pv-node5/input.json new file mode 100644 index 0000000000..3cad99ec59 --- /dev/null +++ b/tests/data/power_flow/pv-node/pv-node5/input.json @@ -0,0 +1,241 @@ +{ + "version": "1.0", + "type": "input", + "is_batch": false, + "attributes": {}, + "data": { + "node": [ + { + "id": 1, + "u_rated": 110.0e3 + }, + { + "id": 2, + "u_rated": 110.0e3 + }, + { + "id": 3, + "u_rated": 110.0e3 + }, + { + "id": 4, + "u_rated": 110.0e3 + }, + { + "id": 5, + "u_rated": 110.0e3 + } + ], + "line": [ + { + "id": 6, + "from_node": 1, + "to_node": 3, + "from_status": 1, + "to_status": 1, + "r1": 1.0, + "x1": 10.0, + "c1": 200.0e-9, + "tan1": 0.0, + "r0": 1.0, + "x0": 10.0, + "c0": 200.0e-9, + "tan0": 0.0, + "i_n": 510.0, + "_length": 20.0 + }, + { + "id": 7, + "from_node": 3, + "to_node": 5, + "from_status": 1, + "to_status": 1, + "r1": 2.5, + "x1": 25.0, + "c1": 500.0e-9, + "tan1": 0.0, + "r0": 2.5, + "x0": 25.0, + "c0": 500.0e-9, + "tan0": 0.0, + "i_n": 510.0, + "_length": 50.0 + }, + { + "id": 8, + "from_node": 5, + "to_node": 4, + "from_status": 1, + "to_status": 1, + "r1": 5.0, + "x1": 50.0, + "c1": 1000.0e-9, + "tan1": 0.0, + "r0": 5.0, + "x0": 50.0, + "c0": 1000.0e-9, + "tan0": 0.0, + "i_n": 510.0, + "_length": 100.0 + }, + { + "id": 9, + "from_node": 4, + "to_node": 1, + "from_status": 1, + "to_status": 1, + "r1": 5.0, + "x1": 50.0, + "c1": 1000.0e-9, + "tan1": 0.0, + "r0": 5.0, + "x0": 50.0, + "c0": 1000.0e-9, + "tan0": 0.0, + "i_n": 510.0, + "_length": 100.0 + }, + { + "id": 10, + "from_node": 4, + "to_node": 2, + "from_status": 1, + "to_status": 1, + "r1": 5.0, + "x1": 50.0, + "c1": 1000.0e-9, + "tan1": 0.0, + "r0": 5.0, + "x0": 50.0, + "c0": 1000.0e-9, + "tan0": 0.0, + "i_n": 510.0, + "_length": 100.0 + }, + { + "id": 11, + "from_node": 2, + "to_node": 1, + "from_status": 1, + "to_status": 1, + "r1": 1.0, + "x1": 10.0, + "c1": 200.0e-9, + "tan1": 0.0, + "r0": 1.0, + "x0": 10.0, + "c0": 200.0e-9, + "tan0": 0.0, + "i_n": 510.0, + "_length": 20.0 + } + ], + "sym_load": [ + { + "id": 15, + "node": 2, + "status": 1, + "type": 0, + "p_specified": 25e6, + "q_specified": 8e6 + }, + { + "id": 16, + "node": 2, + "status": 1, + "type": 0, + "p_specified": 25e6, + "q_specified": 7e6 + }, + { + "id": 17, + "node": 4, + "status": 1, + "type": 0, + "p_specified": 25e6, + "q_specified": 7e6 + }, + { + "id": 18, + "node": 4, + "status": 1, + "type": 0, + "p_specified": 25e6, + "q_specified": 8e6 + }, + { + "id": 19, + "node": 5, + "status": 1, + "type": 0, + "p_specified": 10e6, + "q_specified": 5e6 + }, + { + "id": 20, + "node": 5, + "status": 1, + "type": 0, + "p_specified": 15e6, + "q_specified": 5e6 + } + ], + "sym_gen": [ + { + "id": 21, + "node": 3, + "status": 1, + "type": 0, + "p_specified": 20e6, + "q_specified": 15e6 + }, + { + "id": 23, + "node": 5, + "status": 1, + "type": 0, + "p_specified": 0e6, + "q_specified": 0e6 + } + ], + "asym_gen": [ + { + "id": 22, + "node": 3, + "status": 1, + "type": 0, + "p_specified": [3.0e6, 3.3e6, 3.7e6], + "q_specified": [3.7e6, 3.3e6, 3.0e6] + } + ], + "voltage_regulator": [ + { + "id": 30, + "status": 1, + "regulated_object": 21, + "u_ref": 1.03 + }, + { + "id": 31, + "status": 1, + "regulated_object": 22, + "u_ref": 1.03 + }, + { + "id": 32, + "status": 1, + "regulated_object": 23, + "u_ref": 1.0 + } + ], + "source": [ + { + "id": 12, + "node": 1, + "status": 1, + "u_ref": 1.0, + "sk": 1e99 + } + ] + } +} diff --git a/tests/data/power_flow/pv-node/pv-node5/input.json.license b/tests/data/power_flow/pv-node/pv-node5/input.json.license new file mode 100644 index 0000000000..7601059167 --- /dev/null +++ b/tests/data/power_flow/pv-node/pv-node5/input.json.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: Contributors to the Power Grid Model project + +SPDX-License-Identifier: MPL-2.0 diff --git a/tests/data/power_flow/pv-node/pv-node5/params.json b/tests/data/power_flow/pv-node/pv-node5/params.json new file mode 100644 index 0000000000..4c39b1e6db --- /dev/null +++ b/tests/data/power_flow/pv-node/pv-node5/params.json @@ -0,0 +1,10 @@ +{ + "calculation_method": "newton_raphson", + "rtol": 1e-5, + "atol": 1e-5, + "extra_params": { + "newton_raphson": { + "experimental_features": "enabled" + } + } +} \ No newline at end of file diff --git a/tests/data/power_flow/pv-node/pv-node5/params.json.license b/tests/data/power_flow/pv-node/pv-node5/params.json.license new file mode 100644 index 0000000000..7601059167 --- /dev/null +++ b/tests/data/power_flow/pv-node/pv-node5/params.json.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: Contributors to the Power Grid Model project + +SPDX-License-Identifier: MPL-2.0 diff --git a/tests/data/power_flow/pv-node/pv-node5/sym_output.json b/tests/data/power_flow/pv-node/pv-node5/sym_output.json new file mode 100644 index 0000000000..bf1b565ab1 --- /dev/null +++ b/tests/data/power_flow/pv-node/pv-node5/sym_output.json @@ -0,0 +1,243 @@ +{ + "version": "1.0", + "type": "sym_output", + "is_batch": false, + "attributes": {}, + "data": { + "node": [ + { + "id": 1, + "energized": 1, + "u_pu": 1.0, + "u": 110000.0, + "u_angle": -0.0 + }, + { + "id": 2, + "energized": 1, + "u_pu": 0.981052, + "u": 107915.708985, + "u_angle": -0.052658 + }, + { + "id": 3, + "energized": 1, + "u_pu": 1.03, + "u": 113300.0, + "u_angle": -0.006934 + }, + { + "id": 4, + "energized": 1, + "u_pu": 0.96997, + "u": 106696.678857, + "u_angle": -0.112448 + }, + { + "id": 5, + "energized": 1, + "u_pu": 1.0, + "u": 110000.0, + "u_angle": -0.074368 + } + ], + "sym_load": [ + { + "id": 15, + "energized": 1, + "p": 25000000.0, + "q": 8000000.0, + "i": 140.431429, + "s": 26248809.496813, + "pf": 0.952424 + }, + { + "id": 16, + "energized": 1, + "p": 25000000.0, + "q": 7000000.0, + "i": 138.894373, + "s": 25961509.971494, + "pf": 0.962964 + }, + { + "id": 17, + "energized": 1, + "p": 25000000.0, + "q": 7000000.0, + "i": 140.481268, + "s": 25961509.971494, + "pf": 0.962964 + }, + { + "id": 18, + "energized": 1, + "p": 25000000.0, + "q": 8000000.0, + "i": 142.035885, + "s": 26248809.496813, + "pf": 0.952424 + }, + { + "id": 19, + "energized": 1, + "p": 10000000.0, + "q": 5000000.0, + "i": 58.681566, + "s": 11180339.887499, + "pf": 0.894427 + }, + { + "id": 20, + "energized": 1, + "p": 15000000.0, + "q": 5000000.0, + "i": 82.988266, + "s": 15811388.300842, + "pf": 0.948683 + } + ], + "sym_gen": [ + { + "id": 21, + "energized": 1, + "p": 20000000.0, + "q": 27048049.69586, + "i": 171.417578, + "s": 33639218.069832, + "pf": 0.594544 + }, + { + "id": 23, + "energized": 1, + "p": 0.0, + "q": 3699505.482704, + "i": 19.417368, + "s": 3699505.482704, + "pf": 0.0 + } + ], + "asym_gen": [ + { + "id": 22, + "energized": 1, + "p": 10000000.0, + "q": 22048049.69586, + "i": 123.367691, + "s": 24209842.944371, + "pf": 0.413055 + } + ], + "source": [ + { + "id": 12, + "energized": 1, + "p": 96204980.506008, + "q": -15229064.572078, + "i": 511.232581, + "s": 97402888.467961, + "pf": 0.987702 + } + ], + "line": [ + { + "id": 6, + "energized": 1, + "p_from": 4965576.477175, + "q_from": -37146726.009285, + "i_from": 196.703984, + "s_from": 37477142.406533, + "p_to": -4851821.160439, + "q_to": 37500863.672372, + "i_to": 192.688348, + "s_to": 37813422.811836, + "loading": 0.385694 + }, + { + "id": 7, + "energized": 1, + "p_from": 34851821.160439, + "q_from": 11595235.719349, + "i_from": 187.167897, + "s_from": 36730082.079768, + "p_to": -34584331.097396, + "q_to": -10878873.849605, + "i_to": 190.28946, + "s_to": 36255011.428628, + "loading": 0.373117 + }, + { + "id": 8, + "energized": 1, + "p_from": 9584331.097396, + "q_from": 4578379.332308, + "i_from": 55.749578, + "s_from": 10621721.136192, + "p_to": -9529026.386931, + "q_to": -7714218.796747, + "i_to": 66.341395, + "s_to": 12260159.686028, + "loading": 0.130081 + }, + { + "id": 9, + "energized": 1, + "p_from": -26629927.095875, + "q_from": -4691823.584425, + "i_from": 146.317588, + "s_from": 27040085.533869, + "p_to": 26945094.210583, + "q_to": 4154608.162418, + "i_to": 143.096306, + "s_to": 27263508.046479, + "loading": 0.286897 + }, + { + "id": 10, + "energized": 1, + "p_from": -13841046.517195, + "q_from": -2593957.618828, + "i_from": 76.19971, + "s_from": 14082016.361992, + "p_to": 13925472.341166, + "q_to": -179325.179997, + "i_to": 74.507612, + "s_to": 13926626.922725, + "loading": 0.149411 + }, + { + "id": 11, + "energized": 1, + "p_from": -63925472.341166, + "q_from": -14820674.820003, + "i_from": 351.073209, + "s_from": 65621021.145373, + "p_to": 64294309.818251, + "q_to": 17763053.274789, + "i_to": 350.099724, + "s_to": 66702955.981337, + "loading": 0.688379 + } + ], + "voltage_regulator": [ + { + "id": 30, + "energized": 1, + "q": 12048049.69586, + "limit_violated": 0 + }, + { + "id": 31, + "energized": 1, + "q": 12048049.69586, + "limit_violated": 0 + }, + { + "id": 32, + "energized": 1, + "q": 3699505.482704, + "limit_violated": 0 + } + ] + } +} \ No newline at end of file diff --git a/tests/data/power_flow/pv-node/pv-node5/sym_output.json.license b/tests/data/power_flow/pv-node/pv-node5/sym_output.json.license new file mode 100644 index 0000000000..7601059167 --- /dev/null +++ b/tests/data/power_flow/pv-node/pv-node5/sym_output.json.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: Contributors to the Power Grid Model project + +SPDX-License-Identifier: MPL-2.0 diff --git a/tests/unit/validation/test_input_validation.py b/tests/unit/validation/test_input_validation.py index b1c5162932..c6ceb87040 100644 --- a/tests/unit/validation/test_input_validation.py +++ b/tests/unit/validation/test_input_validation.py @@ -24,9 +24,11 @@ from power_grid_model.validation import validate_input_data from power_grid_model.validation.errors import ( FaultPhaseError, + InfinityError, InvalidAssociatedEnumValueError, InvalidEnumValueError, InvalidIdError, + InvalidVoltageRegulationError, MixedPowerCurrentSensorError, MultiComponentNotUniqueError, MultiFieldValidationError, @@ -374,6 +376,14 @@ def original_data() -> dict[ComponentType, np.ndarray]: fault["fault_object"] = [200, 3] + list(range(10, 28, 2)) + 9 * [0] fault["r_f"] = [-1.0, 0.0, 1.0] + 17 * [_nan_type(ComponentType.fault, "r_f")] fault["x_f"] = [-1.0, 0.0, 1.0] + 17 * [_nan_type(ComponentType.fault, "x_f")] + + voltage_regulator = initialize_array(DatasetType.input, ComponentType.voltage_regulator, 8) + voltage_regulator["id"] = [60, 61, 62, 63, 64, 65, 66, 67] + # 20-27 are gen/load IDs, 200 is invalid, 16,18 are shunts/sources, 20 is duplicate with different u_ref + voltage_regulator["regulated_object"] = [20, 23, 25, 27, 200, 16, 18, 20] + voltage_regulator["status"] = [1, 0, -1, 1, 1, 5, 0, 1] # -1 and 5 are invalid boolean values + voltage_regulator["u_ref"] = [1.02, 100, 100.0, 100.0, 1.0, np.inf, 0.0, 1.03] + data = { ComponentType.node: node, ComponentType.line: line, @@ -396,6 +406,7 @@ def original_data() -> dict[ComponentType, np.ndarray]: ComponentType.sym_current_sensor: sym_current_sensor, ComponentType.asym_current_sensor: asym_current_sensor, ComponentType.fault: fault, + ComponentType.voltage_regulator: voltage_regulator, } return data # noqa: RET504 @@ -954,6 +965,26 @@ def test_validate_input_data_transformer_tap_regulator(input_data): assert NotUniqueError(ComponentType.transformer_tap_regulator, "regulated_object", [51, 54]) in validation_errors +def test_validate_input_data_voltage_regulator(input_data): + validation_errors = validate_input_data(input_data, calculation_type=CalculationType.power_flow) + assert validation_errors is not None + + assert NotBooleanError(ComponentType.voltage_regulator, "status", [62, 65]) in validation_errors + assert NotUniqueError(ComponentType.voltage_regulator, "regulated_object", [60, 67]) in validation_errors + assert NotGreaterThanError(ComponentType.voltage_regulator, "u_ref", [66], 0.0) in validation_errors + assert InfinityError(ComponentType.voltage_regulator, "u_ref", [65]) in validation_errors + assert ( + InvalidIdError( + ComponentType.voltage_regulator, + "regulated_object", + [64, 65, 66], + [ComponentType.sym_gen, ComponentType.asym_gen, ComponentType.sym_load, ComponentType.asym_load], + ) + in validation_errors + ) + assert InvalidVoltageRegulationError(ComponentType.voltage_regulator, "u_ref", [60, 67]) in validation_errors + + def test_fault(input_data): validation_errors = validate_input_data(input_data, calculation_type=CalculationType.short_circuit) assert validation_errors is not None diff --git a/tests/unit/validation/test_validation_functions.py b/tests/unit/validation/test_validation_functions.py index 927889f190..c9a11ef403 100644 --- a/tests/unit/validation/test_validation_functions.py +++ b/tests/unit/validation/test_validation_functions.py @@ -239,6 +239,8 @@ def test_validate_required_values_sym_calculation(calculation_type, symmetric): fault = initialize_array(DatasetType.input, ComponentType.fault, 1) + voltage_regulator = initialize_array(DatasetType.input, ComponentType.voltage_regulator, 1) + data = { ComponentType.node: node, ComponentType.line: line, @@ -258,6 +260,7 @@ def test_validate_required_values_sym_calculation(calculation_type, symmetric): ComponentType.sym_current_sensor: sym_current_sensor, ComponentType.asym_current_sensor: asym_current_sensor, ComponentType.fault: fault, + ComponentType.voltage_regulator: voltage_regulator, } required_values_errors = validate_required_values(data=data, calculation_type=calculation_type, symmetric=symmetric) @@ -474,6 +477,23 @@ def test_validate_required_values_sym_calculation(calculation_type, symmetric): assert (MissingValueError(ComponentType.fault, "status", [NaN]) in required_values_errors) == sc_dependent assert (MissingValueError(ComponentType.fault, "fault_type", [NaN]) in required_values_errors) == sc_dependent + assert ( + MissingValueError(ComponentType.voltage_regulator, "id", [NaN]) + in required_values_errors + ) + assert ( + MissingValueError(ComponentType.voltage_regulator, "status", [NaN]) + in required_values_errors + ) + assert ( + MissingValueError(ComponentType.voltage_regulator, "regulated_object", [NaN]) + in required_values_errors + ) + assert ( + MissingValueError(ComponentType.voltage_regulator, "u_ref", [NaN]) + in required_values_errors + ) == pf_dependent + def test_validate_required_values_asym_calculation(): line = initialize_array(DatasetType.input, ComponentType.line, 1)