Skip to content

Commit e3d9c4f

Browse files
Merge pull request #862 from PowerGridModel/experimental/pivot-perturbation
Using pivot perturbation for ill-conditioned state estimation
2 parents 9fc25ef + 5fb5aa0 commit e3d9c4f

File tree

123 files changed

+1869
-108
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

123 files changed

+1869
-108
lines changed

.clang-tidy

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ readability-*,
5555
-readability-function-cognitive-complexity,
5656
-readability-identifier-length,
5757
-readability-magic-numbers,
58+
-bugprone-unchecked-optional-access,
5859
'
5960

6061
WarningsAsErrors: '*'

CMakeLists.txt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,11 @@ set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
2929

3030
include(GNUInstallDirs)
3131

32-
cmake_policy(SET CMP0167 OLD) # libboost-headers packaged by conda does not come with BoostConfig.cmake
32+
if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.30.0")
33+
# libboost-headers packaged by conda does not come with BoostConfig.cmake
34+
cmake_policy(SET CMP0167 OLD)
35+
endif()
36+
3337
find_package(Boost REQUIRED)
3438
find_package(Eigen3 CONFIG REQUIRED)
3539
find_package(nlohmann_json CONFIG REQUIRED)

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ class SparseMatrixError : public PowerGridError {
108108
std::string("If you get this error from state estimation, ") +
109109
"it might mean the system is not fully observable, i.e. not enough measurements.\n" +
110110
"It might also mean that you are running into a corner case where PGM cannot resolve yet." +
111-
"See https://github.com/PowerGridModel/power-grid-model/issues/853.");
111+
"See https://github.com/PowerGridModel/power-grid-model/issues/864.");
112112
}
113113
};
114114

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,9 +152,18 @@ inline ComplexValue<asymmetric_t> piecewise_complex_value(ComplexValue<asymmetri
152152
inline double cabs(double x) { return std::abs(x); }
153153
inline double cabs(DoubleComplex const& x) { return std::sqrt(std::norm(x)); }
154154
inline double abs2(DoubleComplex const& x) { return std::norm(x); }
155-
template <column_vector_or_tensor DerivedA> inline auto cabs(Eigen::ArrayBase<DerivedA> const& m) {
155+
template <column_vector_or_tensor DerivedA>
156+
inline auto cabs(Eigen::ArrayBase<DerivedA> const& m)
157+
requires(std::same_as<typename DerivedA::Scalar, DoubleComplex>)
158+
{
156159
return sqrt(abs2(m));
157160
}
161+
template <column_vector_or_tensor DerivedA>
162+
inline auto cabs(Eigen::ArrayBase<DerivedA> const& m)
163+
requires(std::same_as<typename DerivedA::Scalar, double>)
164+
{
165+
return m.abs();
166+
}
158167

159168
// phase_shift(x) = e^{i arg(x)} = x / |x|
160169
inline DoubleComplex phase_shift(DoubleComplex const x) {

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,11 +95,14 @@ template <symmetry_tag sym_type> class IterativeLinearSESolver {
9595
// preprocess measured value
9696
sub_timer = Timer(calculation_info, 2221, "Pre-process measured value");
9797
MeasuredValues<sym> const measured_values{y_bus.shared_topology(), input};
98-
necessary_observability_check(measured_values, y_bus.math_topology(), y_bus.y_bus_structure());
98+
auto const observability_result =
99+
necessary_observability_check(measured_values, y_bus.math_topology(), y_bus.y_bus_structure());
99100

100-
// prepare matrix, including pre-factorization
101+
// prepare matrix
101102
sub_timer = Timer(calculation_info, 2222, "Prepare matrix, including pre-factorization");
102103
prepare_matrix(y_bus, measured_values);
104+
// prefactorize
105+
sparse_solver_.prefactorize(data_gain_, perm_, observability_result.use_perturbation());
103106

104107
// initialize voltage with initial angle
105108
sub_timer = Timer(calculation_info, 2223, "Initialize voltages");
@@ -252,8 +255,6 @@ template <symmetry_tag sym_type> class IterativeLinearSESolver {
252255
Idx const data_idx_tranpose = y_bus.lu_transpose_entry()[data_idx_lu];
253256
data_gain_[data_idx_lu].qh() = hermitian_transpose(data_gain_[data_idx_tranpose].q());
254257
}
255-
// prefactorize
256-
sparse_solver_.prefactorize(data_gain_, perm_);
257258
}
258259

259260
void prepare_rhs(YBus<sym> const& y_bus, MeasuredValues<sym> const& measured_value,

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

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,8 @@ template <symmetry_tag sym_type> class NewtonRaphsonSESolver {
159159
// preprocess measured value
160160
sub_timer = Timer(calculation_info, 2221, "Pre-process measured value");
161161
MeasuredValues<sym> const measured_values{y_bus.shared_topology(), input};
162-
necessary_observability_check(measured_values, y_bus.math_topology(), y_bus.y_bus_structure());
162+
auto const observability_result =
163+
necessary_observability_check(measured_values, y_bus.math_topology(), y_bus.y_bus_structure());
163164

164165
// initialize voltage with initial angle
165166
sub_timer = Timer(calculation_info, 2223, "Initialize voltages");
@@ -175,7 +176,8 @@ template <symmetry_tag sym_type> class NewtonRaphsonSESolver {
175176
prepare_matrix_and_rhs(y_bus, measured_values, output.u);
176177
// solve with prefactorization
177178
sub_timer = Timer(calculation_info, 2225, "Solve sparse linear equation");
178-
sparse_solver_.prefactorize_and_solve(data_gain_, perm_, delta_x_rhs_, delta_x_rhs_);
179+
sparse_solver_.prefactorize_and_solve(data_gain_, perm_, delta_x_rhs_, delta_x_rhs_,
180+
observability_result.use_perturbation());
179181
sub_timer = Timer(calculation_info, 2226, "Iterate unknown");
180182
max_dev = iterate_unknown(output.u, measured_values);
181183
};
@@ -519,7 +521,8 @@ template <symmetry_tag sym_type> class NewtonRaphsonSESolver {
519521
/// eta(row) += w_k . (z_k - f_k(x))
520522
///
521523
/// In case there is no angle measurement, the slack bus or arbitray bus measurement is considered to have a virtual
522-
/// angle measurement of zero. w_theta = 1.0 by default for all measurements
524+
/// angle measurement of zero. w_theta = w_k by default for all measurements
525+
/// angle_error = u_error / u_rated (1.0) = w_k
523526
///
524527
/// @param block LHS(row, col), ie. LHS(row, row)
525528
/// @param rhs_block RHS(row)
@@ -545,10 +548,10 @@ template <symmetry_tag sym_type> class NewtonRaphsonSESolver {
545548
RealValue<sym> delta_theta{};
546549
if (measured_values.has_angle_measurement(bus)) {
547550
delta_theta = RealValue<sym>{arg(measured_values.voltage(bus))} - RealValue<sym>{x_[bus].theta()};
548-
w_theta = RealTensor<sym>{1.0};
551+
w_theta = RealTensor<sym>{w_v};
549552
} else if (bus == virtual_angle_measurement_bus && !measured_values.has_angle()) {
550553
delta_theta = arg(ComplexValue<sym>{1.0}) - RealValue<sym>{x_[bus].theta()};
551-
w_theta = RealTensor<sym>{1.0};
554+
w_theta = RealTensor<sym>{w_v};
552555
}
553556

554557
block.g_P_theta() += w_theta;

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

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,16 +32,25 @@ std::tuple<Idx, Idx> count_voltage_sensors(const Idx n_bus, const MeasuredValues
3232
// lower triangle part is always zero
3333
// for diagonal part, it will be one if there is bus injection
3434
// for upper triangle part, it will be one if there is branch flow sensor and the branch is fully connected
35+
// return a pair of
36+
// a vector of flow sensor count
37+
// a boolean indicating if the system is possibly ill-conditioned
3538
template <symmetry_tag sym>
36-
std::vector<int8_t> count_flow_sensors(MeasuredValues<sym> const& measured_values, MathModelTopology const& topo,
37-
YBusStructure const& y_bus_structure) {
39+
std::pair<std::vector<int8_t>, bool> count_flow_sensors(MeasuredValues<sym> const& measured_values,
40+
MathModelTopology const& topo,
41+
YBusStructure const& y_bus_structure) {
3842
Idx const n_bus{topo.n_bus()};
39-
std::vector<int8_t> flow_sensors(y_bus_structure.row_indptr.back(), 0); // initialize all to zero
43+
std::pair<std::vector<int8_t>, bool> result{};
44+
auto& [flow_sensors, possibly_ill_conditioned] = result;
45+
flow_sensors.resize(y_bus_structure.row_indptr.back(), 0); // initialize all to zero
46+
possibly_ill_conditioned = false;
4047
for (Idx row = 0; row != n_bus; ++row) {
48+
bool has_at_least_one_sensor{false};
4149
// lower triangle is ignored and kept as zero
4250
// diagonal for bus injection measurement
4351
if (measured_values.has_bus_injection(row)) {
4452
flow_sensors[y_bus_structure.bus_entry[row]] = 1;
53+
has_at_least_one_sensor = true;
4554
}
4655
// upper triangle for branch flow measurement
4756
for (Idx ybus_index = y_bus_structure.bus_entry[row] + 1; ybus_index != y_bus_structure.row_indptr[row + 1];
@@ -57,13 +66,22 @@ std::vector<int8_t> count_flow_sensors(MeasuredValues<sym> const& measured_value
5766
if ((measured_values.has_branch_from(branch) || measured_values.has_branch_to(branch)) &&
5867
topo.branch_bus_idx[branch][0] != -1 && topo.branch_bus_idx[branch][1] != -1) {
5968
flow_sensors[ybus_index] = 1;
69+
has_at_least_one_sensor = true;
6070
break;
6171
}
6272
}
6373
}
6474
}
75+
// check voltage sensor
76+
if (measured_values.has_voltage(row) && measured_values.has_angle_measurement(row)) {
77+
has_at_least_one_sensor = true;
78+
}
79+
// the system could be ill-conditioned if there is no flow sensor for one bus, except the last bus
80+
if (!has_at_least_one_sensor && row != n_bus - 1) {
81+
possibly_ill_conditioned = true;
82+
}
6583
}
66-
return flow_sensors;
84+
return result;
6785
}
6886

6987
// re-organize the flow sensor for radial grid without phasor measurement
@@ -103,17 +121,27 @@ inline void assign_injection_sensor_radial(YBusStructure const& y_bus_structure,
103121

104122
} // namespace detail
105123

124+
struct ObservabilityResult {
125+
bool is_sufficiently_observable{false};
126+
bool is_possibly_ill_conditioned{false};
127+
constexpr bool use_perturbation() const { return is_possibly_ill_conditioned && is_sufficiently_observable; }
128+
};
129+
106130
template <symmetry_tag sym>
107-
inline void necessary_observability_check(MeasuredValues<sym> const& measured_values, MathModelTopology const& topo,
108-
YBusStructure const& y_bus_structure) {
131+
inline ObservabilityResult necessary_observability_check(MeasuredValues<sym> const& measured_values,
132+
MathModelTopology const& topo,
133+
YBusStructure const& y_bus_structure) {
109134
Idx const n_bus{topo.n_bus()};
135+
ObservabilityResult result{};
110136

111137
auto const [n_voltage_sensor, n_voltage_phasor_sensor] = detail::count_voltage_sensors(n_bus, measured_values);
112138
if (n_voltage_sensor < 1) {
113139
throw NotObservableError{"No voltage sensor found!\n"};
114140
}
115141

116-
std::vector<int8_t> flow_sensors = detail::count_flow_sensors(measured_values, topo, y_bus_structure);
142+
std::vector<int8_t> flow_sensors;
143+
std::tie(flow_sensors, result.is_possibly_ill_conditioned) =
144+
detail::count_flow_sensors(measured_values, topo, y_bus_structure);
117145
// count flow sensors, note we manually specify the intial value type to avoid overflow
118146
Idx const n_flow_sensor = std::reduce(flow_sensors.cbegin(), flow_sensors.cend(), Idx{}, std::plus<Idx>{});
119147

@@ -130,12 +158,16 @@ inline void necessary_observability_check(MeasuredValues<sym> const& measured_va
130158
if (topo.is_radial && n_voltage_phasor_sensor == 0) {
131159
detail::assign_injection_sensor_radial(y_bus_structure, flow_sensors);
132160
// count flow sensors again
133-
Idx const n_flow_sensor_new = std::reduce(flow_sensors.cbegin(), flow_sensors.cend(), Idx{}, std::plus<Idx>{});
134-
if (n_flow_sensor_new < n_bus - 1) {
161+
if (Idx const n_flow_sensor_new =
162+
std::reduce(flow_sensors.cbegin(), flow_sensors.cend(), Idx{}, std::plus<Idx>{});
163+
n_flow_sensor_new < n_bus - 1) {
135164
throw NotObservableError{"The number of power sensors appears sufficient, but they are not independent "
136165
"enough. The system is still not observable.\n"};
137166
}
167+
result.is_sufficiently_observable = true;
138168
}
169+
170+
return result;
139171
}
140172

141173
} // namespace power_grid_model::math_solver

0 commit comments

Comments
 (0)