Skip to content

Commit 9577d53

Browse files
authored
Merge pull request #975 from PowerGridModel/feature/voltage-sensor-residuals
voltage sensor angle residual mod 2pi
2 parents 24423ed + 23b0e37 commit 9577d53

File tree

6 files changed

+87
-6
lines changed

6 files changed

+87
-6
lines changed

docs/user_manual/components.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -627,10 +627,12 @@ A sensor only has output for state estimation. For other calculation types, sens
627627
$$
628628
\begin{eqnarray}
629629
& u_{\text{residual}} = u_{\text{measured}} - u_{\text{state}} \\
630-
& \theta_{\text{residual}} = \theta_{\text{measured}} - \theta_{\text{state}}
630+
& \theta_{\text{residual}} = \theta_{\text{measured}} - \theta_{\text{state}} \pmod{2 \pi}
631631
\end{eqnarray}
632632
$$
633633

634+
The $\pmod{2\pi}$ is handled such that $-\pi \lt \theta_{\text{angle},\text{residual}} \leq \pi$.
635+
634636
### Generic Power Sensor
635637

636638
* type name: `generic_power_sensor`

power_grid_model_c/power_grid_model/include/power_grid_model/common/three_phase_tensor.hpp

+8
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,14 @@ inline ComplexValue<asymmetric_t> phase_shift(ComplexValue<asymmetric_t> const&
176176
return {phase_shift(m(0)), phase_shift(m(1)), phase_shift(m(2))};
177177
}
178178

179+
// arg(e^(i * phase)) = phase (mod 2pi). By convention restrict to [-pi, pi].
180+
inline auto phase_mod_2pi(double phase) {
181+
return RealValue<symmetric_t>{arg(ComplexValue<symmetric_t>{exp(1.0i * phase)})};
182+
}
183+
inline auto phase_mod_2pi(RealValue<asymmetric_t> const& phase) {
184+
return RealValue<asymmetric_t>{arg(ComplexValue<asymmetric_t>{exp(1.0i * phase)})};
185+
}
186+
179187
// calculate kron product of two vector
180188
inline double vector_outer_product(double x, double y) { return x * y; }
181189
inline DoubleComplex vector_outer_product(DoubleComplex x, DoubleComplex y) { return x * y; }

power_grid_model_c/power_grid_model/include/power_grid_model/component/current_sensor.hpp

+1-3
Original file line numberDiff line numberDiff line change
@@ -174,9 +174,7 @@ template <symmetry_tag current_sensor_symmetry_> class CurrentSensor : public Ge
174174
}
175175
}();
176176
output.i_residual = (cabs(i_measured_complex) - cabs(i_output)) * base_current_;
177-
// arg(e^i * u_angle) = u_angle in (-pi, pi]
178-
auto const unbounded_i_angle_residual = arg(i_measured_complex) - arg(i_output);
179-
output.i_angle_residual = arg(ComplexValue<sym_calc>{exp(1.0i * unbounded_i_angle_residual)});
177+
output.i_angle_residual = phase_mod_2pi(arg(i_measured_complex) - arg(i_output));
180178
return output;
181179
}
182180
};

power_grid_model_c/power_grid_model/include/power_grid_model/component/voltage_sensor.hpp

+2-2
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ template <symmetry_tag sym> class VoltageSensor : public GenericVoltageSensor {
151151
} else {
152152
value.u_residual = (real(u1_measured) - cabs(u)) * u_rated_;
153153
}
154-
value.u_angle_residual = arg(u1_measured) - arg(u);
154+
value.u_angle_residual = phase_mod_2pi(arg(u1_measured) - arg(u));
155155
return value;
156156
}
157157

@@ -160,7 +160,7 @@ template <symmetry_tag sym> class VoltageSensor : public GenericVoltageSensor {
160160
value.id = id();
161161
value.energized = 1;
162162
value.u_residual = (u_measured_ - cabs(u)) * u_rated_ / sqrt3;
163-
value.u_angle_residual = u_angle_measured_ - arg(u);
163+
value.u_angle_residual = phase_mod_2pi(u_angle_measured_ - arg(u));
164164
return value;
165165
}
166166
};

tests/cpp_unit_tests/test_three_phase_tensor.cpp

