Skip to content

Current sensor: statistics + observability #947

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 28 commits into from
Apr 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
32d404e
add basic current measurement support to measured values
mgovers Apr 1, 2025
968786d
separate accumulation for real and imag part
mgovers Apr 1, 2025
19b6feb
minor
mgovers Apr 1, 2025
086403d
minor
mgovers Apr 3, 2025
4af2e06
combine measurements in statistics.hpp
mgovers Apr 3, 2025
286dc27
measured values basic support for local current measurement
mgovers Apr 3, 2025
07fb8ad
Merge remote-tracking branch 'origin/feature/rename-angle-measurement…
mgovers Apr 4, 2025
b14b5c1
local_angle, global_angle + add error handling current sensor
mgovers Apr 7, 2025
2ed562c
test statistics combine measurements
mgovers Apr 7, 2025
0dea471
measured values unit tests
mgovers Apr 7, 2025
61781e8
make gcc happy
mgovers Apr 8, 2025
ca96b98
Merge branch 'main' into feature/current-sensor-measurements-observab…
mgovers Apr 8, 2025
fb66064
well this is why you need testing
mgovers Apr 8, 2025
3268e6a
do not read out of bounds (thank you tests)
mgovers Apr 8, 2025
8cc1bf2
actually throw sparse matrix error
mgovers Apr 8, 2025
dd9233b
fix python test
mgovers Apr 8, 2025
2f9dfe4
fix sonar cloud
mgovers Apr 9, 2025
888271a
make missing voltage angle not observable
mgovers Apr 10, 2025
0aae93c
move global angle current sensor with voltage angle to observability
mgovers Apr 10, 2025
d65427d
fix test
mgovers Apr 10, 2025
2f3dae0
sonar cloud
mgovers Apr 10, 2025
7e5fc8e
resolve some comments
mgovers Apr 10, 2025
f6a400d
add asym tests for measured values
mgovers Apr 11, 2025
1953c85
resolve comments
mgovers Apr 14, 2025
04e1273
also migrate combine_magnitude to statistics
mgovers Apr 14, 2025
51d0e8e
fix tests
mgovers Apr 14, 2025
5b6113f
assertion in combine magnitude causes crashes in first batch validati…
mgovers Apr 14, 2025
f09bacc
Merge branch 'main' into feature/current-sensor-measurements-observab…
mgovers Apr 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,10 @@ struct MathModelTopology {

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

Idx n_branch_from_current_sensor() const { return current_sensors_per_branch_from.element_size(); }

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(); }
};

Expand Down Expand Up @@ -235,6 +239,8 @@ template <symmetry_tag sym_type> struct StateEstimationInput {
std::vector<PowerSensorCalcParam<sym>> measured_branch_from_power;
std::vector<PowerSensorCalcParam<sym>> measured_branch_to_power;
std::vector<PowerSensorCalcParam<sym>> measured_bus_injection;
std::vector<CurrentSensorCalcParam<sym>> measured_branch_from_current;
std::vector<CurrentSensorCalcParam<sym>> measured_branch_to_current;
};

