use super::entry::fit_from_formula;
use super::request::{FitConfig, FitResult, StandardFitResult};
use csv::StringRecord;
use gam_data::encode_recordswith_inferred_schema;
use gam_terms::smooth::SmoothBasisSpec;
use rand::SeedableRng;
use rand::rngs::StdRng;
use rand_distr::{Distribution, Uniform};
fn small_ball_dataset(n: usize, seed: u64) -> gam_data::EncodedDataset {
let mut rng = StdRng::seed_from_u64(seed);
let unif = Uniform::new(-0.3_f64, 0.3).unwrap();
let headers: Vec<String> = ["x", "z", "y"].iter().map(|s| s.to_string()).collect();
let mut rows = Vec::with_capacity(n);
for _ in 0..n {
let x = unif.sample(&mut rng);
let z = unif.sample(&mut rng);
let y = (4.0 * x).sin() * (4.0 * z).cos();
rows.push(StringRecord::from(vec![
x.to_string(),
z.to_string(),
y.to_string(),
]));
}
encode_recordswith_inferred_schema(headers, rows).expect("encode")
}
fn fitted_kappa_and_pin(formula: &str, ds: &gam_data::EncodedDataset) -> (f64, bool) {
let cfg = FitConfig {
family: Some("gaussian".to_string()),
..FitConfig::default()
};
let result = fit_from_formula(formula, ds, &cfg).expect("curv() fit");
let StandardFitResult { resolvedspec, .. } = match result {
FitResult::Standard(s) => s,
_ => panic!("expected Standard fit"),
};
let SmoothBasisSpec::ConstantCurvature { spec, .. } = &resolvedspec.smooth_terms[0].basis
else {
panic!("expected a ConstantCurvature term after fit");
};
(spec.kappa, spec.kappa_fixed)
}
#[test]
fn pinned_kappa_is_kept_verbatim() {
let ds = small_ball_dataset(600, 2152);
let (k_pos, pinned_pos) = fitted_kappa_and_pin("y ~ curv(x, z, kappa=3, centers=20)", &ds);
assert!(
pinned_pos,
"explicit kappa=3 must mark the term kappa_fixed"
);
assert!(
(k_pos - 3.0).abs() < 1e-9,
"pinned kappa=+3 was re-derived: fit kept κ = {k_pos} (want +3)"
);
let (k_neg, pinned_neg) = fitted_kappa_and_pin("y ~ curv(x, z, kappa=-3, centers=20)", &ds);
assert!(
pinned_neg,
"explicit kappa=-3 must mark the term kappa_fixed"
);
assert!(
(k_neg + 3.0).abs() < 1e-9,
"pinned kappa=-3 was re-derived: fit kept κ = {k_neg} (want -3)"
);
let (k_flat, pinned_flat) = fitted_kappa_and_pin("y ~ curv(x, z, kappa=0, centers=20)", &ds);
assert!(
pinned_flat,
"explicit kappa=0 must mark the term kappa_fixed"
);
assert!(
k_flat.abs() < 1e-9,
"pinned kappa=0 drifted off flat: fit kept κ = {k_flat} (want 0)"
);
assert!(
(k_pos - k_neg).abs() > 1.0,
"pinned +3 and -3 collapsed to the same κ ({k_pos} vs {k_neg})"
);
}
#[test]
fn omitted_kappa_stays_free_for_estimation() {
let ds = small_ball_dataset(400, 7);
let (_k, pinned) = fitted_kappa_and_pin("y ~ curv(x, z, centers=20)", &ds);
assert!(
!pinned,
"omitted kappa= must leave the term free to estimate κ (kappa_fixed=false)"
);
}
#[test]
fn pinned_out_of_chart_kappa_is_rejected() {
let ds = small_ball_dataset(300, 99);
let cfg = FitConfig {
family: Some("gaussian".to_string()),
..FitConfig::default()
};
let result = fit_from_formula("y ~ curv(x, z, kappa=-50, centers=20)", &ds, &cfg);
assert!(
result.is_err(),
"a pinned out-of-chart kappa=-50 must be rejected (design genuinely built \
at κ=-50, so validate_chart_points must fire); got Ok — κ was silently re-derived"
);
}