+32
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,38 @@ TEST_CASE("Three phase tensor") {
206206
CHECK(is_nan(vec(1)));
207207
CHECK(vec(2) == 6.0);
208208
}
209+
SUBCASE("phase_mod_2pi") {
210+
auto const check = [](double value) {
211+
CAPTURE(value);
212+
CHECK_GE(value, -pi);
213+
CHECK_LE(value, pi);
214+
if (value != pi && value != -pi) {
215+
CHECK(phase_mod_2pi(value) == doctest::Approx(value));
216+
}
217+
};
218+
auto const check_asym = [&check](RealValue<asymmetric_t> const& value) {
219+
for (Idx i : {0, 1, 2}) {
220+
CAPTURE(i);
221+
check(value(i));
222+
}
223+
};
224+
check(phase_mod_2pi(0.0));
225+
check(phase_mod_2pi(2.0 * pi));
226+
check(phase_mod_2pi(2.0 * pi + 1.0));
227+
check(phase_mod_2pi(-1.0));
228+
check(phase_mod_2pi(-pi));
229+
check(phase_mod_2pi(pi));
230+
check(phase_mod_2pi(-3.0 * pi));
231+
check(phase_mod_2pi(3.0 * pi));
232+
check(phase_mod_2pi(pi * (1.0 + std::numeric_limits<double>::epsilon())));
233+
check(phase_mod_2pi(pi * (1.0 - std::numeric_limits<double>::epsilon())));
234+
check(phase_mod_2pi(-pi * (1.0 + std::numeric_limits<double>::epsilon())));
235+
check(phase_mod_2pi(-pi * (1.0 - std::numeric_limits<double>::epsilon())));
236+
237+
check_asym(phase_mod_2pi(RealValue<asymmetric_t>{0.0, 2.0 * pi, 2.0 * pi + 1.0}));
238+
check_asym(phase_mod_2pi(RealValue<asymmetric_t>{0.0, 2.0 * pi, 2.0 * pi + 1.0}));
239+
check_asym(phase_mod_2pi(RealValue<asymmetric_t>{0.0, 2.0 * pi, 2.0 * pi + 1.0}));
240+
}
209241
}
210242

211243
} // namespace power_grid_model

tests/cpp_unit_tests/test_voltage_sensor.cpp

+41
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,47 @@ TEST_CASE("Test voltage sensor") {
363363
CHECK(sym_voltage_sensor_asym_output.u_angle_residual[2] == doctest::Approx(-0.2));
364364
}
365365

366+
SUBCASE("Angle = ± pi ∓ 0.1") {
367+
RealValue<symmetric_t> const u_measured{10.1e3};
368+
RealValue<symmetric_t> const u_angle_measured{pi - 0.1};
369+
double const u_sigma = 1.0;
370+
double const u_rated = 10.0e3;
371+
372+
VoltageSensorInput<symmetric_t> voltage_sensor_input{};
373+
voltage_sensor_input.id = 0;
374+
voltage_sensor_input.measured_object = 1;
375+
voltage_sensor_input.u_sigma = u_sigma;
376+
voltage_sensor_input.u_measured = u_measured;
377+
voltage_sensor_input.u_angle_measured = u_angle_measured;
378+
379+
VoltageSensor<symmetric_t> const voltage_sensor{voltage_sensor_input, u_rated};
380+
381+
ComplexValue<symmetric_t> const u_calc_sym{1.02 * exp(1i * (-pi + 0.1))};
382+
VoltageSensorOutput<symmetric_t> sym_voltage_sensor_sym_output =
383+
voltage_sensor.get_output<symmetric_t>(u_calc_sym);
384+
385+
ComplexValue<asymmetric_t> const u_calc_asym{1.02 * exp(1i * (-pi + 0.1)), 1.03 * exp(1i * (-pi + 0.2)),
386+
1.04 * exp(1i * (-pi + 0.3))};
387+
VoltageSensorOutput<asymmetric_t> sym_voltage_sensor_asym_output =
388+
voltage_sensor.get_output<asymmetric_t>(u_calc_asym);
389+
390+
// Check sym output
391+
CHECK(sym_voltage_sensor_sym_output.id == 0);
392+
CHECK(sym_voltage_sensor_sym_output.energized == 1);
393+
CHECK(sym_voltage_sensor_sym_output.u_residual == doctest::Approx(-100.0));
394+
CHECK(sym_voltage_sensor_sym_output.u_angle_residual == doctest::Approx(-0.2).epsilon(1e-12));
395+
396+
// Check asym output
397+
CHECK(sym_voltage_sensor_asym_output.id == 0);
398+
CHECK(sym_voltage_sensor_asym_output.energized == 1);
399+
CHECK(sym_voltage_sensor_asym_output.u_residual[0] == doctest::Approx(-100.0 / sqrt3));
400+
CHECK(sym_voltage_sensor_asym_output.u_residual[1] == doctest::Approx(-200.0 / sqrt3));
401+
CHECK(sym_voltage_sensor_asym_output.u_residual[2] == doctest::Approx(-300.0 / sqrt3));
402+
CHECK(sym_voltage_sensor_asym_output.u_angle_residual[0] == doctest::Approx(-0.2).epsilon(1e-12));
403+
CHECK(sym_voltage_sensor_asym_output.u_angle_residual[1] == doctest::Approx(-0.3));
404+
CHECK(sym_voltage_sensor_asym_output.u_angle_residual[2] == doctest::Approx(-0.4));
405+
}
406+
366407
SUBCASE("Angle = nan") {
367408
RealValue<symmetric_t> const u_measured{10.1e3};
368409
RealValue<symmetric_t> const u_angle_measured{nan};

0 commit comments

Comments
 (0)