1#![allow(clippy::needless_range_loop, clippy::explicit_iter_loop)]
18
19use sprs::CsMat;
20
21use crate::indexed::IndexedNetwork;
22use crate::matrix::BuildOptions;
23use crate::matrix::incidence::{DcConvention, IncidenceParts, build_flow_map, build_incidence};
24use crate::matrix::laplacian::{Grounding, build_weighted_laplacian, ground_with};
25use crate::matrix::triplet::CooBuilder;
26use crate::{Error, Result};
27
28const PRUNE: f64 = 1e-12;
30const DEFAULT_CG_TOLERANCE: f64 = 1e-10;
31const DEFAULT_CG_MAX_ITERATIONS: usize = 20_000;
32const DEFAULT_AUTO_DENSE_THRESHOLD: usize = 8192;
39
40const AUTO_DENSE_MEMORY_BUDGET: usize = 2 << 30;
45
46fn dense_footprint_bytes(reduced_dimension: usize, branches: usize, buses: usize) -> usize {
50 let f = size_of::<f64>();
51 let sq = |a: usize, b: usize| a.saturating_mul(b).saturating_mul(f);
52 sq(reduced_dimension, reduced_dimension)
53 .saturating_mul(3)
54 .saturating_add(sq(branches, buses))
55 .saturating_add(sq(branches, branches))
56}
57const LODF_ISLAND_TOLERANCE: f64 = 1e-9;
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
61#[serde(rename_all = "snake_case")]
62#[non_exhaustive]
63pub enum SensitivitySolver {
64 #[default]
66 Auto,
67 Dense,
69 Iterative,
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
75#[serde(rename_all = "snake_case")]
76#[non_exhaustive]
77pub enum SensitivitySolverPath {
78 DenseCholesky,
79 DenseInverse,
80 IterativeCg,
81}
82
83impl SensitivitySolverPath {
84 #[inline]
85 pub fn as_str(self) -> &'static str {
86 match self {
87 Self::DenseCholesky => "dense_cholesky",
88 Self::DenseInverse => "dense_inverse",
89 Self::IterativeCg => "iterative_cg",
90 }
91 }
92}
93
94#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
96pub struct SensitivityOptions {
97 pub convention: DcConvention,
99 pub solver: SensitivitySolver,
101 pub drop_tolerance: f64,
104 pub cg_tolerance: f64,
106 pub cg_max_iterations: usize,
108 pub auto_dense_threshold: usize,
111}
112
113impl Default for SensitivityOptions {
114 fn default() -> Self {
115 Self {
116 convention: DcConvention::default(),
117 solver: SensitivitySolver::Auto,
118 drop_tolerance: PRUNE,
119 cg_tolerance: DEFAULT_CG_TOLERANCE,
120 cg_max_iterations: DEFAULT_CG_MAX_ITERATIONS,
121 auto_dense_threshold: DEFAULT_AUTO_DENSE_THRESHOLD,
122 }
123 }
124}
125
126impl SensitivityOptions {
127 fn validate(&self) -> Result<()> {
128 if !self.drop_tolerance.is_finite() || self.drop_tolerance < 0.0 {
129 return Err(Error::InvalidSensitivityOptions {
130 reason: format!(
131 "drop_tolerance must be finite and nonnegative, got {}",
132 self.drop_tolerance
133 ),
134 });
135 }
136 if !self.cg_tolerance.is_finite() || self.cg_tolerance <= 0.0 {
137 return Err(Error::InvalidSensitivityOptions {
138 reason: format!(
139 "cg_tolerance must be finite and positive, got {}",
140 self.cg_tolerance
141 ),
142 });
143 }
144 if self.cg_max_iterations == 0 {
145 return Err(Error::InvalidSensitivityOptions {
146 reason: "cg_max_iterations must be positive".into(),
147 });
148 }
149 Ok(())
150 }
151
152 pub fn selected_solver_for_reduced_dimension(
157 &self,
158 reduced_dimension: usize,
159 ) -> SensitivitySolver {
160 self.selected_solver_for_shape(reduced_dimension, reduced_dimension, reduced_dimension)
161 }
162
163 pub fn selected_solver_for_shape(
168 &self,
169 reduced_dimension: usize,
170 branches: usize,
171 buses: usize,
172 ) -> SensitivitySolver {
173 match self.solver {
174 SensitivitySolver::Auto => {
175 let fits = reduced_dimension <= self.auto_dense_threshold
176 && dense_footprint_bytes(reduced_dimension, branches, buses)
177 <= AUTO_DENSE_MEMORY_BUDGET;
178 if fits {
179 SensitivitySolver::Dense
180 } else {
181 SensitivitySolver::Iterative
182 }
183 }
184 other => other,
185 }
186 }
187}
188
189#[derive(Debug, Clone)]
191pub struct SensitivityMatrices {
192 pub ptdf: CsMat<f64>,
193 pub lodf: CsMat<f64>,
194 pub metadata: SensitivityMetadata,
195}
196
197#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
199pub struct SensitivityMetadata {
200 pub requested_solver: SensitivitySolver,
201 pub solver_path: SensitivitySolverPath,
202 pub drop_tolerance: f64,
203 pub cg_tolerance: Option<f64>,
204 pub cg_max_iterations: Option<usize>,
205 pub auto_dense_threshold: usize,
206 pub reduced_dimension: usize,
207 pub ptdf: SensitivityMatrixMetadata,
208 pub lodf: SensitivityMatrixMetadata,
209}
210
211#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
213pub struct SensitivityMatrixMetadata {
214 pub rows: usize,
215 pub cols: usize,
216 pub nnz: usize,
217 pub dropped_entries: usize,
218}
219
220pub fn build_ptdf(case: &IndexedNetwork, conv: DcConvention) -> Result<CsMat<f64>> {
226 case.check_reference_coverage()?;
227 let refs = case.reference_bus_indices();
228 let inc = build_incidence(case, conv, &BuildOptions::default())?;
229 let (dense, m, n) = ptdf_dense(&inc, &refs)?;
230 Ok(dense_to_csr(&dense, m, n))
231}
232
233pub fn build_lodf(case: &IndexedNetwork, conv: DcConvention) -> Result<CsMat<f64>> {
237 case.check_reference_coverage()?;
238 let refs = case.reference_bus_indices();
239 let inc = build_incidence(case, conv, &BuildOptions::default())?;
240 let (ptdf, m, n) = ptdf_dense(&inc, &refs)?;
241 Ok(lodf_from_dense(&ptdf, &inc.a, m, n))
242}
243
244pub fn build_ptdf_lodf(
250 case: &IndexedNetwork,
251 conv: DcConvention,
252) -> Result<(CsMat<f64>, CsMat<f64>)> {
253 case.check_reference_coverage()?;
254 let refs = case.reference_bus_indices();
255 let inc = build_incidence(case, conv, &BuildOptions::default())?;
256 let (dense, m, n) = ptdf_dense(&inc, &refs)?;
257 let ptdf = dense_to_csr(&dense, m, n);
258 let lodf = lodf_from_dense(&dense, &inc.a, m, n);
259 Ok((ptdf, lodf))
260}
261
262pub fn build_ptdf_lodf_with_options(
264 case: &IndexedNetwork,
265 options: &SensitivityOptions,
266) -> Result<SensitivityMatrices> {
267 options.validate()?;
268 case.check_reference_coverage()?;
269 let refs = case.reference_bus_indices();
270 let inc = build_incidence(case, options.convention, &BuildOptions::default())?;
271 let reduced_dimension = inc.n().saturating_sub(Grounding::new(&refs).len());
272
273 let (ptdf, lodf, solver_path, ptdf_dropped, lodf_dropped) = match options
274 .selected_solver_for_shape(reduced_dimension, inc.m(), inc.n())
275 {
276 SensitivitySolver::Dense => {
277 let (dense, m, n, solver_path) = ptdf_dense_with_path(&inc, &refs)?;
278 let (ptdf, ptdf_dropped) = dense_to_csr_with_drop(&dense, m, n, options.drop_tolerance);
279 let (lodf, lodf_dropped) =
280 lodf_from_dense_with_drop(&dense, &inc.a, m, n, options.drop_tolerance);
281 (ptdf, lodf, solver_path, ptdf_dropped, lodf_dropped)
282 }
283 SensitivitySolver::Iterative => {
284 ensure_iterative_solver_eligible(&inc)?;
285 let (ptdf, ptdf_dropped, lodf, lodf_dropped) =
286 iterative_ptdf_lodf(&inc, &refs, options)?;
287 (
288 ptdf,
289 lodf,
290 SensitivitySolverPath::IterativeCg,
291 ptdf_dropped,
292 lodf_dropped,
293 )
294 }
295 SensitivitySolver::Auto => unreachable!("selected_solver resolves Auto"),
296 };
297
298 let metadata = sensitivity_metadata(
299 options,
300 solver_path,
301 reduced_dimension,
302 matrix_metadata(&ptdf, ptdf_dropped),
303 matrix_metadata(&lodf, lodf_dropped),
304 );
305
306 Ok(SensitivityMatrices {
307 ptdf,
308 lodf,
309 metadata,
310 })
311}
312
313pub(crate) fn for_each_ptdf_lodf_entry(
314 case: &IndexedNetwork,
315 options: &SensitivityOptions,
316 mut ptdf_entry: impl FnMut(usize, usize, f64) -> Result<()>,
317 mut lodf_entry: impl FnMut(usize, usize, f64) -> Result<()>,
318) -> Result<SensitivityMetadata> {
319 options.validate()?;
320 case.check_reference_coverage()?;
321 let refs = case.reference_bus_indices();
322 let inc = build_incidence(case, options.convention, &BuildOptions::default())?;
323 let reduced_dimension = inc.n().saturating_sub(Grounding::new(&refs).len());
324
325 let (solver_path, ptdf, lodf) =
326 match options.selected_solver_for_shape(reduced_dimension, inc.m(), inc.n()) {
327 SensitivitySolver::Dense => {
328 let (dense, m, n, solver_path) = ptdf_dense_with_path(&inc, &refs)?;
329 let (ptdf, ptdf_dropped) =
330 dense_to_csr_with_drop(&dense, m, n, options.drop_tolerance);
331 let (lodf, lodf_dropped) =
332 lodf_from_dense_with_drop(&dense, &inc.a, m, n, options.drop_tolerance);
333 let ptdf_meta = matrix_metadata(&ptdf, ptdf_dropped);
334 let lodf_meta = matrix_metadata(&lodf, lodf_dropped);
335 for (&v, (row, col)) in &ptdf {
336 ptdf_entry(row, col, v)?;
337 }
338 for (&v, (row, col)) in &lodf {
339 lodf_entry(row, col, v)?;
340 }
341 (solver_path, ptdf_meta, lodf_meta)
342 }
343 SensitivitySolver::Iterative => {
344 ensure_iterative_solver_eligible(&inc)?;
345 let (ptdf, lodf) =
346 iterative_ptdf_lodf_entries(&inc, &refs, options, ptdf_entry, lodf_entry)?;
347 (SensitivitySolverPath::IterativeCg, ptdf, lodf)
348 }
349 SensitivitySolver::Auto => {
350 unreachable!("selected_solver_for_reduced_dimension resolves Auto")
351 }
352 };
353
354 Ok(sensitivity_metadata(
355 options,
356 solver_path,
357 reduced_dimension,
358 ptdf,
359 lodf,
360 ))
361}
362
363fn sensitivity_metadata(
364 options: &SensitivityOptions,
365 solver_path: SensitivitySolverPath,
366 reduced_dimension: usize,
367 ptdf: SensitivityMatrixMetadata,
368 lodf: SensitivityMatrixMetadata,
369) -> SensitivityMetadata {
370 SensitivityMetadata {
371 requested_solver: options.solver,
372 solver_path,
373 drop_tolerance: options.drop_tolerance,
374 cg_tolerance: matches!(solver_path, SensitivitySolverPath::IterativeCg)
375 .then_some(options.cg_tolerance),
376 cg_max_iterations: matches!(solver_path, SensitivitySolverPath::IterativeCg)
377 .then_some(options.cg_max_iterations),
378 auto_dense_threshold: options.auto_dense_threshold,
379 reduced_dimension,
380 ptdf,
381 lodf,
382 }
383}
384
385fn matrix_metadata(matrix: &CsMat<f64>, dropped_entries: usize) -> SensitivityMatrixMetadata {
386 SensitivityMatrixMetadata {
387 rows: matrix.rows(),
388 cols: matrix.cols(),
389 nnz: matrix.nnz(),
390 dropped_entries,
391 }
392}
393
394fn lodf_from_dense(ptdf: &[f64], a: &CsMat<f64>, m: usize, n: usize) -> CsMat<f64> {
397 lodf_from_dense_with_drop(ptdf, a, m, n, PRUNE).0
398}
399
400fn lodf_from_dense_with_drop(
401 ptdf: &[f64],
402 a: &CsMat<f64>,
403 m: usize,
404 n: usize,
405 drop_tolerance: f64,
406) -> (CsMat<f64>, usize) {
407 let (from, to) = endpoints(a, m);
409
410 let delta = |l: usize, k: usize| ptdf[l * n + from[k]] - ptdf[l * n + to[k]];
413
414 let is_bridge = bridges(&from, &to, n);
419
420 let mut lodf = CooBuilder::new(m); let mut dropped = 0usize;
422 for k in 0..m {
423 let denom = 1.0 - delta(k, k);
424 let islands = is_bridge[k] || denom.abs() < LODF_ISLAND_TOLERANCE;
425 for l in 0..m {
426 let v = if l == k {
427 -1.0
428 } else if islands {
429 0.0
430 } else {
431 delta(l, k) / denom
432 };
433 if l == k || v.abs() > drop_tolerance {
434 lodf.add(l, k, v);
435 } else if v != 0.0 {
436 dropped += 1;
437 }
438 }
439 }
440 (lodf.finish_csr(), dropped)
441}
442
443fn ptdf_dense(inc: &IncidenceParts, refs: &[usize]) -> Result<(Vec<f64>, usize, usize)> {
446 let (ptdf, m, n, _) = ptdf_dense_with_path(inc, refs)?;
447 Ok((ptdf, m, n))
448}
449
450fn ptdf_dense_with_path(
451 inc: &IncidenceParts,
452 refs: &[usize],
453) -> Result<(Vec<f64>, usize, usize, SensitivitySolverPath)> {
454 let n = inc.n();
455 let m = inc.m();
456 let g = Grounding::new(refs);
457 let nr = n - g.len();
458
459 let lr = ground_with(&build_weighted_laplacian(&inc.a, &inc.b), &g);
461 let dense_lr = densify(&lr, nr);
462 let (rinv, solver_path) = DenseCholesky::factor(&dense_lr, nr).map_or_else(
463 || {
464 dense_inverse(&dense_lr, nr)
465 .map(|rinv| (rinv, SensitivitySolverPath::DenseInverse))
466 .ok_or(Error::SingularNetwork)
467 },
468 |chol| Ok((chol.inverse(), SensitivitySolverPath::DenseCholesky)),
469 )?; let flow = build_flow_map(&inc.a, &inc.b); let mut ptdf = vec![0.0; m * n];
477 let full_of = g.full_of_reduced(n);
482 for (&w, (l, c)) in flow.iter() {
483 let Some(rc) = g.reduced(c) else { continue }; let row = &rinv[rc * nr..rc * nr + nr];
485 let out = &mut ptdf[l * n..l * n + n];
486 for (rk, &k) in full_of.iter().enumerate() {
487 out[k] += w * row[rk];
488 }
489 }
490 Ok((ptdf, m, n, solver_path))
491}
492
493fn iterative_ptdf_lodf(
494 inc: &IncidenceParts,
495 refs: &[usize],
496 options: &SensitivityOptions,
497) -> Result<(CsMat<f64>, usize, CsMat<f64>, usize)> {
498 ensure_iterative_solver_eligible(inc)?;
499 let mut ptdf = CooBuilder::new_rect(inc.m(), inc.n());
500 let mut lodf = CooBuilder::new(inc.m());
501 let (ptdf_meta, lodf_meta) = iterative_ptdf_lodf_entries(
502 inc,
503 refs,
504 options,
505 |row, col, value| {
506 ptdf.add(row, col, value);
507 Ok(())
508 },
509 |row, col, value| {
510 lodf.add(row, col, value);
511 Ok(())
512 },
513 )?;
514 Ok((
515 ptdf.finish_csr(),
516 ptdf_meta.dropped_entries,
517 lodf.finish_csr(),
518 lodf_meta.dropped_entries,
519 ))
520}
521
522fn iterative_ptdf_lodf_entries(
523 inc: &IncidenceParts,
524 refs: &[usize],
525 options: &SensitivityOptions,
526 mut ptdf_entry: impl FnMut(usize, usize, f64) -> Result<()>,
527 mut lodf_entry: impl FnMut(usize, usize, f64) -> Result<()>,
528) -> Result<(SensitivityMatrixMetadata, SensitivityMatrixMetadata)> {
529 let n = inc.n();
530 let m = inc.m();
531 let g = Grounding::new(refs);
532 let nr = n - g.len();
533 let lr = ground_with(&build_weighted_laplacian(&inc.a, &inc.b), &g);
534 let solver = CgSolver::new(&lr, options.cg_tolerance, options.cg_max_iterations)?;
535 let (from, to) = endpoints(&inc.a, m);
536
537 let mut rhs = vec![0.0; nr];
538 let mut ptdf_nnz = 0usize;
539 let mut ptdf_dropped = 0usize;
540 for bus in 0..n {
541 let Some(rb) = g.reduced(bus) else {
542 continue;
543 };
544 rhs.fill(0.0);
545 rhs[rb] = 1.0;
546 let theta = solver.solve(&rhs)?;
547 for branch in 0..m {
548 let v = branch_flow(branch, &from, &to, &inc.b, &g, &theta);
549 if v.abs() > options.drop_tolerance {
550 ptdf_entry(branch, bus, v)?;
551 ptdf_nnz += 1;
552 } else if v != 0.0 {
553 ptdf_dropped += 1;
554 }
555 }
556 }
557
558 let is_bridge = bridges(&from, &to, n);
561
562 let mut lodf_nnz = 0usize;
563 let mut lodf_dropped = 0usize;
564 for outage in 0..m {
565 rhs.fill(0.0);
566 if let Some(rf) = g.reduced(from[outage]) {
567 rhs[rf] += 1.0;
568 }
569 if let Some(rt) = g.reduced(to[outage]) {
570 rhs[rt] -= 1.0;
571 }
572 let theta = solver.solve(&rhs)?;
573 let outage_delta = branch_flow(outage, &from, &to, &inc.b, &g, &theta);
574 let denom = 1.0 - outage_delta;
575 let islands = is_bridge[outage] || denom.abs() < LODF_ISLAND_TOLERANCE;
576 for branch in 0..m {
577 let v = if branch == outage {
578 -1.0
579 } else if islands {
580 0.0
581 } else {
582 branch_flow(branch, &from, &to, &inc.b, &g, &theta) / denom
583 };
584 if branch == outage || v.abs() > options.drop_tolerance {
585 lodf_entry(branch, outage, v)?;
586 lodf_nnz += 1;
587 } else if v != 0.0 {
588 lodf_dropped += 1;
589 }
590 }
591 }
592
593 Ok((
594 SensitivityMatrixMetadata {
595 rows: m,
596 cols: n,
597 nnz: ptdf_nnz,
598 dropped_entries: ptdf_dropped,
599 },
600 SensitivityMatrixMetadata {
601 rows: m,
602 cols: m,
603 nnz: lodf_nnz,
604 dropped_entries: lodf_dropped,
605 },
606 ))
607}
608
609fn ensure_iterative_solver_eligible(inc: &IncidenceParts) -> Result<()> {
610 for (branch, &b) in inc.b.iter().enumerate() {
611 if !b.is_finite() || b <= 0.0 {
612 return Err(Error::InvalidSensitivityOptions {
613 reason: format!(
614 "iterative sensitivity solver requires positive finite branch susceptances; \
615 branch {branch} has {b}; use solver=dense for nonsingular indefinite cases"
616 ),
617 });
618 }
619 }
620 Ok(())
621}
622
623fn branch_flow(
624 branch: usize,
625 from: &[usize],
626 to: &[usize],
627 b: &[f64],
628 g: &Grounding,
629 theta: &[f64],
630) -> f64 {
631 let theta_from = g.reduced(from[branch]).map_or(0.0, |i| theta[i]);
632 let theta_to = g.reduced(to[branch]).map_or(0.0, |i| theta[i]);
633 b[branch] * (theta_from - theta_to)
634}
635
636fn bridges(from: &[usize], to: &[usize], n: usize) -> Vec<bool> {
649 let m = from.len();
650 let mut head = vec![usize::MAX; n];
654 let mut next = vec![usize::MAX; 2 * m];
655 let mut dest = vec![0usize; 2 * m];
656 for k in 0..m {
657 for (arc, tail, other) in [(2 * k, from[k], to[k]), (2 * k + 1, to[k], from[k])] {
658 dest[arc] = other;
659 next[arc] = head[tail];
660 head[tail] = arc;
661 }
662 }
663
664 let mut disc = vec![usize::MAX; n];
665 let mut low = vec![0usize; n];
666 let mut is_bridge = vec![false; m];
667 let mut timer = 0usize;
668 let mut stack: Vec<(usize, usize, usize)> = Vec::new();
670
671 for root in 0..n {
672 if disc[root] != usize::MAX {
673 continue;
674 }
675 disc[root] = timer;
676 low[root] = timer;
677 timer += 1;
678 stack.push((root, usize::MAX, head[root]));
679 while let Some(top) = stack.last_mut() {
680 let (v, in_arc) = (top.0, top.1);
681 if top.2 == usize::MAX {
682 stack.pop();
683 if let Some(parent) = stack.last_mut() {
684 let p = parent.0;
685 low[p] = low[p].min(low[v]);
686 if low[v] > disc[p] {
689 is_bridge[in_arc / 2] = true;
690 }
691 }
692 continue;
693 }
694 let arc = top.2;
695 top.2 = next[arc];
696 if arc == in_arc ^ 1 {
698 continue;
699 }
700 let w = dest[arc];
701 if disc[w] == usize::MAX {
702 disc[w] = timer;
703 low[w] = timer;
704 timer += 1;
705 stack.push((w, arc, head[w]));
706 } else {
707 low[v] = low[v].min(disc[w]);
708 }
709 }
710 }
711 is_bridge
712}
713
714fn endpoints(a: &CsMat<f64>, m: usize) -> (Vec<usize>, Vec<usize>) {
715 let mut from = vec![0usize; m];
716 let mut to = vec![0usize; m];
717 for (&v, (bus, branch)) in a.iter() {
718 if v > 0.0 {
719 from[branch] = bus;
720 } else {
721 to[branch] = bus;
722 }
723 }
724 (from, to)
725}
726
727fn densify(a: &CsMat<f64>, n: usize) -> Vec<f64> {
728 let mut d = vec![0.0; n * n];
729 for (&v, (i, j)) in a.iter() {
730 d[i * n + j] = v;
731 }
732 d
733}
734
735fn dense_to_csr(dense: &[f64], rows: usize, cols: usize) -> CsMat<f64> {
736 dense_to_csr_with_drop(dense, rows, cols, PRUNE).0
737}
738
739fn dense_to_csr_with_drop(
740 dense: &[f64],
741 rows: usize,
742 cols: usize,
743 drop_tolerance: f64,
744) -> (CsMat<f64>, usize) {
745 let mut dropped = 0usize;
752 let mut nnz = 0usize;
753 for &v in dense {
754 if v.abs() > drop_tolerance {
755 nnz += 1;
756 } else if v != 0.0 {
757 dropped += 1;
758 }
759 }
760 let mut indptr = Vec::with_capacity(rows + 1);
761 let mut indices = Vec::with_capacity(nnz);
762 let mut data = Vec::with_capacity(nnz);
763 indptr.push(0usize);
764 for i in 0..rows {
765 for j in 0..cols {
766 let v = dense[i * cols + j];
767 if v.abs() > drop_tolerance {
768 indices.push(j);
769 data.push(v);
770 }
771 }
772 indptr.push(data.len());
773 }
774 (CsMat::new((rows, cols), indptr, indices, data), dropped)
775}
776
777#[allow(clippy::cast_precision_loss)]
786fn pivot_floor(a: &[f64], n: usize) -> f64 {
787 let scale = a.iter().fold(0.0_f64, |m, v| m.max(v.abs()));
788 n as f64 * f64::EPSILON * scale
789}
790
791fn dense_inverse(a: &[f64], n: usize) -> Option<Vec<f64>> {
792 let floor = pivot_floor(a, n);
793 let mut a = a.to_vec();
794 let mut inv = vec![0.0; n * n];
795 for i in 0..n {
796 inv[i * n + i] = 1.0;
797 }
798
799 for col in 0..n {
800 let mut pivot_row = col;
801 let mut pivot_abs = a[col * n + col].abs();
802 for r in (col + 1)..n {
803 let v = a[r * n + col].abs();
804 if v > pivot_abs {
805 pivot_abs = v;
806 pivot_row = r;
807 }
808 }
809 if !pivot_abs.is_finite() || pivot_abs <= floor {
810 return None;
811 }
812 if pivot_row != col {
813 swap_dense_rows(&mut a, n, pivot_row, col);
814 swap_dense_rows(&mut inv, n, pivot_row, col);
815 }
816
817 let pivot = a[col * n + col];
818 for c in 0..n {
819 a[col * n + c] /= pivot;
820 inv[col * n + c] /= pivot;
821 }
822 for r in 0..n {
823 if r == col {
824 continue;
825 }
826 let factor = a[r * n + col];
827 if factor == 0.0 {
828 continue;
829 }
830 for c in 0..n {
831 a[r * n + c] -= factor * a[col * n + c];
832 inv[r * n + c] -= factor * inv[col * n + c];
833 }
834 }
835 }
836 Some(inv)
837}
838
839fn swap_dense_rows(a: &mut [f64], n: usize, r1: usize, r2: usize) {
840 for c in 0..n {
841 a.swap(r1 * n + c, r2 * n + c);
842 }
843}
844
845struct CgSolver<'a> {
846 a: &'a CsMat<f64>,
847 diag: Vec<f64>,
848 tolerance: f64,
849 max_iterations: usize,
850}
851
852impl<'a> CgSolver<'a> {
853 fn new(a: &'a CsMat<f64>, tolerance: f64, max_iterations: usize) -> Result<Self> {
854 let n = a.rows();
855 if a.cols() != n {
856 return Err(Error::ShapeMismatch {
857 what: "grounded DC bus susceptance matrix columns",
858 expected: n,
859 got: a.cols(),
860 });
861 }
862 let mut diag = vec![0.0; n];
863 for (i, slot) in diag.iter_mut().enumerate() {
864 *slot = a.get(i, i).copied().unwrap_or(0.0);
865 if !slot.is_finite() || *slot <= 0.0 {
866 return Err(Error::SingularNetwork);
867 }
868 }
869 Ok(Self {
870 a,
871 diag,
872 tolerance,
873 max_iterations,
874 })
875 }
876
877 fn solve(&self, rhs: &[f64]) -> Result<Vec<f64>> {
878 let n = self.a.rows();
879 if rhs.len() != n {
880 return Err(Error::DimensionMismatch {
881 n,
882 b_len: rhs.len(),
883 });
884 }
885 if n == 0 {
886 return Ok(Vec::new());
887 }
888
889 let rhs_norm = norm2(rhs);
890 if rhs_norm == 0.0 {
891 return Ok(vec![0.0; n]);
892 }
893 let target = self.tolerance * rhs_norm;
894 let mut solution = vec![0.0; n];
895 let mut residual_vec = rhs.to_vec();
896 let mut preconditioned = self.precondition(&residual_vec);
897 let mut direction = preconditioned.clone();
898 let mut residual_dot = dot(&residual_vec, &preconditioned);
899 if !residual_dot.is_finite() || residual_dot <= 0.0 {
900 return Err(Error::SingularNetwork);
901 }
902 let mut matvec_out = vec![0.0; n];
903
904 for iter in 1..=self.max_iterations {
905 matvec(self.a, &direction, &mut matvec_out);
906 let denom = dot(&direction, &matvec_out);
907 if !denom.is_finite() || denom <= 0.0 {
908 return Err(Error::SingularNetwork);
909 }
910 let alpha = residual_dot / denom;
911 for i in 0..n {
912 solution[i] += alpha * direction[i];
913 residual_vec[i] -= alpha * matvec_out[i];
914 }
915 let residual = norm2(&residual_vec);
916 if residual <= target {
917 return Ok(solution);
918 }
919 preconditioned = self.precondition(&residual_vec);
920 let next_residual_dot = dot(&residual_vec, &preconditioned);
921 if !next_residual_dot.is_finite() || next_residual_dot <= 0.0 {
922 return Err(Error::SingularNetwork);
923 }
924 let beta = next_residual_dot / residual_dot;
925 for i in 0..n {
926 direction[i] = preconditioned[i] + beta * direction[i];
927 }
928 residual_dot = next_residual_dot;
929
930 if iter == self.max_iterations {
931 return Err(Error::SensitivitySolveDidNotConverge {
932 iterations: iter,
933 relative_residual: residual / rhs_norm,
934 });
935 }
936 }
937 unreachable!("positive max_iterations loop returns")
938 }
939
940 fn precondition(&self, r: &[f64]) -> Vec<f64> {
941 r.iter().zip(&self.diag).map(|(&ri, &di)| ri / di).collect()
942 }
943}
944
945fn matvec(a: &CsMat<f64>, x: &[f64], out: &mut [f64]) {
946 out.fill(0.0);
947 for (i, row) in a.outer_iterator().enumerate() {
948 let mut sum = 0.0;
949 for (j, &v) in row.iter() {
950 sum += v * x[j];
951 }
952 out[i] = sum;
953 }
954}
955
956fn dot(a: &[f64], b: &[f64]) -> f64 {
957 a.iter().zip(b).map(|(&x, &y)| x * y).sum()
958}
959
960fn norm2(a: &[f64]) -> f64 {
961 dot(a, a).sqrt()
962}
963
964struct DenseCholesky {
966 n: usize,
967 l: Vec<f64>, }
969
970impl DenseCholesky {
971 fn factor(a: &[f64], n: usize) -> Option<Self> {
972 let floor = pivot_floor(a, n);
974 let mut l = vec![0.0; n * n];
975 for i in 0..n {
976 for j in 0..=i {
977 let mut s = a[i * n + j];
978 for k in 0..j {
979 s -= l[i * n + k] * l[j * n + k];
980 }
981 if i == j {
982 #[allow(clippy::neg_cmp_op_on_partial_ord)]
988 if !(s > floor) {
989 return None;
990 }
991 l[i * n + i] = s.sqrt();
992 } else {
993 l[i * n + j] = s / l[j * n + j];
994 }
995 }
996 }
997 Some(Self { n, l })
998 }
999
1000 fn solve(&self, b: &mut [f64]) {
1002 let n = self.n;
1003 for i in 0..n {
1004 let mut s = b[i];
1005 for k in 0..i {
1006 s -= self.l[i * n + k] * b[k];
1007 }
1008 b[i] = s / self.l[i * n + i];
1009 }
1010 for i in (0..n).rev() {
1011 let mut s = b[i];
1012 for k in (i + 1)..n {
1013 s -= self.l[k * n + i] * b[k];
1014 }
1015 b[i] = s / self.l[i * n + i];
1016 }
1017 }
1018
1019 fn inverse(&self) -> Vec<f64> {
1021 let n = self.n;
1022 let mut inv = vec![0.0; n * n];
1023 let mut e = vec![0.0; n];
1024 for j in 0..n {
1025 e.fill(0.0);
1026 e[j] = 1.0;
1027 self.solve(&mut e);
1028 for (i, &x) in e.iter().enumerate() {
1029 inv[i * n + j] = x;
1030 }
1031 }
1032 inv
1033 }
1034}
1035
1036#[cfg(test)]
1037mod bridge_tests {
1038 use super::bridges;
1039
1040 #[test]
1041 fn every_edge_of_a_path_is_a_bridge() {
1042 let b = bridges(&[0, 1, 2], &[1, 2, 3], 4);
1044 assert_eq!(b, vec![true, true, true]);
1045 }
1046
1047 #[test]
1048 fn no_edge_of_a_cycle_is_a_bridge() {
1049 let b = bridges(&[0, 1, 2], &[1, 2, 0], 3);
1051 assert_eq!(b, vec![false, false, false]);
1052 }
1053
1054 #[test]
1055 fn parallel_branches_leave_neither_a_bridge() {
1056 let b = bridges(&[0, 0], &[1, 1], 2);
1060 assert_eq!(b, vec![false, false]);
1061 }
1062
1063 #[test]
1064 fn only_the_tie_between_two_loops_is_a_bridge() {
1065 let from = [0, 1, 2, 2, 3, 4, 5];
1067 let to = [1, 2, 0, 3, 4, 5, 3];
1068 let b = bridges(&from, &to, 6);
1069 assert_eq!(b, vec![false, false, false, true, false, false, false]);
1070 }
1071
1072 #[test]
1073 fn a_self_loop_is_not_a_bridge() {
1074 let b = bridges(&[0, 1], &[1, 1], 2);
1075 assert_eq!(b, vec![true, false]);
1076 }
1077
1078 #[test]
1079 fn separate_components_are_each_walked() {
1080 let b = bridges(&[0, 2], &[1, 3], 4);
1083 assert_eq!(b, vec![true, true]);
1084 }
1085
1086 #[test]
1087 fn a_long_path_does_not_overflow_the_stack() {
1088 let n = 200_000;
1090 let from: Vec<usize> = (0..n - 1).collect();
1091 let to: Vec<usize> = (1..n).collect();
1092 let b = bridges(&from, &to, n);
1093 assert_eq!(b.len(), n - 1);
1094 assert!(b.iter().all(|&x| x));
1095 }
1096}
1097
1098#[cfg(test)]
1099mod pivot_tests {
1100 use super::{DenseCholesky, dense_inverse};
1101
1102 #[test]
1106 fn a_pivot_is_judged_against_the_matrix_scale() {
1107 let big = [1e12, 0.0, 0.0, 1e-6];
1110 assert!(dense_inverse(&big, 2).is_none(), "1e-18 relative accepted");
1111 assert!(DenseCholesky::factor(&big, 2).is_none());
1112
1113 let small = [1e-14, 0.0, 0.0, 1e-14];
1116 let inv = dense_inverse(&small, 2).expect("a well conditioned small matrix inverts");
1117 assert!((inv[0] - 1e14).abs() < 1.0, "{inv:?}");
1118 assert!(DenseCholesky::factor(&small, 2).is_some());
1119
1120 assert!(dense_inverse(&[1.0, 1.0, 1.0, 1.0], 2).is_none());
1122 assert!(DenseCholesky::factor(&[1.0, 1.0, 1.0, 1.0], 2).is_none());
1123 }
1124
1125 #[test]
1128 fn a_nan_pivot_does_not_factor() {
1129 assert!(DenseCholesky::factor(&[f64::NAN, 0.0, 0.0, 1.0], 2).is_none());
1130 assert!(dense_inverse(&[f64::NAN, 0.0, 0.0, 1.0], 2).is_none());
1131 }
1132}
1133
1134#[cfg(test)]
1135mod auto_policy_tests {
1136 use super::{
1137 AUTO_DENSE_MEMORY_BUDGET, SensitivityOptions, SensitivitySolver, dense_footprint_bytes,
1138 };
1139
1140 #[test]
1141 fn auto_takes_the_dense_path_for_a_mid_size_case() {
1142 let o = SensitivityOptions::default();
1146 assert_eq!(
1147 o.selected_solver_for_shape(2868, 4582, 2869),
1148 SensitivitySolver::Dense
1149 );
1150 }
1151
1152 #[test]
1153 fn auto_refuses_the_dense_path_for_a_wide_case() {
1154 let o = SensitivityOptions::default();
1158 let (nr, m, n) = (400usize, 40_000usize, 401usize);
1159 assert!(nr <= o.auto_dense_threshold);
1160 assert!(dense_footprint_bytes(nr, m, n) > AUTO_DENSE_MEMORY_BUDGET);
1161 assert_eq!(
1162 o.selected_solver_for_shape(nr, m, n),
1163 SensitivitySolver::Iterative
1164 );
1165 }
1166
1167 #[test]
1168 fn an_explicit_solver_choice_ignores_both_ceilings() {
1169 for solver in [SensitivitySolver::Dense, SensitivitySolver::Iterative] {
1170 let o = SensitivityOptions {
1171 solver,
1172 ..SensitivityOptions::default()
1173 };
1174 assert_eq!(o.selected_solver_for_shape(1, 1, 1), solver);
1175 assert_eq!(o.selected_solver_for_shape(99_999, 99_999, 99_999), solver);
1176 }
1177 }
1178
1179 #[test]
1180 fn the_footprint_saturates_instead_of_overflowing() {
1181 assert_eq!(
1182 SensitivityOptions::default().selected_solver_for_shape(
1183 usize::MAX,
1184 usize::MAX,
1185 usize::MAX
1186 ),
1187 SensitivitySolver::Iterative
1188 );
1189 }
1190}