#![cfg(test)]
use crate::fit_orchestration::{FitConfig, FitRequest, materialize};
use gam_data::encode_recordswith_inferred_schema;
use gam_terms::smooth::build_term_collection_design;
use rand::SeedableRng;
use rand::rngs::StdRng;
use rand_distr::{Distribution, Normal, Uniform};
fn truth(a: f64, b: f64) -> f64 {
(1.2 * a).sin() * (1.1 * b).cos()
+ 0.6 * (3.0 * a).sin() * (2.8 * b).cos()
+ 0.4 * (5.0 * a + 0.5 * b).sin()
}
fn make_dataset(n: usize) -> gam_data::EncodedDataset {
let mut rng = StdRng::seed_from_u64(424242);
let u = Uniform::new(-3.0, 3.0).expect("uniform");
let noise = Normal::new(0.0, 0.15).expect("normal");
let x1: Vec<f64> = (0..n).map(|_| u.sample(&mut rng)).collect();
let x2: Vec<f64> = (0..n).map(|_| u.sample(&mut rng)).collect();
let y: Vec<f64> = (0..n)
.map(|i| truth(x1[i], x2[i]) + noise.sample(&mut rng))
.collect();
let headers = vec!["x1".to_string(), "x2".to_string(), "y".to_string()];
let rows: Vec<csv::StringRecord> = (0..n)
.map(|i| {
csv::StringRecord::from(vec![
x1[i].to_string(),
x2[i].to_string(),
y[i].to_string(),
])
})
.collect();
encode_recordswith_inferred_schema(headers, rows).expect("encode")
}
fn cold_design_cols(formula: &str, ds: &gam_data::EncodedDataset, cfg: &FitConfig) -> usize {
let mat = materialize(formula, ds, cfg).expect("materialize");
let FitRequest::Standard(req) = &mat.request else {
panic!("expected a standard fit request for '{formula}'");
};
let design = build_term_collection_design(req.data.view(), &req.spec).expect("cold design");
design.design.ncols()
}
#[test]
fn matern_cold_design_does_not_collapse_and_k_has_effect_1629() {
let ds = make_dataset(2000);
let cfg = FitConfig {
family: Some("gaussian".to_string()),
..FitConfig::default()
};
let matern_default = cold_design_cols("y ~ matern(x1, x2)", &ds, &cfg);
assert!(
matern_default >= 180,
"matern(x1, x2) cold design collapsed to {matern_default} columns \
(#1629: the diameter seed's realized-design orthogonality whitener pruned \
the basis pre-optimization). Expected ~200."
);
let thinplate = cold_design_cols("y ~ thinplate(x1, x2)", &ds, &cfg);
assert!(
matern_default + 40 >= thinplate,
"matern(x1, x2) ({matern_default} cols) must resolve a basis comparable to \
thinplate ({thinplate} cols), not a small fraction of it (#1629 6× gap)"
);
let tensor = cold_design_cols("y ~ tensor(x1, x2)", &ds, &cfg);
assert!(
matern_default + 40 >= tensor,
"matern(x1, x2) ({matern_default} cols) must resolve a basis comparable to \
tensor ({tensor} cols), not a small fraction of it (#1629 6× gap)"
);
let mut prev = matern_default;
for &k in &[150usize, 100, 60] {
let cols = cold_design_cols(&format!("y ~ matern(x1, x2, k={k})"), &ds, &cfg);
assert!(
cols + 25 >= k,
"matern(x1, x2, k={k}) cold design collapsed to {cols} columns; \
expected ~{k} (#1629: realized rank, not k, was capping the basis)"
);
assert!(
cols < prev,
"matern k={k} ({cols} cols) must resolve a strictly smaller basis than the \
next-larger k ({prev} cols); pre-fix `k=` had no effect because every \
rail collapsed to the same realized rank (#1629)"
);
prev = cols;
}
let matern_nu32 = cold_design_cols("y ~ matern(x1, x2, nu=3/2)", &ds, &cfg);
assert!(
matern_nu32 >= 180,
"matern(x1, x2, nu=3/2) cold design collapsed to {matern_nu32} columns; \
the #1629 basis-collapse must not depend on the Matérn smoothness order"
);
}