struct ShortCircuitInput {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,13 @@ class InvalidArguments : public PowerGridError {
};

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

template <class... Options>
requires(std::same_as<std::remove_cvref_t<Options>, TypeValuePair> && ...)
InvalidArguments(std::string const& method, Options const&... options)
InvalidArguments(std::string_view method, Options const&... options)
: InvalidArguments{method, "the following combination of options"} {
(append_msg(std::format(" {}: {}\n", options.name, options.value)), ...);
}
Expand All @@ -54,7 +54,7 @@ class InvalidArguments : public PowerGridError {
class MissingCaseForEnumError : public InvalidArguments {
public:
template <typename T>
MissingCaseForEnumError(std::string const& method, T const& value)
MissingCaseForEnumError(std::string_view method, T const& value)
: InvalidArguments{method,
std::format("{} #{}", typeid(T).name(), detail::to_string(static_cast<IntS>(value)))} {}
};
Expand Down Expand Up @@ -97,7 +97,7 @@ class InvalidTransformerClock : public PowerGridError {

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

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

class MaxIterationReached : public IterationDiverge {
public:
MaxIterationReached(std::string const& msg = "") {
MaxIterationReached(std::string_view msg = "") {
append_msg(std::format("Maximum number of iterations reached{}\n", msg));
}
};
Expand All @@ -161,25 +161,25 @@ class Idx2DNotFound : public PowerGridError {

class InvalidMeasuredObject : public PowerGridError {
public:
InvalidMeasuredObject(std::string const& object, std::string const& sensor) {
InvalidMeasuredObject(std::string_view object, std::string_view sensor) {
append_msg(std::format("{} measurement is not supported for object of type {}", sensor, object));
}
};

class InvalidMeasuredTerminalType : public PowerGridError {
public:
InvalidMeasuredTerminalType(MeasuredTerminalType const terminal_type, std::string const& sensor) {
InvalidMeasuredTerminalType(MeasuredTerminalType const terminal_type, std::string_view sensor) {
append_msg(std::format("{} measurement is not supported for object of type {}", sensor,
detail::to_string(static_cast<IntS>(terminal_type))));
}
};

class InvalidRegulatedObject : public PowerGridError {
public:
InvalidRegulatedObject(std::string const& object, std::string const& regulator) {
InvalidRegulatedObject(std::string_view object, std::string_view regulator) {
append_msg(std::format("{} regulator is not supported for object of type {}", regulator, object));
}
InvalidRegulatedObject(ID id, std::string const& regulator) {
InvalidRegulatedObject(ID id, std::string_view regulator) {
append_msg(
std::format("{} regulator is not supported for object with ID {}", regulator, detail::to_string(id)));
}
Expand All @@ -203,7 +203,7 @@ class AutomaticTapCalculationError : public PowerGridError {

class AutomaticTapInputError : public PowerGridError {
public:
AutomaticTapInputError(std::string const& msg) {
AutomaticTapInputError(std::string_view msg) {
append_msg(std::format("Automatic tap changer has invalid configuration. {}", msg));
}
};
Expand All @@ -215,14 +215,21 @@ class IDWrongType : public PowerGridError {
}
};

class ConflictingAngleMeasurementType : public PowerGridError {
public:
ConflictingAngleMeasurementType(std::string_view msg) {
append_msg(std::format("Conflicting angle measurement type. {}", msg));
}
};

class CalculationError : public PowerGridError {
public:
explicit CalculationError(std::string const& msg) { append_msg(msg); }
explicit CalculationError(std::string_view msg) { append_msg(msg); }
};

class BatchCalculationError : public CalculationError {
public:
BatchCalculationError(std::string const& msg, IdxVector failed_scenarios, std::vector<std::string> err_msgs)
BatchCalculationError(std::string_view msg, IdxVector failed_scenarios, std::vector<std::string> err_msgs)
: CalculationError(msg), failed_scenarios_{std::move(failed_scenarios)}, err_msgs_(std::move(err_msgs)) {}

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

class SerializationError : public PowerGridError {
public:
explicit SerializationError(std::string const& msg) { append_msg(msg); }
explicit SerializationError(std::string_view msg) { append_msg(msg); }
};

class DatasetError : public PowerGridError {
public:
explicit DatasetError(std::string const& msg) { append_msg(std::format("Dataset error: {}", msg)); }
explicit DatasetError(std::string_view msg) { append_msg(std::format("Dataset error: {}", msg)); }
};

class ExperimentalFeature : public InvalidArguments {
Expand All @@ -289,7 +296,7 @@ class NotImplementedError : public PowerGridError {

class UnreachableHit : public PowerGridError {
public:
UnreachableHit(std::string const& method, std::string const& reason_for_assumption) {
UnreachableHit(std::string_view method, std::string_view reason_for_assumption) {
append_msg(std::format("Unreachable code hit when executing {}.\n The following assumption for unreachability "
"was not met: {}.\n This may be a bug in the library\n",
method, reason_for_assumption));
Expand All @@ -299,7 +306,7 @@ class UnreachableHit : public PowerGridError {
class TapSearchStrategyIncompatibleError : public InvalidArguments {
public:
template <typename T1, typename T2>
TapSearchStrategyIncompatibleError(std::string const& method, T1 const& value1, T2 const& value2)
TapSearchStrategyIncompatibleError(std::string_view method, T1 const& value1, T2 const& value2)
: InvalidArguments{method, std::format("{} #{} and {} #{}", typeid(T1).name(),
detail::to_string(static_cast<IntS>(value1)), typeid(T2).name(),
detail::to_string(static_cast<IntS>(value2)))} {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
#include "common.hpp"
#include "three_phase_tensor.hpp"

#include <ranges>

/**
* @file statistics.hpp
* @brief This file contains various structures and functions for handling statistical representations of
Expand Down Expand Up @@ -204,4 +206,87 @@ template <symmetry_tag sym_type> struct PolarComplexRandVar {
9.0}};
}
};

namespace statistics {
// combine multiple random variables of one quantity using Kalman filter
template <std::ranges::view RandVarsView>
requires std::same_as<std::ranges::range_value_t<RandVarsView>,
UniformRealRandVar<typename std::ranges::range_value_t<RandVarsView>::sym>> ||
std::same_as<std::ranges::range_value_t<RandVarsView>,
IndependentRealRandVar<typename std::ranges::range_value_t<RandVarsView>::sym>> ||
std::same_as<std::ranges::range_value_t<RandVarsView>,
UniformComplexRandVar<typename std::ranges::range_value_t<RandVarsView>::sym>> ||
std::same_as<std::ranges::range_value_t<RandVarsView>,
IndependentComplexRandVar<typename std::ranges::range_value_t<RandVarsView>::sym>>
constexpr auto combine(RandVarsView rand_vars) {
using RandVarType = std::ranges::range_value_t<RandVarsView>;
using ValueType = decltype(RandVarType::value);
using VarianceType = decltype(RandVarType::variance);

VarianceType accumulated_inverse_variance{};
ValueType weighted_accumulated_value{};

std::ranges::for_each(rand_vars,
[&accumulated_inverse_variance, &weighted_accumulated_value](auto const& measurement) {
accumulated_inverse_variance += VarianceType{1.0} / measurement.variance;
weighted_accumulated_value += measurement.value / measurement.variance;
});

if (!is_normal(accumulated_inverse_variance)) {
return RandVarType{.value = weighted_accumulated_value,
.variance = VarianceType{std::numeric_limits<double>::infinity()}};
}
return RandVarType{.value = weighted_accumulated_value / accumulated_inverse_variance,
.variance = VarianceType{1.0} / accumulated_inverse_variance};
}

template <std::ranges::view RandVarsView>
requires std::same_as<std::ranges::range_value_t<RandVarsView>,
DecomposedComplexRandVar<typename std::ranges::range_value_t<RandVarsView>::sym>>
constexpr auto combine(RandVarsView rand_vars) {
using sym = std::ranges::range_value_t<RandVarsView>::sym;

DecomposedComplexRandVar<sym> result{
.real_component =
combine(rand_vars | std::views::transform([](auto const& x) -> auto& { return x.real_component; })),
.imag_component =
combine(rand_vars | std::views::transform([](auto const& x) -> auto& { return x.imag_component; }))};

if (!(is_normal(result.real_component.variance) && is_normal(result.imag_component.variance))) {
result.real_component.variance = RealValue<sym>{std::numeric_limits<double>::infinity()};
result.imag_component.variance = RealValue<sym>{std::numeric_limits<double>::infinity()};
}
return result;
}

namespace detail {
template <symmetry_tag sym> inline RealValue<sym> cabs_or_real(ComplexValue<sym> const& value) {
if (is_nan(imag(value))) {
return real(value); // only keep real part
}
return cabs(value); // get abs of the value
}
} // namespace detail

template <std::ranges::view RandVarsView>
requires std::same_as<std::ranges::range_value_t<RandVarsView>,
UniformComplexRandVar<typename std::ranges::range_value_t<RandVarsView>::sym>>
constexpr auto combine_magnitude(RandVarsView rand_vars) {
using sym = std::ranges::range_value_t<RandVarsView>::sym;

auto const weighted_average_magnitude_measurement =
statistics::combine(rand_vars | std::views::transform([](auto const& measurement) {
return UniformRealRandVar<sym>{.value = detail::cabs_or_real<sym>(measurement.value),
.variance = measurement.variance};
}));
return UniformComplexRandVar<sym>{.value =
[&weighted_average_magnitude_measurement]() {
ComplexValue<sym> abs_value =
piecewise_complex_value<sym>(DoubleComplex{0.0, nan});
abs_value += weighted_average_magnitude_measurement.value;
return abs_value;
}(),
.variance = weighted_average_magnitude_measurement.variance};
}
} // namespace statistics
} // namespace power_grid_model
Original file line number Diff line number Diff line change
Expand Up @@ -1171,6 +1171,8 @@ class MainModelImpl<ExtraRetrievableTypes<ExtraRetrievableType...>, ComponentLis
se_input[i].measured_branch_from_power.resize(state.math_topology[i]->n_branch_from_power_sensor());
se_input[i].measured_branch_to_power.resize(state.math_topology[i]->n_branch_to_power_sensor());
se_input[i].measured_bus_injection.resize(state.math_topology[i]->n_bus_power_sensor());
se_input[i].measured_branch_from_current.resize(state.math_topology[i]->n_branch_from_current_sensor());
se_input[i].measured_branch_to_current.resize(state.math_topology[i]->n_branch_to_current_sensor());
}

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

prepare_input<StateEstimationInput<sym>, CurrentSensorCalcParam<sym>,
&StateEstimationInput<sym>::measured_branch_from_current, GenericCurrentSensor>(
state, state.topo_comp_coup->current_sensor, se_input, [&state](Idx i) {
using enum MeasuredTerminalType;
return state.comp_topo->current_sensor_terminal_type[i] == branch_from ||
// all branch3 sensors are at from side in the mathematical model
state.comp_topo->current_sensor_terminal_type[i] == branch3_1 ||
state.comp_topo->current_sensor_terminal_type[i] == branch3_2 ||
state.comp_topo->current_sensor_terminal_type[i] == branch3_3;
});
prepare_input<StateEstimationInput<sym>, CurrentSensorCalcParam<sym>,
&StateEstimationInput<sym>::measured_branch_to_current, GenericCurrentSensor>(
state, state.topo_comp_coup->current_sensor, se_input, [&state](Idx i) {
return state.comp_topo->current_sensor_terminal_type[i] == MeasuredTerminalType::branch_to;
});

return se_input;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,8 @@ template <symmetry_tag sym_type> class IterativeLinearSESolver {

private:
// array selection function pointer
static constexpr std::array has_branch_{&MeasuredValues<sym>::has_branch_from, &MeasuredValues<sym>::has_branch_to};
static constexpr std::array has_branch_power_{&MeasuredValues<sym>::has_branch_from_power,
&MeasuredValues<sym>::has_branch_to_power};
static constexpr std::array branch_power_{&MeasuredValues<sym>::branch_from_power,
&MeasuredValues<sym>::branch_to_power};

Expand Down Expand Up @@ -210,7 +211,7 @@ template <symmetry_tag sym_type> class IterativeLinearSESolver {
// measured at from-side: 0, to-side: 1
for (IntS const measured_side : std::array<IntS, 2>{0, 1}) {
// has measurement
if (std::invoke(has_branch_[measured_side], measured_value, obj)) {
if (std::invoke(has_branch_power_[measured_side], measured_value, obj)) {
// G += Y{side, b0}^H * (variance^-1) * Y{side, b1}
auto const& power = std::invoke(branch_power_[measured_side], measured_value, obj);
block.g() +=
Expand Down Expand Up @@ -301,7 +302,7 @@ template <symmetry_tag sym_type> class IterativeLinearSESolver {
// measured at from-side: 0, to-side: 1
for (IntS const measured_side : std::array<IntS, 2>{0, 1}) {
// has measurement
if (std::invoke(has_branch_[measured_side], measured_value, obj)) {
if (std::invoke(has_branch_power_[measured_side], measured_value, obj)) {
PowerSensorCalcParam<sym> const& m =
std::invoke(branch_power_[measured_side], measured_value, obj);
// the current needs to be calculated with the voltage of the measured bus side
Expand Down
Loading
Loading