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 = 512;
33const LODF_ISLAND_TOLERANCE: f64 = 1e-9;
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
37#[serde(rename_all = "snake_case")]
38#[non_exhaustive]
39pub enum SensitivitySolver {
40 #[default]
42 Auto,
43 Dense,
45 Iterative,
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
51#[serde(rename_all = "snake_case")]
52#[non_exhaustive]
53pub enum SensitivitySolverPath {
54 DenseCholesky,
55 DenseInverse,
56 IterativeCg,
57}
58
59impl SensitivitySolverPath {
60 #[inline]
61 pub fn as_str(self) -> &'static str {
62 match self {
63 Self::DenseCholesky => "dense_cholesky",
64 Self::DenseInverse => "dense_inverse",
65 Self::IterativeCg => "iterative_cg",
66 }
67 }
68}
69
70#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
72pub struct SensitivityOptions {
73 pub convention: DcConvention,
75 pub solver: SensitivitySolver,
77 pub drop_tolerance: f64,
80 pub cg_tolerance: f64,
82 pub cg_max_iterations: usize,
84 pub auto_dense_threshold: usize,
87}
88
89impl Default for SensitivityOptions {
90 fn default() -> Self {
91 Self {
92 convention: DcConvention::PaperPure,
93 solver: SensitivitySolver::Auto,
94 drop_tolerance: PRUNE,
95 cg_tolerance: DEFAULT_CG_TOLERANCE,
96 cg_max_iterations: DEFAULT_CG_MAX_ITERATIONS,
97 auto_dense_threshold: DEFAULT_AUTO_DENSE_THRESHOLD,
98 }
99 }
100}
101
102impl SensitivityOptions {
103 fn validate(&self) -> Result<()> {
104 if !self.drop_tolerance.is_finite() || self.drop_tolerance < 0.0 {
105 return Err(Error::InvalidSensitivityOptions {
106 reason: format!(
107 "drop_tolerance must be finite and nonnegative, got {}",
108 self.drop_tolerance
109 ),
110 });
111 }
112 if !self.cg_tolerance.is_finite() || self.cg_tolerance <= 0.0 {
113 return Err(Error::InvalidSensitivityOptions {
114 reason: format!(
115 "cg_tolerance must be finite and positive, got {}",
116 self.cg_tolerance
117 ),
118 });
119 }
120 if self.cg_max_iterations == 0 {
121 return Err(Error::InvalidSensitivityOptions {
122 reason: "cg_max_iterations must be positive".into(),
123 });
124 }
125 Ok(())
126 }
127
128 pub fn selected_solver_for_reduced_dimension(
130 &self,
131 reduced_dimension: usize,
132 ) -> SensitivitySolver {
133 match self.solver {
134 SensitivitySolver::Auto if reduced_dimension > self.auto_dense_threshold => {
135 SensitivitySolver::Iterative
136 }
137 SensitivitySolver::Auto => SensitivitySolver::Dense,
138 other => other,
139 }
140 }
141}
142
143#[derive(Debug, Clone)]
145pub struct SensitivityMatrices {
146 pub ptdf: CsMat<f64>,
147 pub lodf: CsMat<f64>,
148 pub metadata: SensitivityMetadata,
149}
150
151#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
153pub struct SensitivityMetadata {
154 pub requested_solver: SensitivitySolver,
155 pub solver_path: SensitivitySolverPath,
156 pub drop_tolerance: f64,
157 pub cg_tolerance: Option<f64>,
158 pub cg_max_iterations: Option<usize>,
159 pub auto_dense_threshold: usize,
160 pub reduced_dimension: usize,
161 pub ptdf: SensitivityMatrixMetadata,
162 pub lodf: SensitivityMatrixMetadata,
163}
164
165#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
167pub struct SensitivityMatrixMetadata {
168 pub rows: usize,
169 pub cols: usize,
170 pub nnz: usize,
171 pub dropped_entries: usize,
172}
173
174pub fn build_ptdf(case: &IndexedNetwork, conv: DcConvention) -> Result<CsMat<f64>> {
180 case.check_reference_coverage()?;
181 let refs = case.reference_bus_indices();
182 let inc = build_incidence(case, conv, &BuildOptions::default())?;
183 let (dense, m, n) = ptdf_dense(&inc, &refs)?;
184 Ok(dense_to_csr(&dense, m, n))
185}
186
187pub fn build_lodf(case: &IndexedNetwork, conv: DcConvention) -> Result<CsMat<f64>> {
191 case.check_reference_coverage()?;
192 let refs = case.reference_bus_indices();
193 let inc = build_incidence(case, conv, &BuildOptions::default())?;
194 let (ptdf, m, n) = ptdf_dense(&inc, &refs)?;
195 Ok(lodf_from_dense(&ptdf, &inc.a, m, n))
196}
197
198pub fn build_ptdf_lodf(
204 case: &IndexedNetwork,
205 conv: DcConvention,
206) -> Result<(CsMat<f64>, CsMat<f64>)> {
207 case.check_reference_coverage()?;
208 let refs = case.reference_bus_indices();
209 let inc = build_incidence(case, conv, &BuildOptions::default())?;
210 let (dense, m, n) = ptdf_dense(&inc, &refs)?;
211 let ptdf = dense_to_csr(&dense, m, n);
212 let lodf = lodf_from_dense(&dense, &inc.a, m, n);
213 Ok((ptdf, lodf))
214}
215
216pub fn build_ptdf_lodf_with_options(
218 case: &IndexedNetwork,
219 options: &SensitivityOptions,
220) -> Result<SensitivityMatrices> {
221 options.validate()?;
222 case.check_reference_coverage()?;
223 let refs = case.reference_bus_indices();
224 let inc = build_incidence(case, options.convention, &BuildOptions::default())?;
225 let reduced_dimension = inc.n().saturating_sub(Grounding::new(&refs).len());
226
227 let (ptdf, lodf, solver_path, ptdf_dropped, lodf_dropped) = match options
228 .selected_solver_for_reduced_dimension(reduced_dimension)
229 {
230 SensitivitySolver::Dense => {
231 let (dense, m, n, solver_path) = ptdf_dense_with_path(&inc, &refs)?;
232 let (ptdf, ptdf_dropped) = dense_to_csr_with_drop(&dense, m, n, options.drop_tolerance);
233 let (lodf, lodf_dropped) =
234 lodf_from_dense_with_drop(&dense, &inc.a, m, n, options.drop_tolerance);
235 (ptdf, lodf, solver_path, ptdf_dropped, lodf_dropped)
236 }
237 SensitivitySolver::Iterative => {
238 ensure_iterative_solver_eligible(&inc)?;
239 let (ptdf, ptdf_dropped, lodf, lodf_dropped) =
240 iterative_ptdf_lodf(&inc, &refs, options)?;
241 (
242 ptdf,
243 lodf,
244 SensitivitySolverPath::IterativeCg,
245 ptdf_dropped,
246 lodf_dropped,
247 )
248 }
249 SensitivitySolver::Auto => unreachable!("selected_solver resolves Auto"),
250 };
251
252 let metadata = sensitivity_metadata(
253 options,
254 solver_path,
255 reduced_dimension,
256 matrix_metadata(&ptdf, ptdf_dropped),
257 matrix_metadata(&lodf, lodf_dropped),
258 );
259
260 Ok(SensitivityMatrices {
261 ptdf,
262 lodf,
263 metadata,
264 })
265}
266
267pub(crate) fn for_each_ptdf_lodf_entry(
268 case: &IndexedNetwork,
269 options: &SensitivityOptions,
270 mut ptdf_entry: impl FnMut(usize, usize, f64) -> Result<()>,
271 mut lodf_entry: impl FnMut(usize, usize, f64) -> Result<()>,
272) -> Result<SensitivityMetadata> {
273 options.validate()?;
274 case.check_reference_coverage()?;
275 let refs = case.reference_bus_indices();
276 let inc = build_incidence(case, options.convention, &BuildOptions::default())?;
277 let reduced_dimension = inc.n().saturating_sub(Grounding::new(&refs).len());
278
279 let (solver_path, ptdf, lodf) = match options
280 .selected_solver_for_reduced_dimension(reduced_dimension)
281 {
282 SensitivitySolver::Dense => {
283 let (dense, m, n, solver_path) = ptdf_dense_with_path(&inc, &refs)?;
284 let (ptdf, ptdf_dropped) = dense_to_csr_with_drop(&dense, m, n, options.drop_tolerance);
285 let (lodf, lodf_dropped) =
286 lodf_from_dense_with_drop(&dense, &inc.a, m, n, options.drop_tolerance);
287 let ptdf_meta = matrix_metadata(&ptdf, ptdf_dropped);
288 let lodf_meta = matrix_metadata(&lodf, lodf_dropped);
289 for (&v, (row, col)) in &ptdf {
290 ptdf_entry(row, col, v)?;
291 }
292 for (&v, (row, col)) in &lodf {
293 lodf_entry(row, col, v)?;
294 }
295 (solver_path, ptdf_meta, lodf_meta)
296 }
297 SensitivitySolver::Iterative => {
298 ensure_iterative_solver_eligible(&inc)?;
299 let (ptdf, lodf) =
300 iterative_ptdf_lodf_entries(&inc, &refs, options, ptdf_entry, lodf_entry)?;
301 (SensitivitySolverPath::IterativeCg, ptdf, lodf)
302 }
303 SensitivitySolver::Auto => {
304 unreachable!("selected_solver_for_reduced_dimension resolves Auto")
305 }
306 };
307
308 Ok(sensitivity_metadata(
309 options,
310 solver_path,
311 reduced_dimension,
312 ptdf,
313 lodf,
314 ))
315}
316
317fn sensitivity_metadata(
318 options: &SensitivityOptions,
319 solver_path: SensitivitySolverPath,
320 reduced_dimension: usize,
321 ptdf: SensitivityMatrixMetadata,
322 lodf: SensitivityMatrixMetadata,
323) -> SensitivityMetadata {
324 SensitivityMetadata {
325 requested_solver: options.solver,
326 solver_path,
327 drop_tolerance: options.drop_tolerance,
328 cg_tolerance: matches!(solver_path, SensitivitySolverPath::IterativeCg)
329 .then_some(options.cg_tolerance),
330 cg_max_iterations: matches!(solver_path, SensitivitySolverPath::IterativeCg)
331 .then_some(options.cg_max_iterations),
332 auto_dense_threshold: options.auto_dense_threshold,
333 reduced_dimension,
334 ptdf,
335 lodf,
336 }
337}
338
339fn matrix_metadata(matrix: &CsMat<f64>, dropped_entries: usize) -> SensitivityMatrixMetadata {
340 SensitivityMatrixMetadata {
341 rows: matrix.rows(),
342 cols: matrix.cols(),
343 nnz: matrix.nnz(),
344 dropped_entries,
345 }
346}
347
348fn lodf_from_dense(ptdf: &[f64], a: &CsMat<f64>, m: usize, n: usize) -> CsMat<f64> {
351 lodf_from_dense_with_drop(ptdf, a, m, n, PRUNE).0
352}
353
354fn lodf_from_dense_with_drop(
355 ptdf: &[f64],
356 a: &CsMat<f64>,
357 m: usize,
358 n: usize,
359 drop_tolerance: f64,
360) -> (CsMat<f64>, usize) {
361 let (from, to) = endpoints(a, m);
363
364 let delta = |l: usize, k: usize| ptdf[l * n + from[k]] - ptdf[l * n + to[k]];
367
368 let mut lodf = CooBuilder::new(m); let mut dropped = 0usize;
370 for k in 0..m {
371 let denom = 1.0 - delta(k, k);
372 let islands = denom.abs() < LODF_ISLAND_TOLERANCE;
373 for l in 0..m {
374 let v = if l == k {
375 -1.0
376 } else if islands {
377 0.0
378 } else {
379 delta(l, k) / denom
380 };
381 if l == k || v.abs() > drop_tolerance {
382 lodf.add(l, k, v);
383 } else if v != 0.0 {
384 dropped += 1;
385 }
386 }
387 }
388 (lodf.finish_csr(), dropped)
389}
390
391fn ptdf_dense(inc: &IncidenceParts, refs: &[usize]) -> Result<(Vec<f64>, usize, usize)> {
394 let (ptdf, m, n, _) = ptdf_dense_with_path(inc, refs)?;
395 Ok((ptdf, m, n))
396}
397
398fn ptdf_dense_with_path(
399 inc: &IncidenceParts,
400 refs: &[usize],
401) -> Result<(Vec<f64>, usize, usize, SensitivitySolverPath)> {
402 let n = inc.n();
403 let m = inc.m();
404 let g = Grounding::new(refs);
405 let nr = n - g.len();
406
407 let lr = ground_with(&build_weighted_laplacian(&inc.a, &inc.b), &g);
409 let dense_lr = densify(&lr, nr);
410 let (rinv, solver_path) = DenseCholesky::factor(&dense_lr, nr).map_or_else(
411 || {
412 dense_inverse(&dense_lr, nr)
413 .map(|rinv| (rinv, SensitivitySolverPath::DenseInverse))
414 .ok_or(Error::SingularNetwork)
415 },
416 |chol| Ok((chol.inverse(), SensitivitySolverPath::DenseCholesky)),
417 )?; let flow = build_flow_map(&inc.a, &inc.b); let mut ptdf = vec![0.0; m * n];
425 for (&w, (l, c)) in flow.iter() {
426 let Some(rc) = g.reduced(c) else { continue }; for k in 0..n {
428 if let Some(rk) = g.reduced(k) {
429 ptdf[l * n + k] += w * rinv[rc * nr + rk];
430 }
431 }
432 }
433 Ok((ptdf, m, n, solver_path))
434}
435
436fn iterative_ptdf_lodf(
437 inc: &IncidenceParts,
438 refs: &[usize],
439 options: &SensitivityOptions,
440) -> Result<(CsMat<f64>, usize, CsMat<f64>, usize)> {
441 ensure_iterative_solver_eligible(inc)?;
442 let mut ptdf = CooBuilder::new_rect(inc.m(), inc.n());
443 let mut lodf = CooBuilder::new(inc.m());
444 let (ptdf_meta, lodf_meta) = iterative_ptdf_lodf_entries(
445 inc,
446 refs,
447 options,
448 |row, col, value| {
449 ptdf.add(row, col, value);
450 Ok(())
451 },
452 |row, col, value| {
453 lodf.add(row, col, value);
454 Ok(())
455 },
456 )?;
457 Ok((
458 ptdf.finish_csr(),
459 ptdf_meta.dropped_entries,
460 lodf.finish_csr(),
461 lodf_meta.dropped_entries,
462 ))
463}
464
465fn iterative_ptdf_lodf_entries(
466 inc: &IncidenceParts,
467 refs: &[usize],
468 options: &SensitivityOptions,
469 mut ptdf_entry: impl FnMut(usize, usize, f64) -> Result<()>,
470 mut lodf_entry: impl FnMut(usize, usize, f64) -> Result<()>,
471) -> Result<(SensitivityMatrixMetadata, SensitivityMatrixMetadata)> {
472 let n = inc.n();
473 let m = inc.m();
474 let g = Grounding::new(refs);
475 let nr = n - g.len();
476 let lr = ground_with(&build_weighted_laplacian(&inc.a, &inc.b), &g);
477 let solver = CgSolver::new(&lr, options.cg_tolerance, options.cg_max_iterations)?;
478 let (from, to) = endpoints(&inc.a, m);
479
480 let mut rhs = vec![0.0; nr];
481 let mut ptdf_nnz = 0usize;
482 let mut ptdf_dropped = 0usize;
483 for bus in 0..n {
484 let Some(rb) = g.reduced(bus) else {
485 continue;
486 };
487 rhs.fill(0.0);
488 rhs[rb] = 1.0;
489 let theta = solver.solve(&rhs)?;
490 for branch in 0..m {
491 let v = branch_flow(branch, &from, &to, &inc.b, &g, &theta);
492 if v.abs() > options.drop_tolerance {
493 ptdf_entry(branch, bus, v)?;
494 ptdf_nnz += 1;
495 } else if v != 0.0 {
496 ptdf_dropped += 1;
497 }
498 }
499 }
500
501 let mut lodf_nnz = 0usize;
502 let mut lodf_dropped = 0usize;
503 for outage in 0..m {
504 rhs.fill(0.0);
505 if let Some(rf) = g.reduced(from[outage]) {
506 rhs[rf] += 1.0;
507 }
508 if let Some(rt) = g.reduced(to[outage]) {
509 rhs[rt] -= 1.0;
510 }
511 let theta = solver.solve(&rhs)?;
512 let outage_delta = branch_flow(outage, &from, &to, &inc.b, &g, &theta);
513 let denom = 1.0 - outage_delta;
514 let islands = denom.abs() < LODF_ISLAND_TOLERANCE;
515 for branch in 0..m {
516 let v = if branch == outage {
517 -1.0
518 } else if islands {
519 0.0
520 } else {
521 branch_flow(branch, &from, &to, &inc.b, &g, &theta) / denom
522 };
523 if branch == outage || v.abs() > options.drop_tolerance {
524 lodf_entry(branch, outage, v)?;
525 lodf_nnz += 1;
526 } else if v != 0.0 {
527 lodf_dropped += 1;
528 }
529 }
530 }
531
532 Ok((
533 SensitivityMatrixMetadata {
534 rows: m,
535 cols: n,
536 nnz: ptdf_nnz,
537 dropped_entries: ptdf_dropped,
538 },
539 SensitivityMatrixMetadata {
540 rows: m,
541 cols: m,
542 nnz: lodf_nnz,
543 dropped_entries: lodf_dropped,
544 },
545 ))
546}
547
548fn ensure_iterative_solver_eligible(inc: &IncidenceParts) -> Result<()> {
549 for (branch, &b) in inc.b.iter().enumerate() {
550 if !b.is_finite() || b <= 0.0 {
551 return Err(Error::InvalidSensitivityOptions {
552 reason: format!(
553 "iterative sensitivity solver requires positive finite branch susceptances; \
554 branch {branch} has {b}; use solver=dense for nonsingular indefinite cases"
555 ),
556 });
557 }
558 }
559 Ok(())
560}
561
562fn branch_flow(
563 branch: usize,
564 from: &[usize],
565 to: &[usize],
566 b: &[f64],
567 g: &Grounding,
568 theta: &[f64],
569) -> f64 {
570 let theta_from = g.reduced(from[branch]).map_or(0.0, |i| theta[i]);
571 let theta_to = g.reduced(to[branch]).map_or(0.0, |i| theta[i]);
572 b[branch] * (theta_from - theta_to)
573}
574
575fn endpoints(a: &CsMat<f64>, m: usize) -> (Vec<usize>, Vec<usize>) {
577 let mut from = vec![0usize; m];
578 let mut to = vec![0usize; m];
579 for (&v, (bus, branch)) in a.iter() {
580 if v > 0.0 {
581 from[branch] = bus;
582 } else {
583 to[branch] = bus;
584 }
585 }
586 (from, to)
587}
588
589fn densify(a: &CsMat<f64>, n: usize) -> Vec<f64> {
590 let mut d = vec![0.0; n * n];
591 for (&v, (i, j)) in a.iter() {
592 d[i * n + j] = v;
593 }
594 d
595}
596
597fn dense_to_csr(dense: &[f64], rows: usize, cols: usize) -> CsMat<f64> {
598 dense_to_csr_with_drop(dense, rows, cols, PRUNE).0
599}
600
601fn dense_to_csr_with_drop(
602 dense: &[f64],
603 rows: usize,
604 cols: usize,
605 drop_tolerance: f64,
606) -> (CsMat<f64>, usize) {
607 let mut coo = CooBuilder::with_capacity_rect(rows, cols, dense.len() / 2);
608 let mut dropped = 0usize;
609 for i in 0..rows {
610 for j in 0..cols {
611 let v = dense[i * cols + j];
612 if v.abs() > drop_tolerance {
613 coo.add(i, j, v);
614 } else if v != 0.0 {
615 dropped += 1;
616 }
617 }
618 }
619 (coo.finish_csr(), dropped)
620}
621
622fn dense_inverse(a: &[f64], n: usize) -> Option<Vec<f64>> {
623 let mut a = a.to_vec();
624 let mut inv = vec![0.0; n * n];
625 for i in 0..n {
626 inv[i * n + i] = 1.0;
627 }
628
629 for col in 0..n {
630 let mut pivot_row = col;
631 let mut pivot_abs = a[col * n + col].abs();
632 for r in (col + 1)..n {
633 let v = a[r * n + col].abs();
634 if v > pivot_abs {
635 pivot_abs = v;
636 pivot_row = r;
637 }
638 }
639 if !pivot_abs.is_finite() || pivot_abs <= 1e-12 {
640 return None;
641 }
642 if pivot_row != col {
643 swap_dense_rows(&mut a, n, pivot_row, col);
644 swap_dense_rows(&mut inv, n, pivot_row, col);
645 }
646
647 let pivot = a[col * n + col];
648 for c in 0..n {
649 a[col * n + c] /= pivot;
650 inv[col * n + c] /= pivot;
651 }
652 for r in 0..n {
653 if r == col {
654 continue;
655 }
656 let factor = a[r * n + col];
657 if factor == 0.0 {
658 continue;
659 }
660 for c in 0..n {
661 a[r * n + c] -= factor * a[col * n + c];
662 inv[r * n + c] -= factor * inv[col * n + c];
663 }
664 }
665 }
666 Some(inv)
667}
668
669fn swap_dense_rows(a: &mut [f64], n: usize, r1: usize, r2: usize) {
670 for c in 0..n {
671 a.swap(r1 * n + c, r2 * n + c);
672 }
673}
674
675struct CgSolver<'a> {
676 a: &'a CsMat<f64>,
677 diag: Vec<f64>,
678 tolerance: f64,
679 max_iterations: usize,
680}
681
682impl<'a> CgSolver<'a> {
683 fn new(a: &'a CsMat<f64>, tolerance: f64, max_iterations: usize) -> Result<Self> {
684 let n = a.rows();
685 if a.cols() != n {
686 return Err(Error::ShapeMismatch {
687 what: "grounded DC bus susceptance matrix columns",
688 expected: n,
689 got: a.cols(),
690 });
691 }
692 let mut diag = vec![0.0; n];
693 for (i, slot) in diag.iter_mut().enumerate() {
694 *slot = a.get(i, i).copied().unwrap_or(0.0);
695 if !slot.is_finite() || *slot <= 0.0 {
696 return Err(Error::SingularNetwork);
697 }
698 }
699 Ok(Self {
700 a,
701 diag,
702 tolerance,
703 max_iterations,
704 })
705 }
706
707 fn solve(&self, rhs: &[f64]) -> Result<Vec<f64>> {
708 let n = self.a.rows();
709 if rhs.len() != n {
710 return Err(Error::DimensionMismatch {
711 n,
712 b_len: rhs.len(),
713 });
714 }
715 if n == 0 {
716 return Ok(Vec::new());
717 }
718
719 let rhs_norm = norm2(rhs);
720 if rhs_norm == 0.0 {
721 return Ok(vec![0.0; n]);
722 }
723 let target = self.tolerance * rhs_norm;
724 let mut solution = vec![0.0; n];
725 let mut residual_vec = rhs.to_vec();
726 let mut preconditioned = self.precondition(&residual_vec);
727 let mut direction = preconditioned.clone();
728 let mut residual_dot = dot(&residual_vec, &preconditioned);
729 if !residual_dot.is_finite() || residual_dot <= 0.0 {
730 return Err(Error::SingularNetwork);
731 }
732 let mut matvec_out = vec![0.0; n];
733
734 for iter in 1..=self.max_iterations {
735 matvec(self.a, &direction, &mut matvec_out);
736 let denom = dot(&direction, &matvec_out);
737 if !denom.is_finite() || denom <= 0.0 {
738 return Err(Error::SingularNetwork);
739 }
740 let alpha = residual_dot / denom;
741 for i in 0..n {
742 solution[i] += alpha * direction[i];
743 residual_vec[i] -= alpha * matvec_out[i];
744 }
745 let residual = norm2(&residual_vec);
746 if residual <= target {
747 return Ok(solution);
748 }
749 preconditioned = self.precondition(&residual_vec);
750 let next_residual_dot = dot(&residual_vec, &preconditioned);
751 if !next_residual_dot.is_finite() || next_residual_dot <= 0.0 {
752 return Err(Error::SingularNetwork);
753 }
754 let beta = next_residual_dot / residual_dot;
755 for i in 0..n {
756 direction[i] = preconditioned[i] + beta * direction[i];
757 }
758 residual_dot = next_residual_dot;
759
760 if iter == self.max_iterations {
761 return Err(Error::SensitivitySolveDidNotConverge {
762 iterations: iter,
763 relative_residual: residual / rhs_norm,
764 });
765 }
766 }
767 unreachable!("positive max_iterations loop returns")
768 }
769
770 fn precondition(&self, r: &[f64]) -> Vec<f64> {
771 r.iter().zip(&self.diag).map(|(&ri, &di)| ri / di).collect()
772 }
773}
774
775fn matvec(a: &CsMat<f64>, x: &[f64], out: &mut [f64]) {
776 out.fill(0.0);
777 for (i, row) in a.outer_iterator().enumerate() {
778 let mut sum = 0.0;
779 for (j, &v) in row.iter() {
780 sum += v * x[j];
781 }
782 out[i] = sum;
783 }
784}
785
786fn dot(a: &[f64], b: &[f64]) -> f64 {
787 a.iter().zip(b).map(|(&x, &y)| x * y).sum()
788}
789
790fn norm2(a: &[f64]) -> f64 {
791 dot(a, a).sqrt()
792}
793
794struct DenseCholesky {
796 n: usize,
797 l: Vec<f64>, }
799
800impl DenseCholesky {
801 fn factor(a: &[f64], n: usize) -> Option<Self> {
802 let mut l = vec![0.0; n * n];
803 for i in 0..n {
804 for j in 0..=i {
805 let mut s = a[i * n + j];
806 for k in 0..j {
807 s -= l[i * n + k] * l[j * n + k];
808 }
809 if i == j {
810 #[allow(clippy::neg_cmp_op_on_partial_ord)]
816 if !(s > 0.0) {
817 return None;
818 }
819 l[i * n + i] = s.sqrt();
820 } else {
821 l[i * n + j] = s / l[j * n + j];
822 }
823 }
824 }
825 Some(Self { n, l })
826 }
827
828 fn solve(&self, b: &mut [f64]) {
830 let n = self.n;
831 for i in 0..n {
832 let mut s = b[i];
833 for k in 0..i {
834 s -= self.l[i * n + k] * b[k];
835 }
836 b[i] = s / self.l[i * n + i];
837 }
838 for i in (0..n).rev() {
839 let mut s = b[i];
840 for k in (i + 1)..n {
841 s -= self.l[k * n + i] * b[k];
842 }
843 b[i] = s / self.l[i * n + i];
844 }
845 }
846
847 fn inverse(&self) -> Vec<f64> {
849 let n = self.n;
850 let mut inv = vec![0.0; n * n];
851 let mut e = vec![0.0; n];
852 for j in 0..n {
853 e.fill(0.0);
854 e[j] = 1.0;
855 self.solve(&mut e);
856 for (i, &x) in e.iter().enumerate() {
857 inv[i * n + j] = x;
858 }
859 }
860 inv
861 }
862}