Skip to content

Commit 4511e4b

Browse files
authored
Merge branch 'feature/current-sensor-measurements-observability' into feature/current-sensor-math-solver
2 parents 260d989 + 5b6113f commit 4511e4b

File tree

8 files changed

+598
-394
lines changed

8 files changed

+598
-394
lines changed

power_grid_model_c/power_grid_model/include/power_grid_model/common/statistics.hpp

Lines changed: 45 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -218,43 +218,75 @@ template <std::ranges::view RandVarsView>
218218
UniformComplexRandVar<typename std::ranges::range_value_t<RandVarsView>::sym>> ||
219219
std::same_as<std::ranges::range_value_t<RandVarsView>,
220220
IndependentComplexRandVar<typename std::ranges::range_value_t<RandVarsView>::sym>>
221-
constexpr auto accumulate(RandVarsView rand_vars) {
221+
constexpr auto combine(RandVarsView rand_vars) {
222222
using RandVarType = std::ranges::range_value_t<RandVarsView>;
223223
using ValueType = decltype(RandVarType::value);
224224
using VarianceType = decltype(RandVarType::variance);
225225

226226
VarianceType accumulated_inverse_variance{};
227-
ValueType accumulated_value{};
227+
ValueType weighted_accumulated_value{};
228228

229-
std::ranges::for_each(rand_vars, [&accumulated_inverse_variance, &accumulated_value](auto const& measurement) {
230-
accumulated_inverse_variance += VarianceType{1.0} / measurement.variance;
231-
accumulated_value += measurement.value / measurement.variance;
232-
});
229+
std::ranges::for_each(rand_vars,
230+
[&accumulated_inverse_variance, &weighted_accumulated_value](auto const& measurement) {
231+
accumulated_inverse_variance += VarianceType{1.0} / measurement.variance;
232+
weighted_accumulated_value += measurement.value / measurement.variance;
233+
});
233234

234-
if (is_normal(accumulated_inverse_variance)) {
235-
return RandVarType{.value = accumulated_value / accumulated_inverse_variance,
236-
.variance = VarianceType{1.0} / accumulated_inverse_variance};
235+
if (!is_normal(accumulated_inverse_variance)) {
236+
return RandVarType{.value = weighted_accumulated_value,
237+
.variance = VarianceType{std::numeric_limits<double>::infinity()}};
237238
}
238-
return RandVarType{.value = accumulated_value, .variance = VarianceType{std::numeric_limits<double>::infinity()}};
239+
return RandVarType{.value = weighted_accumulated_value / accumulated_inverse_variance,
240+
.variance = VarianceType{1.0} / accumulated_inverse_variance};
239241
}
240242

241243
template <std::ranges::view RandVarsView>
242244
requires std::same_as<std::ranges::range_value_t<RandVarsView>,
243245
DecomposedComplexRandVar<typename std::ranges::range_value_t<RandVarsView>::sym>>
244-
constexpr auto accumulate(RandVarsView rand_vars) {
246+
constexpr auto combine(RandVarsView rand_vars) {
245247
using sym = std::ranges::range_value_t<RandVarsView>::sym;
246248

247249
DecomposedComplexRandVar<sym> result{
248250
.real_component =
249-
accumulate(rand_vars | std::views::transform([](auto const& x) -> auto& { return x.real_component; })),
251+
combine(rand_vars | std::views::transform([](auto const& x) -> auto& { return x.real_component; })),
250252
.imag_component =
251-
accumulate(rand_vars | std::views::transform([](auto const& x) -> auto& { return x.imag_component; }))};
253+
combine(rand_vars | std::views::transform([](auto const& x) -> auto& { return x.imag_component; }))};
252254

253255
if (!(is_normal(result.real_component.variance) && is_normal(result.imag_component.variance))) {
254256
result.real_component.variance = RealValue<sym>{std::numeric_limits<double>::infinity()};
255257
result.imag_component.variance = RealValue<sym>{std::numeric_limits<double>::infinity()};
256258
}
257259
return result;
258260
}
261+
262+
namespace detail {
263+
template <symmetry_tag sym> inline RealValue<sym> cabs_or_real(ComplexValue<sym> const& value) {
264+
if (is_nan(imag(value))) {
265+
return real(value); // only keep real part
266+
}
267+
return cabs(value); // get abs of the value
268+
}
269+
} // namespace detail
270+
271+
template <std::ranges::view RandVarsView>
272+
requires std::same_as<std::ranges::range_value_t<RandVarsView>,
273+
UniformComplexRandVar<typename std::ranges::range_value_t<RandVarsView>::sym>>
274+
constexpr auto combine_magnitude(RandVarsView rand_vars) {
275+
using sym = std::ranges::range_value_t<RandVarsView>::sym;
276+
277+
auto const weighted_average_magnitude_measurement =
278+
statistics::combine(rand_vars | std::views::transform([](auto const& measurement) {
279+
return UniformRealRandVar<sym>{.value = detail::cabs_or_real<sym>(measurement.value),
280+
.variance = measurement.variance};
281+
}));
282+
return UniformComplexRandVar<sym>{.value =
283+
[&weighted_average_magnitude_measurement]() {
284+
ComplexValue<sym> abs_value =
285+
piecewise_complex_value<sym>(DoubleComplex{0.0, nan});
286+
abs_value += weighted_average_magnitude_measurement.value;
287+
return abs_value;
288+
}(),
289+
.variance = weighted_average_magnitude_measurement.variance};
290+
}
259291
} // namespace statistics
260292
} // namespace power_grid_model

