rustyqlib/core/fd_solvers/
psor.rs1#[derive(Debug, Clone)]
11pub struct PsorResult {
12 pub x: Vec<f64>,
13 pub iterations: usize,
14 pub converged: bool,
15}
16
17pub fn psor(
29 a: &[f64],
30 b: &[f64],
31 c: &[f64],
32 d: &[f64],
33 floor: Option<&[f64]>,
34 cap: Option<&[f64]>,
35 omega: f64,
36 tol: f64,
37 max_iter: usize,
38) -> PsorResult {
39 let n = d.len();
40 assert!(b.len() == n && a.len() == n - 1 && c.len() == n - 1);
41 assert!(omega > 0.0 && omega < 2.0, "SOR needs omega in (0, 2)");
42 let project = |i: usize, v: f64| -> f64 {
43 let mut v = v;
44 if let Some(f) = floor {
45 v = v.max(f[i]);
46 }
47 if let Some(cp) = cap {
48 v = v.min(cp[i]);
49 }
50 v
51 };
52
53 let mut x: Vec<f64> = (0..n).map(|i| project(i, d[i] / b[i])).collect();
55 for it in 1..=max_iter {
56 let mut max_update: f64 = 0.0;
57 for i in 0..n {
58 let mut gs = d[i];
59 if i > 0 {
60 gs -= a[i - 1] * x[i - 1];
61 }
62 if i < n - 1 {
63 gs -= c[i] * x[i + 1];
64 }
65 gs /= b[i];
66 let xi = project(i, (1.0 - omega) * x[i] + omega * gs);
67 max_update = max_update.max((xi - x[i]).abs());
68 x[i] = xi;
69 }
70 if max_update <= tol {
71 return PsorResult { x, iterations: it, converged: true };
72 }
73 }
74 PsorResult { x, iterations: max_iter, converged: false }
75}
76
77#[cfg(test)]
78mod tests {
79 use super::super::brennan_schwartz::brennan_schwartz;
80 use super::super::tridiagonal::thomas_algorithm;
81 use super::*;
82
83 const A: [f64; 2] = [1.0, 1.0];
84 const B: [f64; 3] = [3.0, 3.0, 3.0];
85 const C: [f64; 2] = [1.0, 1.0];
86 const D: [f64; 3] = [5.0, 10.0, 11.0];
87
88 #[test]
89 fn unconstrained_psor_matches_thomas() {
90 let free = thomas_algorithm(&A, &B, &C, &D);
91 let r = psor(&A, &B, &C, &D, None, None, 1.2, 1e-13, 10_000);
92 assert!(r.converged);
93 for (x, y) in r.x.iter().zip(&free) {
94 assert!((x - y).abs() < 1e-10, "{:?} vs {free:?}", r.x);
95 }
96 }
97
98 #[test]
99 fn floored_psor_matches_brennan_schwartz_on_a_put_lcp() {
100 let n = 60;
105 let lam = 0.45;
106 let strike = 30.0;
107 let a = vec![-lam; n - 1];
108 let b = vec![1.0 + 2.0 * lam; n];
109 let c = vec![-lam; n - 1];
110 let payoff: Vec<f64> = (0..n).map(|i| (strike - i as f64).max(0.0)).collect();
111
112 let bs = brennan_schwartz(&a, &b, &c, &payoff, &payoff, true);
113 let r = psor(&a, &b, &c, &payoff, Some(&payoff), None, 1.4, 1e-13, 20_000);
114 assert!(r.converged, "psor did not converge");
115 for (x, y) in r.x.iter().zip(&bs) {
116 assert!((x - y).abs() < 1e-7, "psor vs brennan-schwartz mismatch");
117 }
118 }
119
120 #[test]
121 fn two_sided_constraints_are_enforced() {
122 let floor = [1.0, 1.0, 1.0];
123 let cap = [2.0, 2.0, 2.0];
124 let r = psor(&A, &B, &C, &D, Some(&floor), Some(&cap), 1.2, 1e-13, 10_000);
125 assert!(r.converged);
126 assert!(r.x.iter().all(|&v| v >= 1.0 - 1e-12 && v <= 2.0 + 1e-12), "{:?}", r.x);
127 let free = thomas_algorithm(&A, &B, &C, &D);
129 assert!(free.iter().any(|&v| v > 2.0));
130 assert!(r.x.iter().any(|&v| (v - 2.0).abs() < 1e-10));
131 }
132}