Skip to content

Commit 4d153e9

Browse files
Merge pull request #896 from PowerGridModel/feature/statistics-module
Measurement statistics module
2 parents ea5db64 + 0202694 commit 4d153e9

File tree

8 files changed

+1144
-68
lines changed

8 files changed

+1144
-68
lines changed

power_grid_model_c/power_grid_model/include/power_grid_model/calculation_parameters.hpp

+4-14
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include "common/common.hpp"
88
#include "common/enum.hpp"
99
#include "common/grouped_index_vector.hpp"
10+
#include "common/statistics.hpp"
1011
#include "common/three_phase_tensor.hpp"
1112

1213
namespace power_grid_model {
@@ -78,21 +79,10 @@ template <symmetry_tag sym_type> struct ApplianceShortCircuitSolverOutput {
7879
ComplexValue<sym> i{};
7980
};
8081

81-
// Complex measured value of a sensor in p.u. with a uniform variance across all phases and axes of the complex plane
82-
// (circularly symmetric)
83-
template <symmetry_tag sym_type> struct UniformComplexRandomVariable {
84-
using sym = sym_type;
85-
86-
static constexpr bool symmetric{is_symmetric_v<sym>};
87-
88-
ComplexValue<sym> value{};
89-
double variance{}; // variance (sigma^2) of the error range, in p.u.
90-
};
91-
9282
// voltage sensor calculation parameters for state estimation
9383
// The value is the complex voltage
9484
// If the imaginary part is NaN, it means the angle calculation is not correct
95-
template <symmetry_tag sym> using VoltageSensorCalcParam = UniformComplexRandomVariable<sym>;
85+
template <symmetry_tag sym> using VoltageSensorCalcParam = UniformComplexRandVar<sym>;
9686

9787
// power sensor calculation parameters for state estimation
9888
// The value is the complex power
@@ -118,8 +108,8 @@ template <symmetry_tag sym_type> struct CurrentSensorCalcParam {
118108

119109
AngleMeasurementType angle_measurement_type{};
120110
ComplexValue<sym> value{};
121-
double i_real_variance{}; // variance (sigma^2) of the error range of real part of the current, in p.u.
122-
double i_imag_variance{}; // variance (sigma^2) of the error range of imaginary part of the current, in p.u.
111+
RealValue<sym> i_real_variance{}; // variance (sigma^2) of the error range of real part of the current, in p.u.
112+
RealValue<sym> i_imag_variance{}; // variance (sigma^2) of the error range of imaginary part of the current, in p.u.
123113
};
124114

125115
template <typename T>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
// SPDX-FileCopyrightText: Contributors to the Power Grid Model project <powergridmodel@lfenergy.org>
2+
//
3+
// SPDX-License-Identifier: MPL-2.0
4+
5+
#pragma once
6+
7+
#include "common.hpp"
8+
#include "three_phase_tensor.hpp"
9+
10+
/**
11+
* @file statistics.hpp
12+
* @brief This file contains various structures and functions for handling statistical representations of
13+
* random variables (RandVar) used in the Power Grid Model, like in the State Estimation algorithms to
14+
* handle measurements.
15+
*
16+
* The structures provided in this file are used to represent measured values of sensors
17+
* with different types of variances. These structures support both symmetric and asymmetric representations and
18+
* provide conversion operators to transform between these representations.
19+
*
20+
* A random variable in PGM can have following characteristics:
21+
* - Uniform: Single total variance for all phases
22+
* - Independent: all phases are independent from each other
23+
* - Scalar: Named as `Real` in this file, a scalar value `RealValue`, eg. real axis: RealValue (* 1), imaginary axis:
24+
* RealValue (* i).
25+
* - Complex: a complex value with real and imaginary parts.
26+
*
27+
* Based on these, we use combined variables in Decomposed/Polar forms:
28+
* - Decomposed: treat random variables individually as in cartesian co-ordinates with separated variances for both real
29+
* and imaginary part.
30+
* - Polar: random variables are in polar co-ordinates, with magnitudes and angles.
31+
*
32+
*/
33+
34+
namespace power_grid_model {
35+
template <symmetry_tag sym_type> struct UniformRealRandVar {
36+
using sym = sym_type;
37+
38+
RealValue<sym> value{};
39+
double variance{}; // variance (sigma^2) of the error range
40+
41+
explicit operator UniformRealRandVar<asymmetric_t>() const
42+
requires(is_symmetric_v<sym>)
43+
{
44+
return {.value = RealValue<asymmetric_t>{std::piecewise_construct, value}, .variance = variance};
45+
}
46+
explicit operator UniformRealRandVar<symmetric_t>() const
47+
requires(is_asymmetric_v<sym>)
48+
{
49+
return {.value = mean_val(value), .variance = variance / 3.0};
50+
}
51+
};
52+
53+
template <symmetry_tag sym_type> struct IndependentRealRandVar {
54+
using sym = sym_type;
55+
56+
RealValue<sym> value{};
57+
RealValue<sym> variance{}; // variance (sigma^2) of the error range
58+
59+
explicit operator UniformRealRandVar<symmetric_t>() const {
60+
constexpr auto scale = is_asymmetric_v<sym> ? 3.0 : 1.0;
61+
return {.value = mean_val(value), .variance = mean_val(variance) / scale};
62+
}
63+
explicit operator UniformRealRandVar<asymmetric_t>() const {
64+
return {.value = value, .variance = mean_val(variance)};
65+
}
66+
explicit operator IndependentRealRandVar<asymmetric_t>() const
67+
requires(is_symmetric_v<sym>)
68+
{
69+
return {.value = RealValue<asymmetric_t>{std::piecewise_construct, value},
70+
.variance = RealValue<asymmetric_t>{std::piecewise_construct, variance}};
71+
}
72+
explicit operator IndependentRealRandVar<symmetric_t>() const
73+
requires(is_asymmetric_v<sym>)
74+
{
75+
return {.value = mean_val(value), .variance = mean_val(variance) / 3.0};
76+
}
77+
};
78+
79+
// Complex measured value of a sensor with a uniform variance across all phases and axes of the complex plane
80+
// (rotationally symmetric)
81+
template <symmetry_tag sym_type> struct UniformComplexRandVar {
82+
using sym = sym_type;
83+
84+
ComplexValue<sym> value{};
85+
double variance{}; // variance (sigma^2) of the error range
86+
};
87+
88+
inline UniformComplexRandVar<symmetric_t> pos_seq(UniformComplexRandVar<asymmetric_t> const& var) {
89+
return {.value = pos_seq(var.value), .variance = var.variance / 3.0};
90+
}
91+
inline UniformComplexRandVar<asymmetric_t> three_phase(UniformComplexRandVar<symmetric_t> const& var) {
92+
return {.value = ComplexValue<asymmetric_t>{var.value}, .variance = var.variance};
93+
}
94+
95+
// Complex measured value of a sensor with separate variances per phase (but rotationally symmetric in the
96+
// complex plane)
97+
template <symmetry_tag sym_type> struct IndependentComplexRandVar {
98+
using sym = sym_type;
99+
100+
ComplexValue<sym> value{};
101+
RealValue<sym> variance{}; // variance (sigma^2) of the error range
102+
103+
explicit operator UniformComplexRandVar<sym>() const {
104+
return UniformComplexRandVar<sym>{.value = value, .variance = sum_val(variance)};
105+
}
106+
};
107+
108+
// Complex measured value of a sensor modeled as separate real and imaginary components with independent
109+
// variances (rotationally symmetric)
110+
template <symmetry_tag sym_type> struct DecomposedComplexRandVar {
111+
using sym = sym_type;
112+
113+
IndependentRealRandVar<sym> real_component;
114+
IndependentRealRandVar<sym> imag_component;
115+
116+
ComplexValue<sym> value() const { return {real_component.value, imag_component.value}; }
117+
118+
explicit operator UniformComplexRandVar<sym>() const {
119+
return static_cast<UniformComplexRandVar<sym>>(static_cast<IndependentComplexRandVar<sym>>(*this));
120+
}
121+
explicit operator IndependentComplexRandVar<sym>() const {
122+
return IndependentComplexRandVar<sym>{.value = value(),
123+
.variance = real_component.variance + imag_component.variance};
124+
}
125+
};
126+
127+
// Complex measured value of a sensor in polar coordinates (magnitude and angle)
128+
// (rotationally symmetric)
129+
template <symmetry_tag sym_type> struct PolarComplexRandVar {
130+
using sym = sym_type;
131+
132+
UniformRealRandVar<sym> magnitude;
133+
UniformRealRandVar<sym> angle;
134+
135+
ComplexValue<sym> value() const { return magnitude.value * exp(1.0i * angle.value); }
136+
137+
explicit operator UniformComplexRandVar<sym>() const {
138+
return static_cast<UniformComplexRandVar<sym>>(static_cast<IndependentComplexRandVar<sym>>(*this));
139+
}
140+
explicit operator IndependentComplexRandVar<sym>() const {
141+
return IndependentComplexRandVar<sym>{
142+
.value = value(), .variance = magnitude.variance + magnitude.value * magnitude.value * angle.variance};
143+
}
144+
145+
// For sym to sym conversion:
146+
// Var(I_Re) ≈ Var(I) * cos^2(θ) + Var(θ) * I^2 * sin^2(θ)
147+
// Var(I_Im) ≈ Var(I) * sin^2(θ) + Var(θ) * I^2 * cos^2(θ)
148+
// For asym to asym conversion:
149+
// Var(I_Re,p) ≈ Var(I_p) * cos^2(θ_p) + Var(θ_p) * I_p^2 * sin^2(θ_p)
150+
// Var(I_Im,p) ≈ Var(I_p) * sin^2(θ_p) + Var(θ_p) * I_p^2 * cos^2(θ_p)
151+
explicit operator DecomposedComplexRandVar<sym>() const {
152+
auto const cos_theta = cos(angle.value);
153+
auto const sin_theta = sin(angle.value);
154+
auto const real_component = magnitude.value * cos_theta;
155+
auto const imag_component = magnitude.value * sin_theta;
156+
return DecomposedComplexRandVar<sym>{
157+
.real_component = {.value = real_component,
158+
.variance = magnitude.variance * cos_theta * cos_theta +
159+
imag_component * imag_component * angle.variance},
160+
.imag_component = {.value = imag_component,
161+
.variance = magnitude.variance * sin_theta * sin_theta +
162+
real_component * real_component * angle.variance}};
163+
}
164+
165+
// Var(I_Re,p) ≈ Var(I) * cos^2(θ - 2πp/3) + Var(θ) * I^2 * sin^2(θ - 2πp/3)
166+
// Var(I_Im,p) ≈ Var(I) * sin^2(θ - 2πp/3) + Var(θ) * I^2 * cos^2(θ - 2πp/3)
167+
explicit operator DecomposedComplexRandVar<asymmetric_t>() const
168+
requires(is_symmetric_v<sym>)
169+
{
170+
ComplexValue<asymmetric_t> const unit_complex{exp(1.0i * angle.value)};
171+
ComplexValue<asymmetric_t> const complex = magnitude.value * unit_complex;
172+
return DecomposedComplexRandVar<asymmetric_t>{
173+
.real_component = {.value = real(complex),
174+
.variance = magnitude.variance * real(unit_complex) * real(unit_complex) +
175+
imag(complex) * imag(complex) * angle.variance},
176+
.imag_component = {.value = imag(complex),
177+
.variance = magnitude.variance * imag(unit_complex) * imag(unit_complex) +
178+
real(complex) * real(complex) * angle.variance}};
179+
}
180+
181+
// Var(I_Re) ≈ (1 / 9) * sum_p(Var(I_p) * cos^2(theta_p + 2 * pi * p / 3) + Var(theta_p) * I_p^2 * sin^2(theta_p + 2
182+
// * pi * p / 3))
183+
// Var(I_Im) ≈ (1 / 9) * sum_p(Var(I_p) * sin^2(theta_p + 2 * pi * p / 3) + Var(theta_p) * I_p^2 *
184+
// cos^2(theta_p + 2 * pi * p / 3))
185+
explicit operator DecomposedComplexRandVar<symmetric_t>() const
186+
requires(is_asymmetric_v<sym>)
187+
{
188+
ComplexValue<asymmetric_t> const unit_complex{exp(1.0i * angle.value)};
189+
ComplexValue<asymmetric_t> const unit_pos_seq_per_phase{unit_complex(0), a * unit_complex(1),
190+
a2 * unit_complex(2)};
191+
DoubleComplex const pos_seq_value = pos_seq(magnitude.value * unit_complex);
192+
return DecomposedComplexRandVar<symmetric_t>{
193+
.real_component = {.value = real(pos_seq_value),
194+
.variance = sum_val(magnitude.variance * real(unit_pos_seq_per_phase) *
195+
real(unit_pos_seq_per_phase) +
196+
imag(unit_pos_seq_per_phase) * imag(unit_pos_seq_per_phase) *
197+
magnitude.value * magnitude.value * angle.variance) /
198+
9.0},
199+
.imag_component = {
200+
.value = imag(pos_seq_value),
201+
.variance = sum_val(magnitude.variance * imag(unit_pos_seq_per_phase) * imag(unit_pos_seq_per_phase) +
202+
real(unit_pos_seq_per_phase) * real(unit_pos_seq_per_phase) * magnitude.value *
203+
magnitude.value * angle.variance) /
204+
9.0}};
205+
}
206+
};
207+
} // namespace power_grid_model

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

+34-32
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#include "../common/common.hpp"
1111
#include "../common/enum.hpp"
1212
#include "../common/exception.hpp"
13+
#include "../common/statistics.hpp"
1314

1415
namespace power_grid_model {
1516

@@ -48,6 +49,10 @@ class GenericCurrentSensor : public Sensor {
4849
}
4950
}
5051

52+
protected:
53+
MeasuredTerminalType terminal_type() const { return terminal_type_; }
54+
AngleMeasurementType angle_measurement_type() const { return angle_measurement_type_; }
55+
5156
private:
5257
MeasuredTerminalType terminal_type_;
5358
AngleMeasurementType angle_measurement_type_;
@@ -73,12 +78,12 @@ template <symmetry_tag current_sensor_symmetry_> class CurrentSensor : public Ge
7378

7479
explicit CurrentSensor(CurrentSensorInput<current_sensor_symmetry> const& current_sensor_input, double u_rated)
7580
: GenericCurrentSensor{current_sensor_input},
81+
base_current_{base_power_3p * inv_sqrt3 / u_rated},
82+
base_current_inv_{1.0 / base_current_},
7683
i_angle_measured_{current_sensor_input.i_angle_measured},
7784
i_angle_sigma_{current_sensor_input.i_angle_sigma},
78-
base_current_{base_power_3p * inv_sqrt3 / u_rated},
79-
base_current_inv_{1.0 / base_current_} {
80-
set_current(current_sensor_input);
81-
85+
i_sigma_{current_sensor_input.i_sigma * base_current_inv_},
86+
i_measured_{current_sensor_input.i_measured * base_current_inv_} {
8287
switch (current_sensor_input.measured_terminal_type) {
8388
using enum MeasuredTerminalType;
8489
case branch_from:
@@ -93,14 +98,13 @@ template <symmetry_tag current_sensor_symmetry_> class CurrentSensor : public Ge
9398
};
9499

95100
UpdateChange update(CurrentSensorUpdate<current_sensor_symmetry> const& update_data) {
96-
if (!is_nan(update_data.i_sigma)) {
97-
i_sigma_ = update_data.i_sigma * base_current_inv_;
98-
}
99-
if (!is_nan(update_data.i_angle_sigma)) {
100-
i_angle_sigma_ = update_data.i_angle_sigma;
101-
}
101+
assert(update_data.id == this->id() || is_nan(update_data.id));
102+
103+
update_real_value<symmetric_t>(update_data.i_sigma, i_sigma_, base_current_inv_);
104+
update_real_value<symmetric_t>(update_data.i_angle_sigma, i_angle_sigma_, 1.0);
102105
update_real_value<current_sensor_symmetry>(update_data.i_measured, i_measured_, base_current_inv_);
103106
update_real_value<current_sensor_symmetry>(update_data.i_angle_measured, i_angle_measured_, 1.0);
107+
104108
return {false, false};
105109
}
106110

@@ -117,29 +121,24 @@ template <symmetry_tag current_sensor_symmetry_> class CurrentSensor : public Ge
117121
}
118122

119123
private:
120-
RealValue<current_sensor_symmetry> i_measured_{};
121-
RealValue<current_sensor_symmetry> i_angle_measured_{};
122-
double i_sigma_{};
123-
double i_angle_sigma_{};
124124
double base_current_{};
125125
double base_current_inv_{};
126+
RealValue<current_sensor_symmetry> i_angle_measured_{};
127+
double i_angle_sigma_{};
128+
double i_sigma_{};
129+
RealValue<current_sensor_symmetry> i_measured_{};
126130

127-
void set_current(CurrentSensorInput<current_sensor_symmetry> const& input) {
128-
i_sigma_ = input.i_sigma * base_current_inv_;
129-
i_measured_ = input.i_measured * base_current_inv_;
130-
}
131-
132-
// TODO when filling the functions below take in mind that i_angle_sigma is optional
133-
134-
CurrentSensorCalcParam<symmetric_t> sym_calc_param() const final {
135-
CurrentSensorCalcParam<symmetric_t> calc_param{};
136-
// TODO
137-
return calc_param;
138-
}
139-
CurrentSensorCalcParam<asymmetric_t> asym_calc_param() const final {
140-
CurrentSensorCalcParam<asymmetric_t> calc_param{};
141-
// TODO
142-
return calc_param;
131+
CurrentSensorCalcParam<symmetric_t> sym_calc_param() const final { return calc_decomposed_param<symmetric_t>(); }
132+
CurrentSensorCalcParam<asymmetric_t> asym_calc_param() const final { return calc_decomposed_param<asymmetric_t>(); }
133+
134+
template <symmetry_tag sym_calc> CurrentSensorCalcParam<sym_calc> calc_decomposed_param() const {
135+
auto const i_polar = PolarComplexRandVar<current_sensor_symmetry>(
136+
{i_measured_, i_sigma_ * i_sigma_}, {i_angle_measured_, i_angle_sigma_ * i_angle_sigma_});
137+
auto const i_decomposed = DecomposedComplexRandVar<sym_calc>(i_polar);
138+
return CurrentSensorCalcParam<sym_calc>{.angle_measurement_type = angle_measurement_type(),
139+
.value = i_decomposed.value(),
140+
.i_real_variance = i_decomposed.real_component.variance,
141+
.i_imag_variance = i_decomposed.imag_component.variance};
143142
}
144143
CurrentSensorOutput<symmetric_t> get_sym_output(ComplexValue<symmetric_t> const& i) const final {
145144
return get_generic_output<symmetric_t>(i);
@@ -150,8 +149,11 @@ template <symmetry_tag current_sensor_symmetry_> class CurrentSensor : public Ge
150149
template <symmetry_tag sym_calc>
151150
CurrentSensorOutput<sym_calc> get_generic_output(ComplexValue<sym_calc> const& i) const {
152151
CurrentSensorOutput<sym_calc> output{};
153-
// TODO
154-
(void)i; // Suppress unused variable warning
152+
output.id = id();
153+
ComplexValue<sym_calc> const i_residual{process_mean_val<sym_calc>(i_measured_ - i) * base_current_};
154+
output.energized = 1; // current sensor is always energized
155+
output.i_residual = cabs(i_residual);
156+
output.i_angle_residual = arg(i_residual);
155157
return output;
156158
}
157159
};

power_grid_model_c/power_grid_model/include/power_grid_model/component/power_sensor.hpp

+3-3
Original file line numberDiff line numberDiff line change
@@ -87,11 +87,11 @@ template <symmetry_tag power_sensor_symmetry_> class PowerSensor : public Generi
8787
};
8888

8989
UpdateChange update(PowerSensorUpdate<power_sensor_symmetry> const& update_data) {
90+
assert(update_data.id == this->id() || is_nan(update_data.id));
91+
9092
set_power(update_data.p_measured, update_data.q_measured);
9193

92-
if (!is_nan(update_data.power_sigma)) {
93-
apparent_power_sigma_ = update_data.power_sigma * inv_base_power;
94-
}
94+
update_real_value<symmetric_t>(update_data.power_sigma, apparent_power_sigma_, inv_base_power);
9595
update_real_value<power_sensor_symmetry>(update_data.p_sigma, p_sigma_, inv_base_power);
9696
update_real_value<power_sensor_symmetry>(update_data.q_sigma, q_sigma_, inv_base_power);
9797

power_grid_model_c/power_grid_model_c/src/handle.hpp

+2-2
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ struct PGM_Handle {
2828
};
2929

3030
template <class Exception = std::exception, class Functor>
31-
auto call_with_catch(PGM_Handle* handle, Functor func, PGM_Idx error_code,
32-
std::string_view extra_msg = {}) -> std::invoke_result_t<Functor> {
31+
inline auto call_with_catch(PGM_Handle* handle, Functor func, PGM_Idx error_code,
32+
std::string_view extra_msg = {}) -> std::invoke_result_t<Functor> {
3333
if (handle) {
3434
PGM_clear_error(handle);
3535
}

tests/cpp_unit_tests/CMakeLists.txt

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ set(PROJECT_SOURCES
1212
"test_component_output.cpp"
1313
"test_component_update.cpp"
1414
"test_three_phase_tensor.cpp"
15+
"test_statistics.cpp"
1516
"test_node.cpp"
1617
"test_line.cpp"
1718
"test_generic_branch.cpp"

0 commit comments

Comments
 (0)