1#![allow(clippy::needless_range_loop, clippy::explicit_iter_loop)]
17
18use sprs::CsMat;
19
20use crate::indexed::IndexedNetwork;
21use crate::matrix::incidence::{DcConvention, IncidenceParts, build_flow_map, build_incidence};
22use crate::matrix::laplacian::{Grounding, build_weighted_laplacian, ground_with};
23use crate::matrix::triplet::CooBuilder;
24use crate::{Error, Result};
25
26const PRUNE: f64 = 1e-12;
28
29pub fn build_ptdf(case: &IndexedNetwork, conv: DcConvention) -> Result<CsMat<f64>> {
35 case.check_reference_coverage()?;
36 let refs = case.reference_bus_indices();
37 let inc = build_incidence(case, conv)?;
38 let (dense, m, n) = ptdf_dense(&inc, &refs)?;
39 Ok(dense_to_csr(&dense, m, n))
40}
41
42pub fn build_lodf(case: &IndexedNetwork, conv: DcConvention) -> Result<CsMat<f64>> {
46 case.check_reference_coverage()?;
47 let refs = case.reference_bus_indices();
48 let inc = build_incidence(case, conv)?;
49 let (ptdf, m, n) = ptdf_dense(&inc, &refs)?;
50 Ok(lodf_from_dense(&ptdf, &inc.a, m, n))
51}
52
53pub fn build_ptdf_lodf(
59 case: &IndexedNetwork,
60 conv: DcConvention,
61) -> Result<(CsMat<f64>, CsMat<f64>)> {
62 case.check_reference_coverage()?;
63 let refs = case.reference_bus_indices();
64 let inc = build_incidence(case, conv)?;
65 let (dense, m, n) = ptdf_dense(&inc, &refs)?;
66 let ptdf = dense_to_csr(&dense, m, n);
67 let lodf = lodf_from_dense(&dense, &inc.a, m, n);
68 Ok((ptdf, lodf))
69}
70
71fn lodf_from_dense(ptdf: &[f64], a: &CsMat<f64>, m: usize, n: usize) -> CsMat<f64> {
74 let (from, to) = endpoints(a, m);
76
77 let delta = |l: usize, k: usize| ptdf[l * n + from[k]] - ptdf[l * n + to[k]];
80
81 let mut lodf = CooBuilder::new(m); for k in 0..m {
83 let denom = 1.0 - delta(k, k);
84 let islands = denom.abs() < 1e-9;
85 for l in 0..m {
86 let v = if l == k {
87 -1.0
88 } else if islands {
89 0.0
90 } else {
91 delta(l, k) / denom
92 };
93 if v.abs() > PRUNE {
94 lodf.add(l, k, v);
95 }
96 }
97 }
98 lodf.finish_csr()
99}
100
101fn ptdf_dense(inc: &IncidenceParts, refs: &[usize]) -> Result<(Vec<f64>, usize, usize)> {
104 let n = inc.n();
105 let m = inc.m();
106 let g = Grounding::new(refs);
107 let nr = n - g.len();
108
109 let lr = ground_with(&build_weighted_laplacian(&inc.a, &inc.b), &g);
111 let dense_lr = densify(&lr, nr);
112 let rinv = DenseCholesky::factor(&dense_lr, nr).map_or_else(
113 || dense_inverse(&dense_lr, nr).ok_or(Error::SingularNetwork),
114 |chol| Ok(chol.inverse()),
115 )?; let flow = build_flow_map(&inc.a, &inc.b); let mut ptdf = vec![0.0; m * n];
123 for (&w, (l, c)) in flow.iter() {
124 let Some(rc) = g.reduced(c) else { continue }; for k in 0..n {
126 if let Some(rk) = g.reduced(k) {
127 ptdf[l * n + k] += w * rinv[rc * nr + rk];
128 }
129 }
130 }
131 Ok((ptdf, m, n))
132}
133
134fn endpoints(a: &CsMat<f64>, m: usize) -> (Vec<usize>, Vec<usize>) {
136 let mut from = vec![0usize; m];
137 let mut to = vec![0usize; m];
138 for (&v, (bus, branch)) in a.iter() {
139 if v > 0.0 {
140 from[branch] = bus;
141 } else {
142 to[branch] = bus;
143 }
144 }
145 (from, to)
146}
147
148fn densify(a: &CsMat<f64>, n: usize) -> Vec<f64> {
149 let mut d = vec![0.0; n * n];
150 for (&v, (i, j)) in a.iter() {
151 d[i * n + j] = v;
152 }
153 d
154}
155
156fn dense_to_csr(dense: &[f64], rows: usize, cols: usize) -> CsMat<f64> {
157 let mut coo = CooBuilder::with_capacity_rect(rows, cols, dense.len() / 2);
158 for i in 0..rows {
159 for j in 0..cols {
160 let v = dense[i * cols + j];
161 if v.abs() > PRUNE {
162 coo.add(i, j, v);
163 }
164 }
165 }
166 coo.finish_csr()
167}
168
169fn dense_inverse(a: &[f64], n: usize) -> Option<Vec<f64>> {
170 let mut a = a.to_vec();
171 let mut inv = vec![0.0; n * n];
172 for i in 0..n {
173 inv[i * n + i] = 1.0;
174 }
175
176 for col in 0..n {
177 let mut pivot_row = col;
178 let mut pivot_abs = a[col * n + col].abs();
179 for r in (col + 1)..n {
180 let v = a[r * n + col].abs();
181 if v > pivot_abs {
182 pivot_abs = v;
183 pivot_row = r;
184 }
185 }
186 if !pivot_abs.is_finite() || pivot_abs <= 1e-12 {
187 return None;
188 }
189 if pivot_row != col {
190 swap_dense_rows(&mut a, n, pivot_row, col);
191 swap_dense_rows(&mut inv, n, pivot_row, col);
192 }
193
194 let pivot = a[col * n + col];
195 for c in 0..n {
196 a[col * n + c] /= pivot;
197 inv[col * n + c] /= pivot;
198 }
199 for r in 0..n {
200 if r == col {
201 continue;
202 }
203 let factor = a[r * n + col];
204 if factor == 0.0 {
205 continue;
206 }
207 for c in 0..n {
208 a[r * n + c] -= factor * a[col * n + c];
209 inv[r * n + c] -= factor * inv[col * n + c];
210 }
211 }
212 }
213 Some(inv)
214}
215
216fn swap_dense_rows(a: &mut [f64], n: usize, r1: usize, r2: usize) {
217 for c in 0..n {
218 a.swap(r1 * n + c, r2 * n + c);
219 }
220}
221
222struct DenseCholesky {
224 n: usize,
225 l: Vec<f64>, }
227
228impl DenseCholesky {
229 fn factor(a: &[f64], n: usize) -> Option<Self> {
230 let mut l = vec![0.0; n * n];
231 for i in 0..n {
232 for j in 0..=i {
233 let mut s = a[i * n + j];
234 for k in 0..j {
235 s -= l[i * n + k] * l[j * n + k];
236 }
237 if i == j {
238 #[allow(clippy::neg_cmp_op_on_partial_ord)]
244 if !(s > 0.0) {
245 return None;
246 }
247 l[i * n + i] = s.sqrt();
248 } else {
249 l[i * n + j] = s / l[j * n + j];
250 }
251 }
252 }
253 Some(Self { n, l })
254 }
255
256 fn solve(&self, b: &mut [f64]) {
258 let n = self.n;
259 for i in 0..n {
260 let mut s = b[i];
261 for k in 0..i {
262 s -= self.l[i * n + k] * b[k];
263 }
264 b[i] = s / self.l[i * n + i];
265 }
266 for i in (0..n).rev() {
267 let mut s = b[i];
268 for k in (i + 1)..n {
269 s -= self.l[k * n + i] * b[k];
270 }
271 b[i] = s / self.l[i * n + i];
272 }
273 }
274
275 fn inverse(&self) -> Vec<f64> {
277 let n = self.n;
278 let mut inv = vec![0.0; n * n];
279 let mut e = vec![0.0; n];
280 for j in 0..n {
281 e.fill(0.0);
282 e[j] = 1.0;
283 self.solve(&mut e);
284 for (i, &x) in e.iter().enumerate() {
285 inv[i * n + j] = x;
286 }
287 }
288 inv
289 }
290}