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