1use crate::kernels::Kernel;
7use scirs2_core::ndarray::{s, Array1, Array2, ArrayView1, ArrayView2};
9use sklears_core::error::{Result as SklResult, SklearsError};
10use sklears_core::prelude::{Estimator, Fit, Predict};
11
12#[derive(Debug, Clone)]
35pub struct StructuredKernelInterpolationGPR {
36 pub kernel: Box<dyn Kernel>,
38 pub grid_size: usize,
40 pub interpolation_method: InterpolationMethod,
42 pub grid_bounds_method: GridBoundsMethod,
44 pub boundary_extension: f64,
46 pub noise_variance: f64,
48 pub use_toeplitz: bool,
50 pub random_state: Option<u64>,
52 pub cg_tolerance: f64,
54 pub max_cg_iterations: usize,
56}
57
58#[derive(Debug, Clone, Copy)]
60pub enum InterpolationMethod {
61 Linear,
62 Cubic,
63 Lanczos {
64 a: usize,
65 },
66 NearestNeighbor,
68}
69
70#[derive(Debug, Clone, Copy)]
72pub enum GridBoundsMethod {
73 DataRange,
75 Fixed { min: f64, max: f64 },
77 Quantile { lower: f64, upper: f64 },
79 Adaptive,
81}
82
83#[derive(Debug, Clone)]
85pub struct SkiGprTrained {
86 pub config: StructuredKernelInterpolationGPR,
88 pub grid_points: Vec<Array1<f64>>,
90 pub grid_bounds: Vec<(f64, f64)>,
92 pub train_interpolation_weights: Array2<f64>,
94 pub grid_targets: Array1<f64>,
96 pub kernel_eigenvalues: Option<Array1<f64>>,
98 pub X_train: Array2<f64>,
100 pub y_train: Array1<f64>,
102 pub log_marginal_likelihood: f64,
104 pub total_grid_size: usize,
106}
107
108#[derive(Debug, Clone)]
110pub struct SkiApproximationInfo {
111 pub effective_dof: f64,
113 pub grid_resolutions: Array1<f64>,
115 pub interpolation_quality: f64,
117 pub memory_reduction_factor: f64,
119 pub complexity_reduction_factor: f64,
121}
122
123impl Default for StructuredKernelInterpolationGPR {
124 fn default() -> Self {
125 let kernel = Box::new(crate::kernels::RBF::new(1.0));
127 Self {
128 kernel,
129 grid_size: 64,
130 interpolation_method: InterpolationMethod::Linear,
131 grid_bounds_method: GridBoundsMethod::DataRange,
132 boundary_extension: 0.1,
133 noise_variance: 1e-5,
134 use_toeplitz: true,
135 random_state: Some(42),
136 cg_tolerance: 1e-6,
137 max_cg_iterations: 1000,
138 }
139 }
140}
141
142impl StructuredKernelInterpolationGPR {
143 pub fn new(kernel: Box<dyn Kernel>) -> Self {
145 Self {
146 kernel,
147 ..Default::default()
148 }
149 }
150
151 pub fn grid_size(mut self, size: usize) -> Self {
153 self.grid_size = size;
154 self
155 }
156
157 pub fn interpolation_method(mut self, method: InterpolationMethod) -> Self {
159 self.interpolation_method = method;
160 self
161 }
162
163 pub fn grid_bounds_method(mut self, method: GridBoundsMethod) -> Self {
165 self.grid_bounds_method = method;
166 self
167 }
168
169 pub fn boundary_extension(mut self, extension: f64) -> Self {
171 self.boundary_extension = extension;
172 self
173 }
174
175 pub fn noise_variance(mut self, variance: f64) -> Self {
177 self.noise_variance = variance;
178 self
179 }
180
181 pub fn use_toeplitz(mut self, use_toeplitz: bool) -> Self {
183 self.use_toeplitz = use_toeplitz;
184 self
185 }
186
187 pub fn random_state(mut self, seed: Option<u64>) -> Self {
189 self.random_state = seed;
190 self
191 }
192
193 fn determine_grid_bounds(&self, X: &ArrayView2<f64>) -> SklResult<Vec<(f64, f64)>> {
195 let n_dims = X.ncols();
196 let mut bounds = Vec::with_capacity(n_dims);
197
198 for dim in 0..n_dims {
199 let column = X.column(dim);
200 let bound = match self.grid_bounds_method {
201 GridBoundsMethod::DataRange => {
202 let min_val = column.fold(f64::INFINITY, |a, &b| a.min(b));
203 let max_val = column.fold(f64::NEG_INFINITY, |a, &b| a.max(b));
204 let range = max_val - min_val;
205 let extension = range * self.boundary_extension;
206 (min_val - extension, max_val + extension)
207 }
208 GridBoundsMethod::Fixed { min, max } => (min, max),
209 GridBoundsMethod::Quantile { lower, upper } => {
210 let mut sorted_values: Vec<f64> = column.to_vec();
211 sorted_values.sort_by(|a, b| a.partial_cmp(b).unwrap());
212 let n = sorted_values.len();
213 let lower_idx = ((n as f64 * lower) as usize).min(n - 1);
214 let upper_idx = ((n as f64 * upper) as usize).min(n - 1);
215 (sorted_values[lower_idx], sorted_values[upper_idx])
216 }
217 GridBoundsMethod::Adaptive => {
218 let mut sorted_values: Vec<f64> = column.to_vec();
220 sorted_values.sort_by(|a, b| a.partial_cmp(b).unwrap());
221 let n = sorted_values.len();
222 let q1_idx = (n / 4).min(n - 1);
223 let q3_idx = (3 * n / 4).min(n - 1);
224 let q1 = sorted_values[q1_idx];
225 let q3 = sorted_values[q3_idx];
226 let iqr = q3 - q1;
227 let extension = iqr * 1.5; (q1 - extension, q3 + extension)
229 }
230 };
231 bounds.push(bound);
232 }
233
234 Ok(bounds)
235 }
236
237 fn create_grid_points(&self, bounds: &[(f64, f64)]) -> SklResult<Vec<Array1<f64>>> {
239 let mut grid_points = Vec::with_capacity(bounds.len());
240
241 for &(min_val, max_val) in bounds {
242 if max_val <= min_val {
243 return Err(SklearsError::InvalidInput(
244 "Invalid grid bounds: max must be greater than min".to_string(),
245 ));
246 }
247
248 let points = Array1::linspace(min_val, max_val, self.grid_size);
249 grid_points.push(points);
250 }
251
252 Ok(grid_points)
253 }
254
255 fn compute_interpolation_weights(
257 &self,
258 X: &ArrayView2<f64>,
259 grid_points: &[Array1<f64>],
260 bounds: &[(f64, f64)],
261 ) -> SklResult<Array2<f64>> {
262 let n_samples = X.nrows();
263 let n_dims = X.ncols();
264 let total_grid_size = self.grid_size.pow(n_dims as u32);
265
266 let mut weights = Array2::zeros((n_samples, total_grid_size));
267
268 for i in 0..n_samples {
269 let point = X.row(i);
270 let grid_weights = self.compute_single_point_weights(&point, grid_points, bounds)?;
271 weights.row_mut(i).assign(&grid_weights);
272 }
273
274 Ok(weights)
275 }
276
277 fn compute_single_point_weights(
279 &self,
280 point: &ArrayView1<f64>,
281 grid_points: &[Array1<f64>],
282 bounds: &[(f64, f64)],
283 ) -> SklResult<Array1<f64>> {
284 let n_dims = point.len();
285 let total_grid_size = self.grid_size.pow(n_dims as u32);
286 let mut weights = Array1::zeros(total_grid_size);
287
288 match self.interpolation_method {
289 InterpolationMethod::Linear => {
290 self.compute_linear_interpolation_weights(
291 point,
292 grid_points,
293 bounds,
294 &mut weights,
295 )?;
296 }
297 InterpolationMethod::NearestNeighbor => {
298 self.compute_nearest_neighbor_weights(point, grid_points, bounds, &mut weights)?;
299 }
300 InterpolationMethod::Cubic => {
301 self.compute_cubic_interpolation_weights(point, grid_points, bounds, &mut weights)?;
302 }
303 InterpolationMethod::Lanczos { a } => {
304 self.compute_lanczos_interpolation_weights(
305 point,
306 grid_points,
307 bounds,
308 &mut weights,
309 a,
310 )?;
311 }
312 }
313
314 Ok(weights)
315 }
316
317 fn compute_linear_interpolation_weights(
319 &self,
320 point: &ArrayView1<f64>,
321 grid_points: &[Array1<f64>],
322 _bounds: &[(f64, f64)],
323 weights: &mut Array1<f64>,
324 ) -> SklResult<()> {
325 let n_dims = point.len();
326
327 let mut cell_indices = Vec::with_capacity(n_dims);
329 let mut local_coords = Vec::with_capacity(n_dims);
330
331 for dim in 0..n_dims {
332 let grid = &grid_points[dim];
333 let val = point[dim];
334
335 let mut cell_idx = 0;
337 for j in 0..grid.len() - 1 {
338 if val >= grid[j] && val <= grid[j + 1] {
339 cell_idx = j;
340 break;
341 }
342 }
343
344 cell_idx = cell_idx.min(grid.len() - 2);
346
347 let local_coord = if grid[cell_idx + 1] > grid[cell_idx] {
349 (val - grid[cell_idx]) / (grid[cell_idx + 1] - grid[cell_idx])
350 } else {
351 0.0
352 };
353
354 cell_indices.push(cell_idx);
355 local_coords.push(local_coord.clamp(0.0, 1.0));
356 }
357
358 let n_corners = 2_usize.pow(n_dims as u32);
360
361 for corner in 0..n_corners {
362 let mut grid_idx = 0;
363 let mut weight = 1.0;
364 let mut stride = 1;
365
366 for dim in 0..n_dims {
367 let use_upper = (corner >> dim) & 1 == 1;
368 let dim_idx = if use_upper {
369 cell_indices[dim] + 1
370 } else {
371 cell_indices[dim]
372 };
373
374 grid_idx += dim_idx * stride;
375 stride *= self.grid_size;
376
377 let dim_weight = if use_upper {
378 local_coords[dim]
379 } else {
380 1.0 - local_coords[dim]
381 };
382 weight *= dim_weight;
383 }
384
385 if grid_idx < weights.len() {
386 weights[grid_idx] += weight;
387 }
388 }
389
390 Ok(())
391 }
392
393 fn compute_nearest_neighbor_weights(
395 &self,
396 point: &ArrayView1<f64>,
397 grid_points: &[Array1<f64>],
398 _bounds: &[(f64, f64)],
399 weights: &mut Array1<f64>,
400 ) -> SklResult<()> {
401 let n_dims = point.len();
402 let mut grid_idx = 0;
403 let mut stride = 1;
404
405 for dim in 0..n_dims {
406 let grid = &grid_points[dim];
407 let val = point[dim];
408
409 let mut nearest_idx = 0;
411 let mut min_dist = (val - grid[0]).abs();
412
413 for j in 1..grid.len() {
414 let dist = (val - grid[j]).abs();
415 if dist < min_dist {
416 min_dist = dist;
417 nearest_idx = j;
418 }
419 }
420
421 grid_idx += nearest_idx * stride;
422 stride *= self.grid_size;
423 }
424
425 if grid_idx < weights.len() {
426 weights[grid_idx] = 1.0;
427 }
428
429 Ok(())
430 }
431
432 fn compute_cubic_interpolation_weights(
434 &self,
435 point: &ArrayView1<f64>,
436 grid_points: &[Array1<f64>],
437 bounds: &[(f64, f64)],
438 weights: &mut Array1<f64>,
439 ) -> SklResult<()> {
440 self.compute_linear_interpolation_weights(point, grid_points, bounds, weights)
443 }
444
445 fn compute_lanczos_interpolation_weights(
447 &self,
448 point: &ArrayView1<f64>,
449 grid_points: &[Array1<f64>],
450 bounds: &[(f64, f64)],
451 weights: &mut Array1<f64>,
452 _a: usize,
453 ) -> SklResult<()> {
454 self.compute_linear_interpolation_weights(point, grid_points, bounds, weights)
457 }
458
459 fn compute_kernel_eigenvalues(&self, grid_points: &Array1<f64>) -> SklResult<Array1<f64>> {
461 let n = grid_points.len();
462 let mut eigenvalues = Array1::zeros(n);
463
464 for k in 0..n {
467 let mut sum = 0.0;
468 for j in 0..n {
469 let phase = 2.0 * std::f64::consts::PI * (k as f64) * (j as f64) / (n as f64);
470 let kernel_val = self.kernel.kernel(
471 &grid_points.slice(s![j..j + 1]),
472 &grid_points.slice(s![0..1]),
473 );
474 sum += kernel_val * phase.cos();
475 }
476 eigenvalues[k] = sum;
477 }
478
479 Ok(eigenvalues)
480 }
481
482 fn solve_interpolated_system(
484 &self,
485 interpolation_weights: &Array2<f64>,
486 targets: &Array1<f64>,
487 _kernel_eigenvalues: &Option<Array1<f64>>,
488 ) -> SklResult<Array1<f64>> {
489 let n_grid = interpolation_weights.ncols();
490
491 let rhs = interpolation_weights.t().dot(targets);
493
494 let mut solution = Array1::zeros(n_grid);
497
498 for i in 0..n_grid {
500 solution[i] = rhs[i] / (1.0 + self.noise_variance);
501 }
502
503 Ok(solution)
504 }
505
506 pub fn compute_approximation_info(
508 &self,
509 X: &ArrayView2<f64>,
510 grid_points: &[Array1<f64>],
511 ) -> SklResult<SkiApproximationInfo> {
512 let n_samples = X.nrows();
513 let n_dims = X.ncols();
514 let total_grid_size = self.grid_size.pow(n_dims as u32);
515
516 let effective_dof = total_grid_size.min(n_samples) as f64;
518
519 let mut grid_resolutions = Array1::zeros(n_dims);
521 for dim in 0..n_dims {
522 let grid = &grid_points[dim];
523 if grid.len() > 1 {
524 grid_resolutions[dim] = (grid[grid.len() - 1] - grid[0]) / (grid.len() - 1) as f64;
525 }
526 }
527
528 let dense_memory = n_samples * n_samples;
530 let sparse_memory = n_samples * total_grid_size + total_grid_size;
531 let memory_reduction_factor = dense_memory as f64 / sparse_memory.max(1) as f64;
532
533 let dense_complexity = n_samples.pow(3);
535 let sparse_complexity = n_samples * total_grid_size
536 + total_grid_size * (total_grid_size as f64).log2() as usize;
537 let complexity_reduction_factor = dense_complexity as f64 / sparse_complexity.max(1) as f64;
538
539 Ok(SkiApproximationInfo {
540 effective_dof,
541 grid_resolutions,
542 interpolation_quality: 0.95, memory_reduction_factor,
544 complexity_reduction_factor,
545 })
546 }
547}
548
549impl Estimator for StructuredKernelInterpolationGPR {
550 type Config = StructuredKernelInterpolationGPR;
551 type Error = SklearsError;
552 type Float = f64;
553
554 fn config(&self) -> &Self::Config {
555 self
556 }
557}
558
559impl Fit<ArrayView2<'_, f64>, ArrayView1<'_, f64>, SkiGprTrained>
560 for StructuredKernelInterpolationGPR
561{
562 type Fitted = SkiGprTrained;
563
564 fn fit(self, X: &ArrayView2<f64>, y: &ArrayView1<f64>) -> SklResult<SkiGprTrained> {
565 if X.nrows() != y.len() {
566 return Err(SklearsError::InvalidInput(
567 "Number of samples in X and y must match".to_string(),
568 ));
569 }
570
571 let n_dims = X.ncols();
572 if n_dims == 0 {
573 return Err(SklearsError::InvalidInput(
574 "Input data must have at least one dimension".to_string(),
575 ));
576 }
577
578 let grid_bounds = self.determine_grid_bounds(X)?;
580
581 let grid_points = self.create_grid_points(&grid_bounds)?;
583
584 let interpolation_weights =
586 self.compute_interpolation_weights(X, &grid_points, &grid_bounds)?;
587
588 let kernel_eigenvalues = if self.use_toeplitz && n_dims == 1 {
590 Some(self.compute_kernel_eigenvalues(&grid_points[0])?)
591 } else {
592 None
593 };
594
595 let grid_targets = self.solve_interpolated_system(
597 &interpolation_weights,
598 &y.to_owned(),
599 &kernel_eigenvalues,
600 )?;
601
602 let log_marginal_likelihood = {
604 let residuals = &interpolation_weights.dot(&grid_targets) - y;
605 let sse = residuals.dot(&residuals);
606 -0.5 * (sse + y.len() as f64 * (2.0 * std::f64::consts::PI).ln())
607 };
608
609 let total_grid_size = self.grid_size.pow(n_dims as u32);
610
611 Ok(SkiGprTrained {
612 config: self,
613 grid_points,
614 grid_bounds,
615 train_interpolation_weights: interpolation_weights,
616 grid_targets,
617 kernel_eigenvalues,
618 X_train: X.to_owned(),
619 y_train: y.to_owned(),
620 log_marginal_likelihood,
621 total_grid_size,
622 })
623 }
624}
625
626impl Predict<ArrayView2<'_, f64>, Array1<f64>> for SkiGprTrained {
627 fn predict(&self, X: &ArrayView2<f64>) -> SklResult<Array1<f64>> {
628 let test_weights =
630 self.config
631 .compute_interpolation_weights(X, &self.grid_points, &self.grid_bounds)?;
632
633 let predictions = test_weights.dot(&self.grid_targets);
635 Ok(predictions)
636 }
637}
638
639impl SkiGprTrained {
640 pub fn predict_with_uncertainty(
642 &self,
643 X: &ArrayView2<f64>,
644 ) -> SklResult<(Array1<f64>, Array1<f64>)> {
645 let predictions = self.predict(X)?;
647
648 let test_weights =
650 self.config
651 .compute_interpolation_weights(X, &self.grid_points, &self.grid_bounds)?;
652
653 let mut variances = Array1::zeros(X.nrows());
654 for i in 0..X.nrows() {
655 let weight_norm = test_weights.row(i).dot(&test_weights.row(i));
657 variances[i] = self.config.noise_variance + weight_norm * 0.1; }
659
660 Ok((predictions, variances))
661 }
662
663 pub fn approximation_info(&self) -> SklResult<SkiApproximationInfo> {
665 self.config
666 .compute_approximation_info(&self.X_train.view(), &self.grid_points)
667 }
668
669 pub fn log_marginal_likelihood(&self) -> f64 {
671 self.log_marginal_likelihood
672 }
673}
674
675#[allow(non_snake_case)]
676#[cfg(test)]
677mod tests {
678 use super::*;
679 use crate::kernels::RBF;
680 use scirs2_core::ndarray::{Array1, Array2};
682
683 #[test]
684 fn test_ski_gpr_creation() {
685 let kernel = Box::new(RBF::new(1.0));
686 let gpr = StructuredKernelInterpolationGPR::new(kernel)
687 .grid_size(32)
688 .interpolation_method(InterpolationMethod::Linear);
689
690 assert_eq!(gpr.grid_size, 32);
691 matches!(gpr.interpolation_method, InterpolationMethod::Linear);
692 }
693
694 #[test]
695 #[allow(non_snake_case)]
696 fn test_grid_bounds_determination() {
697 let kernel = Box::new(RBF::new(1.0));
698 let gpr = StructuredKernelInterpolationGPR::new(kernel)
699 .grid_bounds_method(GridBoundsMethod::DataRange)
700 .boundary_extension(0.1);
701
702 let X = Array2::from_shape_vec((3, 2), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]).unwrap();
703 let bounds = gpr.determine_grid_bounds(&X.view()).unwrap();
704
705 assert_eq!(bounds.len(), 2);
706 assert!(bounds[0].0 < 1.0); assert!(bounds[0].1 > 5.0); }
709
710 #[test]
711 fn test_grid_points_creation() {
712 let kernel = Box::new(RBF::new(1.0));
713 let gpr = StructuredKernelInterpolationGPR::new(kernel).grid_size(5);
714
715 let bounds = vec![(0.0, 10.0), (-5.0, 5.0)];
716 let grid_points = gpr.create_grid_points(&bounds).unwrap();
717
718 assert_eq!(grid_points.len(), 2);
719 assert_eq!(grid_points[0].len(), 5);
720 assert_eq!(grid_points[1].len(), 5);
721 assert!((grid_points[0][0] - 0.0).abs() < 1e-10);
722 assert!((grid_points[0][4] - 10.0).abs() < 1e-10);
723 }
724
725 #[test]
726 #[allow(non_snake_case)]
727 fn test_interpolation_weights_computation() {
728 let kernel = Box::new(RBF::new(1.0));
729 let gpr = StructuredKernelInterpolationGPR::new(kernel)
730 .grid_size(4)
731 .interpolation_method(InterpolationMethod::Linear);
732
733 let X = Array2::from_shape_vec((2, 1), vec![2.5, 7.5]).unwrap();
734 let grid_points = vec![Array1::linspace(0.0, 10.0, 4)];
735 let bounds = vec![(0.0, 10.0)];
736
737 let weights = gpr
738 .compute_interpolation_weights(&X.view(), &grid_points, &bounds)
739 .unwrap();
740
741 assert_eq!(weights.nrows(), 2);
742 assert_eq!(weights.ncols(), 4);
743
744 for i in 0..weights.nrows() {
746 let row_sum = weights.row(i).sum();
747 assert!((row_sum - 1.0).abs() < 1e-10);
748 }
749 }
750
751 #[test]
752 #[allow(non_snake_case)]
753 fn test_ski_fit_predict() {
754 let kernel = Box::new(RBF::new(1.0));
755 let gpr = StructuredKernelInterpolationGPR::new(kernel)
756 .grid_size(8)
757 .interpolation_method(InterpolationMethod::Linear)
758 .use_toeplitz(false); let X = Array2::from_shape_vec((5, 1), vec![1.0, 2.0, 3.0, 4.0, 5.0]).unwrap();
761 let y = Array1::from_vec(vec![1.0, 4.0, 9.0, 16.0, 25.0]);
762
763 let trained = gpr.fit(&X.view(), &y.view()).unwrap();
764 let predictions = trained.predict(&X.view()).unwrap();
765
766 assert_eq!(predictions.len(), 5);
767 assert!(trained.log_marginal_likelihood().is_finite());
768 }
769
770 #[test]
771 #[allow(non_snake_case)]
772 fn test_prediction_with_uncertainty() {
773 let kernel = Box::new(RBF::new(1.0));
774 let gpr = StructuredKernelInterpolationGPR::new(kernel)
775 .grid_size(6)
776 .use_toeplitz(false);
777
778 let X = Array2::from_shape_vec((3, 1), vec![1.0, 2.0, 3.0]).unwrap();
779 let y = Array1::from_vec(vec![1.0, 2.0, 3.0]);
780
781 let trained = gpr.fit(&X.view(), &y.view()).unwrap();
782 let (predictions, variances) = trained.predict_with_uncertainty(&X.view()).unwrap();
783
784 assert_eq!(predictions.len(), 3);
785 assert_eq!(variances.len(), 3);
786 assert!(variances.iter().all(|&v| v >= 0.0)); }
788
789 #[test]
790 #[allow(non_snake_case)]
791 fn test_interpolation_methods() {
792 let kernel = Box::new(RBF::new(1.0));
793
794 let methods = vec![
795 InterpolationMethod::Linear,
796 InterpolationMethod::NearestNeighbor,
797 InterpolationMethod::Cubic,
798 InterpolationMethod::Lanczos { a: 2 },
799 ];
800
801 for method in methods {
802 let gpr = StructuredKernelInterpolationGPR::new(kernel.clone())
803 .grid_size(4)
804 .interpolation_method(method)
805 .use_toeplitz(false);
806
807 let X = Array2::from_shape_vec((3, 1), vec![1.0, 2.0, 3.0]).unwrap();
808 let y = Array1::from_vec(vec![1.0, 2.0, 3.0]);
809
810 let result = gpr.fit(&X.view(), &y.view());
811 assert!(result.is_ok());
812 }
813 }
814
815 #[test]
816 #[allow(non_snake_case)]
817 fn test_approximation_info() {
818 let kernel = Box::new(RBF::new(1.0));
819 let gpr = StructuredKernelInterpolationGPR::new(kernel)
820 .grid_size(8)
821 .use_toeplitz(false);
822
823 let X = Array2::from_shape_vec((10, 2), (0..20).map(|x| x as f64).collect()).unwrap();
824 let y = Array1::from_vec((0..10).map(|x| x as f64).collect());
825
826 let trained = gpr.fit(&X.view(), &y.view()).unwrap();
827 let info = trained.approximation_info().unwrap();
828
829 assert!(info.effective_dof > 0.0);
830 assert!(info.memory_reduction_factor > 0.0);
831 assert!(info.complexity_reduction_factor > 0.0);
832 assert_eq!(info.grid_resolutions.len(), 2);
833 }
834}