Skip to content

Commit 5d38475

Browse files
authored
Merge pull request #947 from PowerGridModel/feature/current-sensor-measurements-observability
Current sensor: statistics + observability
2 parents dd0beb9 + f09bacc commit 5d38475

File tree

16 files changed

+1004
-134
lines changed

16 files changed

+1004
-134
lines changed

power_grid_model_c/power_grid_model/include/power_grid_model/calculation_parameters.hpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,10 @@ struct MathModelTopology {
182182

183183
Idx n_bus_power_sensor() const { return power_sensors_per_bus.element_size(); }
184184

185+
Idx n_branch_from_current_sensor() const { return current_sensors_per_branch_from.element_size(); }
186+
187+
Idx n_branch_to_current_sensor() const { return current_sensors_per_branch_to.element_size(); }
188+
185189
Idx n_transformer_tap_regulator() const { return tap_regulators_per_branch.element_size(); }
186190
};
187191

@@ -235,6 +239,8 @@ template <symmetry_tag sym_type> struct StateEstimationInput {
235239
std::vector<PowerSensorCalcParam<sym>> measured_branch_from_power;
236240
std::vector<PowerSensorCalcParam<sym>> measured_branch_to_power;
237241
std::vector<PowerSensorCalcParam<sym>> measured_bus_injection;
242+
std::vector<CurrentSensorCalcParam<sym>> measured_branch_from_current;
243+
std::vector<CurrentSensorCalcParam<sym>> measured_branch_to_current;
238244
};
239245

