1use crate::linalg::SquareMatrix;
20use crate::{check_input, Error, Result};
21
22const QN_CONSTANT: f64 = 2.21914;
24
25pub fn qn(x: &[f64]) -> Result<f64> {
30 let n = x.len();
31 if n < 2 {
32 return Ok(0.0);
33 }
34 if x.iter().any(|v| !v.is_finite()) {
35 return Err(Error::NonFinite);
36 }
37 let raw = qn_raw(x);
38 let corr = match n {
40 2 => 0.399356,
41 3 => 0.99365,
42 4 => 0.51321,
43 5 => 0.84401,
44 6 => 0.6122,
45 7 => 0.85877,
46 8 => 0.66993,
47 9 => 0.87344,
48 10 => 0.72014,
49 11 => 0.88906,
50 12 => 0.75743,
51 _ => {
52 let nf = n as f64;
53 let c = if n % 2 == 1 {
54 1.60188 + (-2.1284 - 5.172 / nf) / nf
55 } else {
56 3.67561 + (1.9654 + (6.987 - 77.0 / nf) / nf) / nf
57 };
58 1.0 / (c / nf + 1.0)
59 }
60 };
61 Ok(corr * QN_CONSTANT * raw)
62}
63
64fn qn_raw(x: &[f64]) -> f64 {
71 let n = x.len();
72 let h = n / 2 + 1;
73 let k = h * (h - 1) / 2;
74 let mut y = vec![0.0]; {
76 let mut s = x.to_vec();
77 s.sort_unstable_by(f64::total_cmp);
78 y.extend_from_slice(&s);
79 }
80 let mut left = vec![0usize; n + 1];
81 let mut right = vec![0usize; n + 1];
82 for i in 1..=n {
83 left[i] = n - i + 2; right[i] = n;
85 }
86 let nonpos = n * (n + 1) / 2; let knew = k + nonpos;
88 let mut nl = nonpos;
89 let mut nr = n * n;
90 let mut work: Vec<f64> = Vec::with_capacity(n);
91 let mut weight: Vec<i64> = Vec::with_capacity(n);
92 let mut p = vec![0usize; n + 1];
93 let mut q = vec![0usize; n + 1];
94 while nr - nl > n {
95 work.clear();
97 weight.clear();
98 for i in 2..=n {
99 if left[i] <= right[i] {
100 let w = right[i] - left[i] + 1;
101 let jh = left[i] + w / 2;
102 work.push(y[i] - y[n + 1 - jh]);
103 weight.push(w as i64);
104 }
105 }
106 let trial = weighted_high_median(&mut work, &mut weight);
107 let mut j = 0usize;
110 for i in (1..=n).rev() {
111 while j < n && y[i] - y[n - j] < trial {
112 j += 1;
113 }
114 p[i] = j;
115 }
116 let mut j = n + 1;
118 for i in 1..=n {
119 while j > 2 && y[i] - y[n + 2 - j] > trial {
120 j -= 1;
121 }
122 q[i] = j;
123 }
124 let sum_p: usize = p[1..].iter().sum();
125 let sum_q: usize = q[1..].iter().map(|v| v - 1).sum();
126 if knew <= sum_p {
127 right[1..=n].copy_from_slice(&p[1..=n]);
128 nr = sum_p;
129 } else if knew > sum_q {
130 left[1..=n].copy_from_slice(&q[1..=n]);
131 nl = sum_q;
132 } else {
133 return trial;
134 }
135 }
136 work.clear();
138 for i in 2..=n {
139 for jj in left[i]..=right[i] {
140 work.push(y[i] - y[n - jj + 1]);
141 }
142 }
143 select_kth(&mut work, knew - nl - 1)
144}
145
146fn select_kth(a: &mut [f64], k: usize) -> f64 {
148 a.sort_unstable_by(f64::total_cmp);
149 a[k]
150}
151
152fn weighted_high_median(a: &mut Vec<f64>, w: &mut Vec<i64>) -> f64 {
155 let wtotal: i64 = w.iter().sum();
156 let mut wrest: i64 = 0;
157 loop {
158 let nn = a.len();
159 let mut buf = a.clone();
160 let trial = select_kth(&mut buf, nn / 2); let mut wleft = 0i64;
162 let mut wmid = 0i64;
163 for (v, wi) in a.iter().zip(w.iter()) {
164 if *v < trial {
165 wleft += wi;
166 } else if *v == trial {
167 wmid += wi;
168 }
169 }
170 if 2 * (wrest + wleft) > wtotal {
171 let mut ka = 0;
173 for i in 0..nn {
174 if a[i] < trial {
175 a[ka] = a[i];
176 w[ka] = w[i];
177 ka += 1;
178 }
179 }
180 a.truncate(ka);
181 w.truncate(ka);
182 } else if 2 * (wrest + wleft + wmid) > wtotal {
183 return trial;
184 } else {
185 let mut ka = 0;
186 for i in 0..nn {
187 if a[i] > trial {
188 a[ka] = a[i];
189 w[ka] = w[i];
190 ka += 1;
191 }
192 }
193 a.truncate(ka);
194 w.truncate(ka);
195 wrest += wleft + wmid;
196 }
197 }
198}
199
200pub fn robust_autocovariance(x: &[f64], max_lag: usize) -> Result<Vec<f64>> {
212 check_input(x, max_lag + 2)?;
213 let n = x.len();
214 let mut r = Vec::with_capacity(max_lag + 1);
215 let q0 = qn(x)?;
216 r.push(q0 * q0);
217 let mut s = Vec::with_capacity(n);
218 let mut d = Vec::with_capacity(n);
219 for h in 1..=max_lag {
220 s.clear();
221 d.clear();
222 for i in 0..n - h {
223 s.push(x[i] + x[i + h]);
224 d.push(x[i] - x[i + h]);
225 }
226 let qs = qn(&s)?;
227 let qd = qn(&d)?;
228 r.push(0.25 * (qs * qs - qd * qd));
229 }
230 Ok(r)
231}
232
233pub fn robust_autocorrelation(x: &[f64], max_lag: usize) -> Result<Vec<f64>> {
240 check_input(x, max_lag + 2)?;
241 let n = x.len();
242 let mut r = Vec::with_capacity(max_lag + 1);
243 r.push(1.0);
244 let mut s = Vec::with_capacity(n);
245 let mut d = Vec::with_capacity(n);
246 for h in 1..=max_lag {
247 s.clear();
248 d.clear();
249 for i in 0..n - h {
250 s.push(x[i] + x[i + h]);
251 d.push(x[i] - x[i + h]);
252 }
253 let qs = qn(&s)?;
254 let qd = qn(&d)?;
255 let denom = qs * qs + qd * qd;
256 if denom <= 0.0 {
257 return Err(Error::Singular);
258 }
259 r.push((qs * qs - qd * qd) / denom);
260 }
261 Ok(r)
262}
263
264pub fn shrink_to_psd(r: &[f64]) -> Result<Vec<f64>> {
273 if r.is_empty() || r[0] <= 0.0 {
274 return Err(Error::Singular);
275 }
276 let ridge = 1e-3 * r[0];
277 let is_pd = |lam: f64| -> bool {
278 let m = r.len();
279 let mut g = SquareMatrix::zeros(m);
280 for i in 0..m {
281 for j in 0..m {
282 let v = if i == j {
283 r[0] - ridge
284 } else {
285 (1.0 - lam) * r[i.abs_diff(j)]
286 };
287 g.set(i, j, v);
288 }
289 }
290 g.cholesky().is_ok()
291 };
292 if is_pd(0.0) {
293 return Ok(r.to_vec());
294 }
295 let (mut lo, mut hi) = (0.0f64, 1.0f64);
296 for _ in 0..40 {
297 let mid = 0.5 * (lo + hi);
298 if is_pd(mid) {
299 hi = mid;
300 } else {
301 lo = mid;
302 }
303 }
304 let lam = hi.min(1.0);
305 let mut out = r.to_vec();
306 for v in out.iter_mut().skip(1) {
307 *v *= 1.0 - lam;
308 }
309 Ok(out)
310}
311
312#[cfg(test)]
313mod tests {
314 use super::*;
315
316 #[test]
317 #[allow(clippy::excessive_precision)] fn qn_matches_robustbase_fixtures() {
319 let x = [
321 -0.62645381074233242,
322 0.18364332422208227,
323 -0.83562861241004716,
324 1.59528080213779155,
325 0.32950777181536051,
326 -0.82046838411801548,
327 0.48742905242848528,
328 0.73832470512921722,
329 0.57578135165349209,
330 -0.30538838715635602,
331 ];
332 let q = qn(&x).unwrap();
333 assert!((q - 0.82314495445682179).abs() < 1e-14, "qn = {q}");
334 let y: Vec<f64> = (1..=8).map(|v| v as f64).collect();
336 assert!((qn_raw(&y) - 2.0).abs() < 1e-15);
337 let q8 = qn(&y).unwrap();
338 assert!((q8 - 2.9733369204).abs() < 1e-9, "qn(1:8) = {q8}");
339 let mut z: Vec<f64> = (1..=200).map(|v| v as f64).collect();
341 z[9] = 1000.0;
342 z[49] = -500.0;
343 z[119] = 800.0;
344 let q200 = qn(&z).unwrap();
345 assert!((q200 - 63.190583283547305).abs() < 1e-10, "qn = {q200}");
346 }
347
348 #[test]
349 fn qn_brute_force_agreement_and_equivariance() {
350 let mut s = 7u64;
352 let x: Vec<f64> = (0..101)
353 .map(|_| {
354 s = s
355 .wrapping_mul(6364136223846793005)
356 .wrapping_add(1442695040888963407);
357 (s >> 11) as f64 / (1u64 << 53) as f64
358 })
359 .collect();
360 let n = x.len();
361 let h = n / 2 + 1;
362 let k = h * (h - 1) / 2;
363 let mut dists = Vec::new();
364 for i in 0..n {
365 for j in i + 1..n {
366 dists.push((x[i] - x[j]).abs());
367 }
368 }
369 dists.sort_unstable_by(f64::total_cmp);
370 assert!((qn_raw(&x) - dists[k - 1]).abs() < 1e-15);
371 let y: Vec<f64> = x.iter().map(|v| 3.0 * v + 50.0).collect();
373 let (qx, qy) = (qn(&x).unwrap(), qn(&y).unwrap());
374 assert!((3.0 * qx - qy).abs() < 1e-12 * qy);
375 }
376
377 #[test]
378 fn robust_acov_ignores_gross_outliers() {
379 let mut s = 3u64;
382 let mut x = vec![0.0f64; 2000];
383 for t in 1..2000 {
384 s = s
385 .wrapping_mul(6364136223846793005)
386 .wrapping_add(1442695040888963407);
387 let u = (s >> 11) as f64 / (1u64 << 53) as f64 - 0.5;
388 x[t] = 0.7 * x[t - 1] + u;
389 }
390 let clean = robust_autocovariance(&x, 3).unwrap();
391 let rho_clean = clean[1] / clean[0];
392 let mut dirty = x.clone();
393 for t in (0..2000).step_by(97) {
394 dirty[t] += if t % 2 == 0 { 50.0 } else { -50.0 };
395 }
396 let rob = robust_autocovariance(&dirty, 3).unwrap();
397 let rho_rob = rob[1] / rob[0];
398 assert!(
399 (rho_rob - rho_clean).abs() < 0.1,
400 "{rho_clean} vs {rho_rob}"
401 );
402 }
403
404 #[test]
405 fn shrink_leaves_psd_untouched_and_repairs_non_psd() {
406 let ok = [1.0, 0.5, 0.25];
407 assert_eq!(shrink_to_psd(&ok).unwrap(), ok.to_vec());
408 let bad = [1.0, 0.9, -0.9];
410 let fixed = shrink_to_psd(&bad).unwrap();
411 assert!(fixed[1] < 0.9 && fixed[1] > 0.0);
412 let l = crate::levinson::levinson(&fixed).unwrap();
413 for k in &l.pacf {
414 assert!(k.abs() < 1.0, "pacf {k}");
415 }
416 }
417}