power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/measured_values.hpp

Lines changed: 15 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,6 @@ Collect all measured Values
1515
#include <memory>
1616

1717
namespace power_grid_model::math_solver {
18-
19-
namespace detail {
20-
template <symmetry_tag sym> inline RealValue<sym> cabs_or_real(ComplexValue<sym> const& value) {
21-
if (is_nan(imag(value))) {
22-
return real(value); // only keep real part
23-
}
24-
return cabs(value); // get abs of the value
25-
}
26-
} // namespace detail
27-
2818
// processed measurement struct
2919
// combined all measurement of the same quantity
3020
// accumulate for bus injection measurement
@@ -71,6 +61,7 @@ template <symmetry_tag sym> class MeasuredValues {
7161

7262
constexpr bool has_angle() const { return n_voltage_angle_measurements_ > 0; }
7363
constexpr bool has_voltage_measurements() const { return n_voltage_measurements_ > 0; }
64+
constexpr bool has_global_angle_current() const { return n_global_angle_current_measurements_ > 0; }
7465

7566
constexpr bool has_voltage(Idx bus) const { return idx_voltage_[bus] >= 0; }
7667
constexpr bool has_angle_measurement(Idx bus) const { return !is_nan(imag(voltage(bus))); }
@@ -215,6 +206,7 @@ template <symmetry_tag sym> class MeasuredValues {
215206

216207
Idx n_voltage_measurements_{};
217208
Idx n_voltage_angle_measurements_{};
209+
Idx n_global_angle_current_measurements_{};
218210

219211
// average angle shift of voltages with angle measurement
220212
// default is zero is no voltage has angle measurement
@@ -406,15 +398,15 @@ template <symmetry_tag sym> class MeasuredValues {
406398
connected at that side. For each branch the checker checks if the from and to side are connected by checking if
407399
branch_bus_idx = disconnected.
408400
409-
If the branch_bus_idx = disconnected, idx_branch_(to|from)_(power|current)_ is set to disconnected.
401+
If the branch_bus_idx = disconnected, idx_branch_(to/from)_(power/current)_ is set to disconnected.
410402
If the side is connected, but there are no measurements in this branch side
411-
idx_branch_(to|from)_(power|current)_ is set to disconnected.
412-
Else, idx_branch_(to|from)_(power|current)_ is set to the index of the aggregated data in
403+
idx_branch_(to/from)_(power/current)_ is set to disconnected.
404+
Else, idx_branch_(to/from)_(power/current)_ is set to the index of the aggregated data in
413405
power/current_main_value_.
414406
415407
All measurement values for a single side of a branch are combined in a weighted average, which is appended to
416408
power/current_main_value_. The values in power/current_main_value_ can be found using
417-
idx_branch_(to|from)_(power|current)_.
409+
idx_branch_(to/from)_(power/current)_.
418410
*/
419411
MathModelTopology const& topo = math_topology();
420412
static constexpr auto branch_from_checker = [](BranchIdx x) { return x[0] != -1; };
@@ -437,12 +429,10 @@ template <symmetry_tag sym> class MeasuredValues {
437429
process_one_object(branch, topo.current_sensors_per_branch_to, topo.branch_bus_idx,
438430
input.measured_branch_to_current, current_main_value_, branch_to_checker);
439431

440-
if ((!has_angle()) && std::ranges::any_of(current_main_value_, [](auto const& measurement) {
432+
n_global_angle_current_measurements_ =
433+
std::ranges::count_if(current_main_value_, [](auto const& measurement) {
441434
return measurement.angle_measurement_type == AngleMeasurementType::global_angle;
442-
})) {
443-
throw ConflictingAngleMeasurementType{
444-
"Global angle current measurements require a voltage angle measurement in the connected subgrid."};
445-
}
435+
});
446436
}
447437
}
448438

@@ -455,28 +445,16 @@ template <symmetry_tag sym> class MeasuredValues {
455445
IdxRange const& sensors) {
456446
auto complex_measurements = sensors | std::views::transform([&data](Idx pos) -> auto& { return data[pos]; });
457447
if constexpr (only_magnitude) {
458-
auto const combined_magnitude_measurement = statistics::accumulate(
459-
complex_measurements | std::views::transform([](auto const& measurement) {
460-
return UniformRealRandVar<sym>{.value = detail::cabs_or_real<sym>(measurement.value),
461-
.variance = measurement.variance};
462-
}));
463-
return UniformComplexRandVar<sym>{.value =
464-
[&combined_magnitude_measurement]() {
465-
ComplexValue<sym> abs_value =
466-
piecewise_complex_value<sym>(DoubleComplex{0.0, nan});
467-
abs_value += combined_magnitude_measurement.value;
468-
return abs_value;
469-
}(),
470-
.variance = combined_magnitude_measurement.variance};
448+
return statistics::combine_magnitude(complex_measurements);
471449
} else {
472-
return statistics::accumulate(complex_measurements);
450+
return statistics::combine(complex_measurements);
473451
}
474452
}
475453
template <bool only_magnitude = false>
476454
requires(!only_magnitude)
477455
static PowerSensorCalcParam<sym> combine_measurements(std::vector<PowerSensorCalcParam<sym>> const& data,
478456
IdxRange const& sensors) {
479-
return statistics::accumulate(sensors | std::views::transform([&data](Idx pos) -> auto& { return data[pos]; }));
457+
return statistics::combine(sensors | std::views::transform([&data](Idx pos) -> auto& { return data[pos]; }));
480458
}
481459
template <bool only_magnitude = false>
482460
requires(!only_magnitude)
@@ -485,15 +463,15 @@ template <symmetry_tag sym> class MeasuredValues {
485463
auto const params = sensors | std::views::transform([&data](Idx pos) -> auto& { return data[pos]; });
486464
auto const angle_measurement_type = sensors.empty() ? AngleMeasurementType::local_angle // fallback
487465
: params.front().angle_measurement_type;
488-
if (std::ranges::any_of(params, [angle_measurement_type](auto const& params) {
489-
return params.angle_measurement_type != angle_measurement_type;
466+
if (std::ranges::any_of(params, [angle_measurement_type](auto const& param) {
467+
return param.angle_measurement_type != angle_measurement_type;
490468
})) {
491469
throw ConflictingAngleMeasurementType{
492470
"Cannot mix local and global angle current measurements on the same terminal."};
493471
}
494472

495473
return {.angle_measurement_type = angle_measurement_type,
496-
.measurement = statistics::accumulate(
474+
.measurement = statistics::combine(
497475
params | std::views::transform([](auto const& param) -> auto& { return param.measurement; }))};
498476
}
499477

power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/newton_raphson_se_solver.hpp

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,8 @@ template <symmetry_tag sym_type> class NewtonRaphsonSESolver {
214214
typename SparseLUSolver<NRSEGainBlock<sym>, NRSERhs<sym>, NRSEUnknown<sym>>::BlockPermArray perm_;
215215

216216
void initialize_unknown(ComplexValueVector<sym>& initial_u, MeasuredValues<sym> const& measured_values) {
217+
using statistics::detail::cabs_or_real;
218+
217219
reset_unknown();
218220
RealValue<sym> const mean_angle_shift = measured_values.mean_angle_shift();
219221
for (Idx bus = 0; bus != n_bus_; ++bus) {
@@ -224,7 +226,7 @@ template <symmetry_tag sym_type> class NewtonRaphsonSESolver {
224226
if (measured_values.has_angle_measurement(bus)) {
225227
estimated_result.theta() = arg(measured_values.voltage(bus));
226228
}
227-
estimated_result.v() = detail::cabs_or_real<sym>(measured_values.voltage(bus));
229+
estimated_result.v() = cabs_or_real<sym>(measured_values.voltage(bus));
228230
}
229231
initial_u[bus] = estimated_result.v() * exp(1.0i * estimated_result.theta());
230232
}
@@ -532,14 +534,16 @@ template <symmetry_tag sym_type> class NewtonRaphsonSESolver {
532534
/// @param bus bus with voltage measurement
533535
void process_voltage_measurements(NRSEGainBlock<sym>& block, NRSERhs<sym>& rhs_block,
534536
MeasuredValues<sym> const& measured_values, Idx const& bus) {
537+
using statistics::detail::cabs_or_real;
538+
535539
if (!measured_values.has_voltage(bus)) {
536540
return;
537541
}
538542

539543
// G += 1.0 / variance
540544
// for 3x3 tensor, fill diagonal
541545
auto const w_v = RealTensor<sym>{1.0 / measured_values.voltage_var(bus)};
542-
auto const abs_measured_v = detail::cabs_or_real<sym>(measured_values.voltage(bus));
546+
auto const abs_measured_v = cabs_or_real<sym>(measured_values.voltage(bus));
543547
auto const delta_v = abs_measured_v - x_[bus].v();
544548

545549
auto const virtual_angle_measurement_bus = measured_values.has_voltage(math_topo_->slack_bus)

power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/observability.hpp

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,11 @@ inline ObservabilityResult necessary_observability_check(MeasuredValues<sym> con
155155
throw NotObservableError{};
156156
}
157157

158+
if (measured_values.has_global_angle_current() && n_voltage_phasor_sensor == 0) {
159+
throw NotObservableError{
160+
"Global angle current sensors require at least one voltage angle measurement as a reference point.\n"};
161+
}
162+
158163
// for radial grid without phasor measurement, try to assign injection sensor to branch sensor
159164
// we can then check sufficient condition for observability
160165
if (topo.is_radial && n_voltage_phasor_sensor == 0) {
@@ -163,8 +168,9 @@ inline ObservabilityResult necessary_observability_check(MeasuredValues<sym> con
163168
if (Idx const n_flow_sensor_new =
164169
std::reduce(flow_sensors.cbegin(), flow_sensors.cend(), Idx{}, std::plus<Idx>{});
165170
n_flow_sensor_new < n_bus - 1) {
166-
throw NotObservableError{"The number of power sensors appears sufficient, but they are not independent "
167-
"enough. The system is still not observable.\n"};
171+
throw NotObservableError{
172+
"The number of power/current sensors appears sufficient, but they are not independent "
173+
"enough. The system is still not observable.\n"};
168174
}
169175
result.is_sufficiently_observable = true;
170176
}

0 commit comments

Comments
 (0)