240246
struct ShortCircuitInput {

power_grid_model_c/power_grid_model/include/power_grid_model/common/exception.hpp

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,13 @@ class InvalidArguments : public PowerGridError {
3939
};
4040

4141
template <std::same_as<TypeValuePair>... Options>
42-
InvalidArguments(std::string const& method, std::string const& arguments) {
42+
InvalidArguments(std::string_view method, std::string_view arguments) {
4343
append_msg(std::format("{} is not implemented for {}!\n", method, arguments));
4444
}
4545

4646
template <class... Options>
4747
requires(std::same_as<std::remove_cvref_t<Options>, TypeValuePair> && ...)
48-
InvalidArguments(std::string const& method, Options const&... options)
48+
InvalidArguments(std::string_view method, Options const&... options)
4949
: InvalidArguments{method, "the following combination of options"} {
5050
(append_msg(std::format(" {}: {}\n", options.name, options.value)), ...);
5151
}
@@ -54,7 +54,7 @@ class InvalidArguments : public PowerGridError {
5454
class MissingCaseForEnumError : public InvalidArguments {
5555
public:
5656
template <typename T>
57-
MissingCaseForEnumError(std::string const& method, T const& value)
57+
MissingCaseForEnumError(std::string_view method, T const& value)
5858
: InvalidArguments{method,
5959
std::format("{} #{}", typeid(T).name(), detail::to_string(static_cast<IntS>(value)))} {}
6060
};
@@ -97,7 +97,7 @@ class InvalidTransformerClock : public PowerGridError {
9797

9898
class SparseMatrixError : public PowerGridError {
9999
public:
100-
SparseMatrixError(Idx err, std::string const& msg = "") {
100+
SparseMatrixError(Idx err, std::string_view msg = "") {
101101
append_msg(
102102
std::format("Sparse matrix error with error code #{} (possibly singular)\n", detail::to_string(err)));
103103
if (!msg.empty()) {
@@ -117,7 +117,7 @@ class SparseMatrixError : public PowerGridError {
117117

118118
class NotObservableError : public PowerGridError {
119119
public:
120-
NotObservableError(std::string const& msg = "") {
120+
NotObservableError(std::string_view msg = "") {
121121
append_msg("Not enough measurements available for state estimation.\n");
122122
if (!msg.empty()) {
123123
append_msg(std::format("{}\n", msg));
@@ -137,7 +137,7 @@ class IterationDiverge : public PowerGridError {
137137

138138
class MaxIterationReached : public IterationDiverge {
139139
public:
140-
MaxIterationReached(std::string const& msg = "") {
140+
MaxIterationReached(std::string_view msg = "") {
141141
append_msg(std::format("Maximum number of iterations reached{}\n", msg));
142142
}
143143
};
@@ -161,25 +161,25 @@ class Idx2DNotFound : public PowerGridError {
161161

162162
class InvalidMeasuredObject : public PowerGridError {
163163
public:
164-
InvalidMeasuredObject(std::string const& object, std::string const& sensor) {
164+
InvalidMeasuredObject(std::string_view object, std::string_view sensor) {
165165
append_msg(std::format("{} measurement is not supported for object of type {}", sensor, object));
166166
}
167167
};
168168

169169
class InvalidMeasuredTerminalType : public PowerGridError {
170170
public:
171-
InvalidMeasuredTerminalType(MeasuredTerminalType const terminal_type, std::string const& sensor) {
171+
InvalidMeasuredTerminalType(MeasuredTerminalType const terminal_type, std::string_view sensor) {
172172
append_msg(std::format("{} measurement is not supported for object of type {}", sensor,
173173
detail::to_string(static_cast<IntS>(terminal_type))));
174174
}
175175
};
176176

177177
class InvalidRegulatedObject : public PowerGridError {
178178
public:
179-
InvalidRegulatedObject(std::string const& object, std::string const& regulator) {
179+
InvalidRegulatedObject(std::string_view object, std::string_view regulator) {
180180
append_msg(std::format("{} regulator is not supported for object of type {}", regulator, object));
181181
}
182-
InvalidRegulatedObject(ID id, std::string const& regulator) {
182+
InvalidRegulatedObject(ID id, std::string_view regulator) {
183183
append_msg(
184184
std::format("{} regulator is not supported for object with ID {}", regulator, detail::to_string(id)));
185185
}
@@ -203,7 +203,7 @@ class AutomaticTapCalculationError : public PowerGridError {
203203

204204
class AutomaticTapInputError : public PowerGridError {
205205
public:
206-
AutomaticTapInputError(std::string const& msg) {
206+
AutomaticTapInputError(std::string_view msg) {
207207
append_msg(std::format("Automatic tap changer has invalid configuration. {}", msg));
208208
}
209209
};
@@ -215,14 +215,21 @@ class IDWrongType : public PowerGridError {
215215
}
216216
};
217217

218+
class ConflictingAngleMeasurementType : public PowerGridError {
219+
public:
220+
ConflictingAngleMeasurementType(std::string_view msg) {
221+
append_msg(std::format("Conflicting angle measurement type. {}", msg));
222+
}
223+
};
224+
218225
class CalculationError : public PowerGridError {
219226
public:
220-
explicit CalculationError(std::string const& msg) { append_msg(msg); }
227+
explicit CalculationError(std::string_view msg) { append_msg(msg); }
221228
};
222229

223230
class BatchCalculationError : public CalculationError {
224231
public:
225-
BatchCalculationError(std::string const& msg, IdxVector failed_scenarios, std::vector<std::string> err_msgs)
232+
BatchCalculationError(std::string_view msg, IdxVector failed_scenarios, std::vector<std::string> err_msgs)
226233
: CalculationError(msg), failed_scenarios_{std::move(failed_scenarios)}, err_msgs_(std::move(err_msgs)) {}
227234

228235
IdxVector const& failed_scenarios() const { return failed_scenarios_; }
@@ -270,12 +277,12 @@ class InvalidShortCircuitPhaseOrType : public PowerGridError {
270277

271278
class SerializationError : public PowerGridError {
272279
public:
273-
explicit SerializationError(std::string const& msg) { append_msg(msg); }
280+
explicit SerializationError(std::string_view msg) { append_msg(msg); }
274281
};
275282

276283
class DatasetError : public PowerGridError {
277284
public:
278-
explicit DatasetError(std::string const& msg) { append_msg(std::format("Dataset error: {}", msg)); }
285+
explicit DatasetError(std::string_view msg) { append_msg(std::format("Dataset error: {}", msg)); }
279286
};
280287

281288
class ExperimentalFeature : public InvalidArguments {
@@ -289,7 +296,7 @@ class NotImplementedError : public PowerGridError {
289296

290297
class UnreachableHit : public PowerGridError {
291298
public:
292-
UnreachableHit(std::string const& method, std::string const& reason_for_assumption) {
299+
UnreachableHit(std::string_view method, std::string_view reason_for_assumption) {
293300
append_msg(std::format("Unreachable code hit when executing {}.\n The following assumption for unreachability "
294301
"was not met: {}.\n This may be a bug in the library\n",
295302
method, reason_for_assumption));
@@ -299,7 +306,7 @@ class UnreachableHit : public PowerGridError {
299306
class TapSearchStrategyIncompatibleError : public InvalidArguments {
300307
public:
301308
template <typename T1, typename T2>
302-
TapSearchStrategyIncompatibleError(std::string const& method, T1 const& value1, T2 const& value2)
309+
TapSearchStrategyIncompatibleError(std::string_view method, T1 const& value1, T2 const& value2)
303310
: InvalidArguments{method, std::format("{} #{} and {} #{}", typeid(T1).name(),
304311
detail::to_string(static_cast<IntS>(value1)), typeid(T2).name(),
305312
detail::to_string(static_cast<IntS>(value2)))} {}

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

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
#include "common.hpp"
88
#include "three_phase_tensor.hpp"
99

10+
#include <ranges>
11+
1012
/**
1113
* @file statistics.hpp
1214
* @brief This file contains various structures and functions for handling statistical representations of
@@ -204,4 +206,87 @@ template <symmetry_tag sym_type> struct PolarComplexRandVar {
204206
9.0}};
205207
}
206208
};
209+
210+
namespace statistics {
211+
// combine multiple random variables of one quantity using Kalman filter
212+
template <std::ranges::view RandVarsView>
213+
requires std::same_as<std::ranges::range_value_t<RandVarsView>,
214+
UniformRealRandVar<typename std::ranges::range_value_t<RandVarsView>::sym>> ||
215+
std::same_as<std::ranges::range_value_t<RandVarsView>,
216+
IndependentRealRandVar<typename std::ranges::range_value_t<RandVarsView>::sym>> ||
217+
std::same_as<std::ranges::range_value_t<RandVarsView>,
218+
UniformComplexRandVar<typename std::ranges::range_value_t<RandVarsView>::sym>> ||
219+
std::same_as<std::ranges::range_value_t<RandVarsView>,
220+
IndependentComplexRandVar<typename std::ranges::range_value_t<RandVarsView>::sym>>
221+
constexpr auto combine(RandVarsView rand_vars) {
222+
using RandVarType = std::ranges::range_value_t<RandVarsView>;
223+
using ValueType = decltype(RandVarType::value);
224+
using VarianceType = decltype(RandVarType::variance);
225+
226+
VarianceType accumulated_inverse_variance{};
227+
ValueType weighted_accumulated_value{};
228+
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+
});
234+
235+
if (!is_normal(accumulated_inverse_variance)) {
236+
return RandVarType{.value = weighted_accumulated_value,
237+
.variance = VarianceType{std::numeric_limits<double>::infinity()}};
238+
}
239+
return RandVarType{.value = weighted_accumulated_value / accumulated_inverse_variance,
240+
.variance = VarianceType{1.0} / accumulated_inverse_variance};
241+
}
242+
243+
template <std::ranges::view RandVarsView>
244+
requires std::same_as<std::ranges::range_value_t<RandVarsView>,
245+
DecomposedComplexRandVar<typename std::ranges::range_value_t<RandVarsView>::sym>>
246+
constexpr auto combine(RandVarsView rand_vars) {
247+
using sym = std::ranges::range_value_t<RandVarsView>::sym;
248+
249+
DecomposedComplexRandVar<sym> result{
250+
.real_component =
251+
combine(rand_vars | std::views::transform([](auto const& x) -> auto& { return x.real_component; })),
252+
.imag_component =
253+
combine(rand_vars | std::views::transform([](auto const& x) -> auto& { return x.imag_component; }))};
254+
255+
if (!(is_normal(result.real_component.variance) && is_normal(result.imag_component.variance))) {
256+
result.real_component.variance = RealValue<sym>{std::numeric_limits<double>::infinity()};
257+
result.imag_component.variance = RealValue<sym>{std::numeric_limits<double>::infinity()};
258+
}
259+
return result;
260+
}
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+
}
291+
} // namespace statistics
207292
} // namespace power_grid_model

power_grid_model_c/power_grid_model/include/power_grid_model/main_model_impl.hpp

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1171,6 +1171,8 @@ class MainModelImpl<ExtraRetrievableTypes<ExtraRetrievableType...>, ComponentLis
11711171
se_input[i].measured_branch_from_power.resize(state.math_topology[i]->n_branch_from_power_sensor());
11721172
se_input[i].measured_branch_to_power.resize(state.math_topology[i]->n_branch_to_power_sensor());
11731173
se_input[i].measured_bus_injection.resize(state.math_topology[i]->n_bus_power_sensor());
1174+
se_input[i].measured_branch_from_current.resize(state.math_topology[i]->n_branch_from_current_sensor());
1175+
se_input[i].measured_branch_to_current.resize(state.math_topology[i]->n_branch_to_current_sensor());
11741176
}
11751177

11761178
prepare_input_status<sym, &StateEstimationInput<sym>::shunt_status, Shunt>(state, state.topo_comp_coup->shunt,
@@ -1217,6 +1219,22 @@ class MainModelImpl<ExtraRetrievableTypes<ExtraRetrievableType...>, ComponentLis
12171219
state, state.topo_comp_coup->power_sensor, se_input,
12181220
[&state](Idx i) { return state.comp_topo->power_sensor_terminal_type[i] == MeasuredTerminalType::node; });
12191221

1222+
prepare_input<StateEstimationInput<sym>, CurrentSensorCalcParam<sym>,
1223+
&StateEstimationInput<sym>::measured_branch_from_current, GenericCurrentSensor>(
1224+
state, state.topo_comp_coup->current_sensor, se_input, [&state](Idx i) {
1225+
using enum MeasuredTerminalType;
1226+
return state.comp_topo->current_sensor_terminal_type[i] == branch_from ||
1227+
// all branch3 sensors are at from side in the mathematical model
1228+
state.comp_topo->current_sensor_terminal_type[i] == branch3_1 ||
1229+
state.comp_topo->current_sensor_terminal_type[i] == branch3_2 ||
1230+
state.comp_topo->current_sensor_terminal_type[i] == branch3_3;
1231+
});
1232+
prepare_input<StateEstimationInput<sym>, CurrentSensorCalcParam<sym>,
1233+
&StateEstimationInput<sym>::measured_branch_to_current, GenericCurrentSensor>(
1234+
state, state.topo_comp_coup->current_sensor, se_input, [&state](Idx i) {
1235+
return state.comp_topo->current_sensor_terminal_type[i] == MeasuredTerminalType::branch_to;
1236+
});
1237+
12201238
return se_input;
12211239
}
12221240

power_grid_model_c/power_grid_model/include/power_grid_model/math_solver/iterative_linear_se_solver.hpp

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,8 @@ template <symmetry_tag sym_type> class IterativeLinearSESolver {
142142

143143
private:
144144
// array selection function pointer
145-
static constexpr std::array has_branch_{&MeasuredValues<sym>::has_branch_from, &MeasuredValues<sym>::has_branch_to};
145+
static constexpr std::array has_branch_power_{&MeasuredValues<sym>::has_branch_from_power,
146+
&MeasuredValues<sym>::has_branch_to_power};
146147
static constexpr std::array branch_power_{&MeasuredValues<sym>::branch_from_power,
147148
&MeasuredValues<sym>::branch_to_power};
148149

@@ -210,7 +211,7 @@ template <symmetry_tag sym_type> class IterativeLinearSESolver {
210211
// measured at from-side: 0, to-side: 1
211212
for (IntS const measured_side : std::array<IntS, 2>{0, 1}) {
212213
// has measurement
213-
if (std::invoke(has_branch_[measured_side], measured_value, obj)) {
214+
if (std::invoke(has_branch_power_[measured_side], measured_value, obj)) {
214215
// G += Y{side, b0}^H * (variance^-1) * Y{side, b1}
215216
auto const& power = std::invoke(branch_power_[measured_side], measured_value, obj);
216217
block.g() +=
@@ -301,7 +302,7 @@ template <symmetry_tag sym_type> class IterativeLinearSESolver {
301302
// measured at from-side: 0, to-side: 1
302303
for (IntS const measured_side : std::array<IntS, 2>{0, 1}) {
303304
// has measurement
304-
if (std::invoke(has_branch_[measured_side], measured_value, obj)) {
305+
if (std::invoke(has_branch_power_[measured_side], measured_value, obj)) {
305306
PowerSensorCalcParam<sym> const& m =
306307
std::invoke(branch_power_[measured_side], measured_value, obj);
307308
// the current needs to be calculated with the voltage of the measured bus side

0 commit comments

Comments
 (0)