1use super::{DpcaReconstruction, DpcaResult, SpectralDensityResult};
37use crate::error::FdarError;
38use crate::helpers::simpsons_weights;
39use crate::matrix::FdMatrix;
40use nalgebra::DMatrix;
41use rustfft::num_complex::Complex;
42use rustfft::FftPlanner;
43
44fn validate_fts_input(data: &FdMatrix, argvals: &[f64]) -> Result<(usize, usize), FdarError> {
49 let (n, m) = data.shape();
50 if n == 0 || m == 0 {
51 return Err(FdarError::InvalidDimension {
52 parameter: "data",
53 expected: "non-empty matrix".to_string(),
54 actual: format!("{n} rows, {m} columns"),
55 });
56 }
57 if argvals.len() != m {
58 return Err(FdarError::InvalidDimension {
59 parameter: "argvals",
60 expected: format!("{m} elements (matching data columns)"),
61 actual: format!("{} elements", argvals.len()),
62 });
63 }
64 Ok((n, m))
65}
66
67fn mean_curve(data: &FdMatrix, n: usize, m: usize) -> Vec<f64> {
69 let mut xbar = vec![0.0f64; m];
70 let inv_n = 1.0 / n as f64;
71 for (j, xb) in xbar.iter_mut().enumerate() {
72 let mut s = 0.0;
73 for i in 0..n {
74 s += data[(i, j)];
75 }
76 *xb = s * inv_n;
77 }
78 xbar
79}
80
81#[inline]
83fn bartlett_weight(h: usize, bandwidth: usize) -> f64 {
84 1.0 - (h as f64) / (bandwidth as f64)
85}
86
87#[must_use = "the estimated spectral density operator is the return value and should be used"]
114pub fn spectral_density(
115 data: &FdMatrix,
116 argvals: &[f64],
117 bandwidth: Option<usize>,
118) -> Result<SpectralDensityResult, FdarError> {
119 let (n, m) = validate_fts_input(data, argvals)?;
120
121 let resolved_bandwidth = match bandwidth {
122 None => (n as f64).cbrt().floor().max(1.0) as usize,
123 Some(0) => {
124 return Err(FdarError::InvalidParameter {
125 parameter: "bandwidth",
126 message: "must be >= 1".to_string(),
127 });
128 }
129 Some(b) => b,
130 };
131 let max_h = resolved_bandwidth.min(n - 1);
133
134 let xbar = mean_curve(data, n, m);
135 let mut lag_ops: Vec<Vec<f64>> = Vec::with_capacity(max_h + 1);
137 for h in 0..=max_h {
138 lag_ops.push(super::acf::autocovariance_matrix(data, &xbar, h, n, m));
139 }
140
141 let n_freq = n;
142 let mut planner = FftPlanner::<f64>::new();
143 let fft = planner.plan_fft_forward(n_freq);
144
145 let mut re = vec![vec![0.0f64; m * m]; n_freq];
146 let mut im = vec![vec![0.0f64; m * m]; n_freq];
147
148 for j1 in 0..m {
149 for j2 in 0..m {
150 let mut buf = vec![Complex::new(0.0, 0.0); n_freq];
151 for (h, c_h) in lag_ops.iter().enumerate() {
153 let w_h = bartlett_weight(h, resolved_bandwidth);
154 buf[h] += Complex::new(w_h * c_h[j1 + j2 * m], 0.0);
155 }
156 for (h, c_h) in lag_ops.iter().enumerate().skip(1) {
158 let w_h = bartlett_weight(h, resolved_bandwidth);
159 buf[n_freq - h] += Complex::new(w_h * c_h[j2 + j1 * m], 0.0);
160 }
161 fft.process(&mut buf);
162 for k in 0..n_freq {
163 re[k][j1 + j2 * m] = buf[k].re;
164 im[k][j1 + j2 * m] = buf[k].im;
165 }
166 }
167 }
168
169 let freqs: Vec<f64> = (0..n_freq)
170 .map(|k| 2.0 * std::f64::consts::PI * (k as f64) / (n as f64))
171 .collect();
172
173 Ok(SpectralDensityResult {
174 freqs,
175 re,
176 im,
177 m,
178 n_curves: n,
179 bandwidth: resolved_bandwidth,
180 })
181}
182
183fn eigen_at_frequency(
191 spec_real: &[f64],
192 m: usize,
193 ncomp: usize,
194 sqrt_w: &[f64],
195) -> (Vec<f64>, Vec<Vec<f64>>) {
196 let mut mat = DMatrix::from_fn(m, m, |j1, j2| {
199 spec_real[j1 + j2 * m] * sqrt_w[j1] * sqrt_w[j2]
200 });
201 for j1 in 0..m {
202 for j2 in (j1 + 1)..m {
203 let avg = 0.5 * (mat[(j1, j2)] + mat[(j2, j1)]);
204 mat[(j1, j2)] = avg;
205 mat[(j2, j1)] = avg;
206 }
207 }
208 let eig = nalgebra::SymmetricEigen::new(mat);
209 let mut idx: Vec<usize> = (0..m).collect();
213 idx.sort_by(|&a, &b| {
214 eig.eigenvalues[b]
215 .partial_cmp(&eig.eigenvalues[a])
216 .unwrap_or(std::cmp::Ordering::Equal)
217 });
218 let take = ncomp.min(m);
219 let mut eigenvalues: Vec<f64> = Vec::with_capacity(take);
220 let mut eigenvectors: Vec<Vec<f64>> = Vec::with_capacity(take);
221 for &col in idx.iter().take(take) {
222 eigenvalues.push(eig.eigenvalues[col]);
223 let mut evec: Vec<f64> = eig.eigenvectors.column(col).iter().copied().collect();
224 let mut arg = 0usize;
226 let mut best = 0.0f64;
227 for (i, &x) in evec.iter().enumerate() {
228 if x.abs() > best {
229 best = x.abs();
230 arg = i;
231 }
232 }
233 if evec[arg] < 0.0 {
234 evec.iter_mut().for_each(|x| *x = -*x);
235 }
236 eigenvectors.push(evec);
237 }
238 (eigenvalues, eigenvectors)
239}
240
241#[must_use = "the DPCA filters and scores are the return value and should be used"]
266pub fn dpca(
267 data: &FdMatrix,
268 argvals: &[f64],
269 ncomp: usize,
270 bandwidth: Option<usize>,
271 filter_lag: Option<usize>,
272) -> Result<DpcaResult, FdarError> {
273 let (n, m) = validate_fts_input(data, argvals)?;
274 if ncomp == 0 || ncomp > m {
275 return Err(FdarError::InvalidParameter {
276 parameter: "ncomp",
277 message: format!("must be in 1..={m}"),
278 });
279 }
280
281 let sd = spectral_density(data, argvals, bandwidth)?;
282 let l = filter_lag.unwrap_or(sd.bandwidth);
283 if l >= n / 2 {
284 return Err(FdarError::InvalidParameter {
285 parameter: "filter_lag",
286 message: format!("must be < N/2 = {}", n / 2),
287 });
288 }
289
290 let weights = simpsons_weights(argvals);
291 let sqrt_w: Vec<f64> = weights.iter().map(|w| w.sqrt()).collect();
292 let n_freq = sd.n_curves;
293
294 let mut eigenvalues = vec![vec![0.0f64; n_freq]; ncomp];
296 let mut freq_vecs: Vec<Vec<Vec<f64>>> = Vec::with_capacity(n_freq);
298 for k in 0..n_freq {
299 let (vals, vecs) = eigen_at_frequency(&sd.re[k], m, ncomp, &sqrt_w);
300 for c in 0..ncomp {
301 eigenvalues[c][k] = vals[c].max(0.0); }
303 freq_vecs.push(vecs);
304 }
305
306 let mut inv_planner = FftPlanner::<f64>::new();
308 let ifft = inv_planner.plan_fft_inverse(n_freq);
309 let inv_n = 1.0 / (n_freq as f64);
310 let n_rows = 2 * l + 1;
311 let mut filters: Vec<FdMatrix> = Vec::with_capacity(ncomp);
312 for c in 0..ncomp {
313 let mut filt = vec![0.0f64; n_rows * m]; for j in 0..m {
315 let mut buf: Vec<Complex<f64>> = (0..n_freq)
316 .map(|k| Complex::new(freq_vecs[k][c][j], 0.0))
317 .collect();
318 ifft.process(&mut buf);
319 let inv_sw = 1.0 / sqrt_w[j];
320 for lag in 0..=l {
321 let tap = buf[lag].re * inv_n * inv_sw;
322 let row_pos = l + lag; let row_neg = l - lag; filt[row_pos + j * n_rows] = tap;
327 filt[row_neg + j * n_rows] = tap;
328 }
329 }
330 filters.push(
331 FdMatrix::from_column_major(filt, n_rows, m)
332 .expect("dimension invariant: filt.len() == (2L+1) * m"),
333 );
334 }
335
336 let n_interior = n - 2 * l;
338 let mut scores_flat = vec![0.0f64; n_interior * ncomp];
339 for (c, filt) in filters.iter().enumerate() {
340 for t in l..=(n - 1 - l) {
341 let mut s = 0.0;
342 for lag_idx in 0..n_rows {
343 let lag = lag_idx as isize - l as isize;
344 let ct = (t as isize + lag) as usize;
345 for j in 0..m {
346 s += filt[(lag_idx, j)] * data[(ct, j)] * weights[j];
347 }
348 }
349 scores_flat[(t - l) + c * n_interior] = s;
350 }
351 }
352 let scores = FdMatrix::from_column_major(scores_flat, n_interior, ncomp)
353 .expect("dimension invariant: scores.len() == (N-2L) * ncomp");
354
355 Ok(DpcaResult {
356 filters,
357 scores,
358 eigenvalues,
359 n_freqs: n_freq,
360 filter_lag: l,
361 ncomp,
362 valid_range: (l, n - 1 - l),
363 })
364}
365
366#[must_use = "the reconstruction and its per-component error are the return value"]
381pub fn dpca_reconstruct(
382 data: &FdMatrix,
383 argvals: &[f64],
384 dpca: &DpcaResult,
385) -> Result<DpcaReconstruction, FdarError> {
386 let (n, m) = validate_fts_input(data, argvals)?;
387 let l = dpca.filter_lag;
388 let ncomp = dpca.ncomp;
389 if n < 2 * l + 1 {
393 return Err(FdarError::InvalidDimension {
394 parameter: "data",
395 expected: format!("at least {} rows (2*filter_lag + 1)", 2 * l + 1),
396 actual: format!("{n} rows"),
397 });
398 }
399 let n_interior = n - 2 * l;
400
401 if dpca.scores.nrows() != n_interior || dpca.scores.ncols() != ncomp {
403 return Err(FdarError::InvalidDimension {
404 parameter: "dpca.scores",
405 expected: format!("{n_interior} rows × {ncomp} cols (matching data/filter_lag)"),
406 actual: format!(
407 "{} rows × {} cols",
408 dpca.scores.nrows(),
409 dpca.scores.ncols()
410 ),
411 });
412 }
413 if let Some(f0) = dpca.filters.first() {
414 if f0.ncols() != m {
415 return Err(FdarError::InvalidDimension {
416 parameter: "dpca.filters",
417 expected: format!("{m} columns (matching data grid)"),
418 actual: format!("{} columns", f0.ncols()),
419 });
420 }
421 }
422
423 let weights = simpsons_weights(argvals);
424 let n_rows = 2 * l + 1;
425
426 let mut fitted_flat = vec![0.0f64; n_interior * m];
429 for c in 0..ncomp {
430 let filt = &dpca.filters[c];
431 for t in l..=(n - 1 - l) {
432 let row = t - l; for lag_idx in 0..n_rows {
434 let lag = lag_idx as isize - l as isize;
435 let s_idx = row as isize + lag; if s_idx < 0 || s_idx as usize >= n_interior {
437 continue; }
439 let s = dpca.scores[(s_idx as usize, c)];
440 for j in 0..m {
441 fitted_flat[row + j * n_interior] += filt[(lag_idx, j)] * s;
442 }
443 }
444 }
445 }
446 let fitted = FdMatrix::from_column_major(fitted_flat, n_interior, m)
447 .expect("dimension invariant: fitted.len() == (N-2L) * m");
448
449 let lo = 2 * l;
452 let hi = n.saturating_sub(2 * l + 1);
453 let mut reconstruction_error = vec![0.0f64; ncomp];
454 for k in 1..=ncomp {
455 let mut err = 0.0;
456 let mut count = 0usize;
457 for t in lo..=hi {
458 let row = t - l;
459 for j in 0..m {
460 let mut xhat = 0.0;
461 for c in 0..k {
462 let filt = &dpca.filters[c];
463 for lag_idx in 0..n_rows {
464 let lag = lag_idx as isize - l as isize;
465 let s_idx = (row as isize + lag) as usize;
466 xhat += filt[(lag_idx, j)] * dpca.scores[(s_idx, c)];
467 }
468 }
469 let d = data[(t, j)] - xhat;
470 err += d * d * weights[j];
471 }
472 count += 1;
473 }
474 reconstruction_error[k - 1] = if count > 0 { err / count as f64 } else { 0.0 };
475 }
476
477 Ok(DpcaReconstruction {
478 fitted,
479 reconstruction_error,
480 valid_range: dpca.valid_range,
481 })
482}
483
484#[cfg(test)]
485mod tests {
486 use super::*;
487 use rand::rngs::StdRng;
488 use rand::{Rng, SeedableRng};
489
490 fn uniform_grid(m: usize) -> Vec<f64> {
491 (0..m).map(|j| j as f64 / (m - 1) as f64).collect()
492 }
493
494 fn white_noise(n: usize, m: usize, seed: u64) -> FdMatrix {
496 let mut rng = StdRng::seed_from_u64(seed);
497 let mut v = vec![0.0f64; n * m];
498 for x in &mut v {
499 *x = rng.sample::<f64, _>(rand_distr::StandardNormal);
500 }
501 FdMatrix::from_column_major(v, n, m).unwrap()
502 }
503
504 fn multimode_series(n: usize, m: usize, ar: &[f64], seed: u64) -> FdMatrix {
506 let r = ar.len();
507 let grid = uniform_grid(m);
508 let shapes: Vec<Vec<f64>> = (0..r)
510 .map(|c| {
511 grid.iter()
512 .map(|&t| ((c + 1) as f64 * std::f64::consts::PI * t).cos())
513 .collect()
514 })
515 .collect();
516 let mut rng = StdRng::seed_from_u64(seed);
517 let mut b = vec![0.0f64; r];
518 let mut v = vec![0.0f64; n * m];
519 for _ in 0..200 {
521 for c in 0..r {
522 b[c] = ar[c] * b[c] + rng.sample::<f64, _>(rand_distr::StandardNormal);
523 }
524 }
525 for i in 0..n {
526 for c in 0..r {
527 b[c] = ar[c] * b[c] + rng.sample::<f64, _>(rand_distr::StandardNormal);
528 }
529 for j in 0..m {
530 let mut s = 0.0;
531 for c in 0..r {
532 s += b[c] * shapes[c][j];
533 }
534 v[i + j * n] = s;
535 }
536 }
537 FdMatrix::from_column_major(v, n, m).unwrap()
538 }
539
540 #[test]
543 fn tracer_white_noise_flat() {
544 let (n, m) = (120, 6);
545 let argvals = uniform_grid(m);
546 let data = white_noise(n, m, 7);
547 let sd = spectral_density(&data, &argvals, None).unwrap();
548
549 assert_eq!(sd.freqs.len(), n);
551 assert_eq!(sd.re.len(), n);
552 assert_eq!(sd.im.len(), n);
553 assert_eq!(sd.re[0].len(), m * m);
554
555 let xbar = mean_curve(&data, n, m);
557 let bw = sd.bandwidth;
558 let max_h = bw.min(n - 1);
559 let lag_ops: Vec<Vec<f64>> = (0..=max_h)
560 .map(|h| super::super::acf::autocovariance_matrix(&data, &xbar, h, n, m))
561 .collect();
562 for &k in &[0usize, 1, 5, 37, n / 2, n - 1] {
563 let theta = 2.0 * std::f64::consts::PI * (k as f64) / (n as f64);
564 for &(j1, j2) in &[(0usize, 0usize), (1, 3), (4, 2)] {
565 let mut val = Complex::new(0.0, 0.0);
566 for (h, c_h) in lag_ops.iter().enumerate() {
567 let w = bartlett_weight(h, bw);
568 let e_neg = Complex::new((h as f64 * theta).cos(), -(h as f64 * theta).sin());
569 val += Complex::new(w * c_h[j1 + j2 * m], 0.0) * e_neg;
570 if h > 0 {
571 let e_pos =
572 Complex::new((h as f64 * theta).cos(), (h as f64 * theta).sin());
573 val += Complex::new(w * c_h[j2 + j1 * m], 0.0) * e_pos;
574 }
575 }
576 assert!((sd.re[k][j1 + j2 * m] - val.re).abs() < 1e-9);
577 assert!((sd.im[k][j1 + j2 * m] - val.im).abs() < 1e-9);
578 }
579 }
580
581 assert!(sd.re[0][0] > 0.0);
583
584 let mean_diag: f64 = (0..n).map(|k| sd.re[k][0]).sum::<f64>() / n as f64;
587 let c0_00 = lag_ops[0][0];
588 for k in 0..n {
589 assert!((sd.re[k][0] - mean_diag).abs() < 0.6 * c0_00.abs());
590 }
591 }
592
593 #[test]
594 fn spectral_density_errors_empty_and_argvals() {
595 let argvals = uniform_grid(5);
596 let empty = FdMatrix::from_column_major(vec![], 0, 0).unwrap();
597 assert!(matches!(
598 spectral_density(&empty, &[], None),
599 Err(FdarError::InvalidDimension {
600 parameter: "data",
601 ..
602 })
603 ));
604 let data = white_noise(20, 5, 1);
605 assert!(matches!(
606 spectral_density(&data, &argvals[..3], None),
607 Err(FdarError::InvalidDimension {
608 parameter: "argvals",
609 ..
610 })
611 ));
612 assert!(matches!(
613 spectral_density(&data, &argvals, Some(0)),
614 Err(FdarError::InvalidParameter {
615 parameter: "bandwidth",
616 ..
617 })
618 ));
619 }
620
621 #[test]
622 fn spectral_density_deterministic() {
623 let (n, m) = (30, 5);
624 let argvals = uniform_grid(m);
625 let d1 = white_noise(n, m, 99);
626 let d2 = white_noise(n, m, 99);
627 let s1 = spectral_density(&d1, &argvals, None).unwrap();
628 let s2 = spectral_density(&d2, &argvals, None).unwrap();
629 assert_eq!(s1, s2);
630 }
631
632 #[test]
633 fn spectral_density_hermitian_symmetry() {
634 let (n, m) = (60, 6);
635 let argvals = uniform_grid(m);
636 let data = multimode_series(n, m, &[0.7, 0.4], 11);
637 let sd = spectral_density(&data, &argvals, None).unwrap();
638 for &k in &[1usize, 7, 23] {
639 for &(j1, j2) in &[(0usize, 2usize), (1, 5), (3, 4)] {
640 let a = sd.im[k][j1 + j2 * m];
641 let b = sd.im[k][j2 + j1 * m];
642 assert!((a + b).abs() < 1e-9, "Hermitian im antisymmetry at k={k}");
643 }
644 }
645 }
646
647 #[test]
650 fn dpca_shapes_and_finiteness() {
651 let (n, m, ncomp) = (100, 12, 3);
652 let argvals = uniform_grid(m);
653 let data = multimode_series(n, m, &[0.7, 0.5, 0.3], 5);
654 let res = dpca(&data, &argvals, ncomp, None, None).unwrap();
655 let l = res.filter_lag;
656 assert_eq!(res.filters.len(), ncomp);
657 for f in &res.filters {
658 assert_eq!(f.shape(), (2 * l + 1, m));
659 assert!(f.as_slice().iter().all(|x| x.is_finite()));
660 }
661 assert_eq!(res.scores.shape(), (n - 2 * l, ncomp));
662 assert!(res.scores.as_slice().iter().all(|x| x.is_finite()));
663 assert_eq!(res.eigenvalues.len(), ncomp);
664 for ev in &res.eigenvalues {
665 assert_eq!(ev.len(), res.n_freqs);
666 }
667 assert_eq!(res.valid_range, (l, n - 1 - l));
668 }
669
670 #[test]
671 fn dpca_white_noise_flat_eigenvalues() {
672 let (n, m, ncomp) = (120, 8, 2);
673 let argvals = uniform_grid(m);
674 let data = white_noise(n, m, 3);
675 let res = dpca(&data, &argvals, ncomp, None, None).unwrap();
676 let lead = &res.eigenvalues[0];
678 let mean: f64 = lead.iter().sum::<f64>() / lead.len() as f64;
679 assert!(mean > 0.0);
680 for &v in lead {
681 assert!((v - mean).abs() < 0.7 * mean);
682 }
683 }
684
685 #[test]
686 fn dpca_parameter_range_errors() {
687 let (n, m) = (60, 6);
688 let argvals = uniform_grid(m);
689 let data = multimode_series(n, m, &[0.6], 2);
690 assert!(matches!(
691 dpca(&data, &argvals, 0, None, None),
692 Err(FdarError::InvalidParameter {
693 parameter: "ncomp",
694 ..
695 })
696 ));
697 assert!(matches!(
698 dpca(&data, &argvals, m + 1, None, None),
699 Err(FdarError::InvalidParameter {
700 parameter: "ncomp",
701 ..
702 })
703 ));
704 assert!(matches!(
705 dpca(&data, &argvals, 2, None, Some(n / 2)),
706 Err(FdarError::InvalidParameter {
707 parameter: "filter_lag",
708 ..
709 })
710 ));
711 }
712
713 #[test]
716 fn dpca_reconstruct_monotone_error() {
717 let (n, m, ncomp) = (100, 16, 3);
718 let argvals = uniform_grid(m);
719 let data = multimode_series(n, m, &[0.7, 0.5, 0.3], 21);
720 let res = dpca(&data, &argvals, ncomp, None, Some(2)).unwrap();
721 let rec = dpca_reconstruct(&data, &argvals, &res).unwrap();
722 assert_eq!(rec.reconstruction_error.len(), ncomp);
723 for k in 0..ncomp - 1 {
724 assert!(
725 rec.reconstruction_error[k] >= rec.reconstruction_error[k + 1] - 1e-9,
726 "error not monotone at K={k}: {:?}",
727 rec.reconstruction_error
728 );
729 }
730 assert_eq!(rec.valid_range, res.valid_range);
731 }
732
733 #[test]
734 fn dpca_reconstruct_rank1_exact() {
735 let (n, m) = (80, 20);
737 let argvals = uniform_grid(m);
738 let phi: Vec<f64> = argvals
739 .iter()
740 .map(|&t| (2.0 * std::f64::consts::PI * t).sin())
741 .collect();
742 let mut rng = StdRng::seed_from_u64(42);
743 let mut a = 0.0f64;
744 for _ in 0..200 {
745 a = 0.8 * a + rng.sample::<f64, _>(rand_distr::StandardNormal);
746 }
747 let mut v = vec![0.0f64; n * m];
748 for i in 0..n {
749 a = 0.8 * a + rng.sample::<f64, _>(rand_distr::StandardNormal);
750 for j in 0..m {
751 v[i + j * n] = a * phi[j];
752 }
753 }
754 let data = FdMatrix::from_column_major(v, n, m).unwrap();
755 let res = dpca(&data, &argvals, 3, None, Some(3)).unwrap();
756 let rec = dpca_reconstruct(&data, &argvals, &res).unwrap();
757 assert_eq!(rec.fitted.shape(), (n - 2 * res.filter_lag, m));
758 assert!(
760 rec.reconstruction_error[0] < 1e-4,
761 "rank-1 K=1 error too large: {}",
762 rec.reconstruction_error[0]
763 );
764 }
765
766 #[test]
767 fn dpca_reconstruct_dimension_mismatch() {
768 let (n, m) = (60, 6);
769 let argvals = uniform_grid(m);
770 let data = multimode_series(n, m, &[0.6, 0.4], 8);
771 let res = dpca(&data, &argvals, 2, None, Some(2)).unwrap();
772 assert!(matches!(
774 dpca_reconstruct(&data, &argvals[..m - 1], &res),
775 Err(FdarError::InvalidDimension { .. })
776 ));
777 }
778
779 #[test]
780 fn dpca_reconstruct_short_data_errors_not_panics() {
781 let (n, m) = (60, 6);
784 let argvals = uniform_grid(m);
785 let data = multimode_series(n, m, &[0.6, 0.4], 8);
786 let res = dpca(&data, &argvals, 2, None, Some(4)).unwrap();
787 let short = multimode_series(2 * res.filter_lag, m, &[0.6, 0.4], 9); assert!(matches!(
789 dpca_reconstruct(&short, &argvals, &res),
790 Err(FdarError::InvalidDimension {
791 parameter: "data",
792 ..
793 })
794 ));
795 }
796}