Skip to main content

gam_models/fit_orchestration/drivers/
mod.rs

1// GAM fit-orchestration drivers, relocated from `gam-terms/src/smooth/`
2// (`design_construction.rs` + `spatial_optimization.rs`) up into `gam-models`
3// per #1521. They were `include!`d into `gam_terms::smooth` (one flat module
4// alongside `prelude.rs` + `term_specs.rs`); to preserve that single-module
5// flat namespace (and the heavy cross-references between the two files) byte
6// for byte, they are `include!`d here as well. The shared import surface that
7// `prelude.rs`/`term_specs.rs` used to provide is reconstructed below with the
8// relocated paths (families now resolve as `crate::*`, the solver as
9// `gam_solve::*`, basis/term machinery as `gam_terms::*`).
10use gam_terms::basis::{
11    BasisError, BasisMetadata, BasisPsiDerivativeResult, BasisPsiSecondDerivativeResult,
12    BasisWorkspace, CenterStrategy, MaternIdentifiability, PenaltySource, SpatialIdentifiability,
13    build_constant_curvature_basis_kappa_derivatives,
14    build_matern_basis_log_kappa_aniso_derivatives, build_matern_basis_log_kappa_derivatives,
15    build_matern_collocation_operator_matrices, build_measure_jet_basis_psi_derivatives,
16    build_thin_plate_basis_log_kappa_derivatives, estimate_penalty_nullity,
17    initial_aniso_contrasts,
18};
19
20use gam_custom_family::{
21    BlockEffectiveJacobian, BlockGeometryDirectionalDerivative, BlockWorkingSet,
22    BlockwiseFitOptions, CustomFamily, CustomFamilyBlockPsiDerivative, CustomFamilyOwnedMode,
23    CustomFamilyWarmStart, ExactNewtonOuterObjective, FamilyEvaluation, FamilyLinearizationState,
24    ParameterBlockSpec, ParameterBlockState, PenaltyMatrix,
25    evaluate_custom_family_joint_hyper_efs_owned, evaluate_custom_family_joint_hyper_owned,
26    fit_custom_family, fit_custom_family_fixed_log_lambdas_from_owned_mode,
27};
28
29use gam_solve::estimate::{
30    EstimationError, ExternalOptimOptions, FitInference, FitOptions, FittedLinkState, PenaltySpec,
31    UnifiedFitResult, UnifiedFitResultParts, fit_gamwith_heuristic_lambdas,
32};
33
34use gam_solve::estimate::reml::DirectionalHyperParam;
35
36// #1521: `freeze_term_collection_from_design` relocated DOWN into gam_terms::smooth
37// (was an `include!`d `pub fn` in spatial_optimization.rs). Re-export here so the
38// `crate::fit_orchestration::drivers::freeze_term_collection_from_design` path used
39// by families + pyffi resolves unchanged.
40pub use gam_terms::smooth::freeze_term_collection_from_design;
41
42use gam_solve::mixture_link::{
43    inverse_link_jet_for_inverse_link, logit_inverse_link_jet5, state_from_beta_logisticspec,
44    state_from_sasspec, state_fromspec,
45};
46
47use gam_math::quantile::quantile_from_sorted;
48
49use gam_linalg::faer_ndarray::{fast_ab, fast_atb, fast_atv};
50
51use gam_linalg::matrix::{DesignBlock, DesignMatrix, RandomEffectOperator, SymmetricMatrix};
52
53use gam_problem::{ConstraintSet, ExactNewtonJointPsiTerms, LinearInequalityConstraints};
54
55use gam_spec::{
56    InverseLink, LatentCLogLogState, LikelihoodSpec, MixtureLinkState, ResponseFamily,
57    SasLinkState, StandardLink,
58};
59
60use gam_terms::smooth::penalty_priors::{
61    realize_keyed_penalty_block_gamma_priors, realize_penalty_block_gamma_priors,
62};
63
64use gam_terms::smooth::shape_constraints::{
65    linear_constraints_from_lower_bounds_global, merge_linear_constraints_global,
66    shape_lower_bounds_local,
67};
68
69// Every `pub` item that `gam_terms::smooth` exposes (the `term_specs.rs`
70// spec/design machinery, `SmoothError`, the `penalty_priors`/`structure_analysis`
71// re-exports, …). This reconstructs the sibling-module visibility the drivers
72// had while textually pasted inside `gam_terms::smooth`.
73use gam_terms::smooth::*;
74
75use ndarray::{Array1, Array2, ArrayView1, ArrayView2, Axis, s};
76
77use std::collections::BTreeSet;
78use std::ops::Range;
79use std::sync::atomic::AtomicUsize;
80use std::sync::{Arc, Mutex};
81
82// Fit-result carriers relocated out of `gam_terms::smooth::term_specs` with the
83// drivers (they hold a `gam_solve` `UnifiedFitResult` and are consumed only by
84// the drivers / the surrounding fit-orchestration layer).
85#[derive(Clone)]
86pub struct FittedTermCollection {
87    pub fit: UnifiedFitResult,
88    pub design: TermCollectionDesign,
89    pub adaptive_diagnostics: Option<AdaptiveRegularizationDiagnostics>,
90}
91
92#[derive(Clone, Copy, Debug, Default)]
93pub struct SpatialLengthScaleOptimizationTiming {
94    pub log_kappa_dim: usize,
95    pub cost_calls: usize,
96    pub cost_total_s: f64,
97    pub eval_calls: usize,
98    pub eval_total_s: f64,
99    pub efs_calls: usize,
100    pub efs_total_s: f64,
101    pub slow_path_resets: u64,
102    pub design_revision_delta: u64,
103    /// #1868 deterministic n-independence instrument: the number of length-`n`
104    /// row-element touches the Gaussian zero-iteration inner synthesis performed
105    /// on the #1033 n-free κ-trial *skip* path during this κ-optimisation phase
106    /// (excludes the one-time priming eval). The #1033 architectural invariant
107    /// requires each in-window trial to touch only k×k objects, so this MUST NOT
108    /// scale with `n`. A value that grows with `n` is exactly the #1868
109    /// O(n)-per-callback regression. This replaces the old noisy wall-clock
110    /// per-callback ratio with an exact, millisecond-fast integer gate.
111    pub nfree_skip_row_touches: u64,
112    pub nfree_miss_shape: u64,
113    pub nfree_miss_value: u64,
114    pub nfree_miss_gradient: u64,
115    pub nfree_miss_penalty: u64,
116    pub nfree_miss_revision: u64,
117    pub nfree_miss_second_order: u64,
118    pub nfree_miss_other: u64,
119    /// Whether `begin_exact_polish` retired the #1033b n-free ψ-Gram surrogate
120    /// and the optimizer continued on the exact streamed criterion (gam#2760).
121    ///
122    /// Every counter above is a statement about the SEARCH and stops at this
123    /// boundary — "an in-window hyperparameter TRIAL touches only k×k objects"
124    /// is a claim about trials, and the polish is not a trial phase. The two
125    /// fields below carry the polish's own O(n) cost so it is published rather
126    /// than either hidden or charged to the search.
127    pub exact_polish_ran: bool,
128    /// `slow_path_resets` accrued AFTER the exact-polish boundary. Bounded by
129    /// the polish's own iteration budget, so n-independent; every one of them is
130    /// intended, because the polish exists precisely to leave the n-free lane.
131    pub polish_slow_path_resets: u64,
132    /// `nfree_skip_row_touches` accrued after the exact-polish boundary. The
133    /// surrogate is gone by then, so no evaluation can take the skip path and
134    /// this must stay 0 — a nonzero value would mean the retirement did not take.
135    pub polish_nfree_skip_row_touches: u64,
136    pub optim_total_s: f64,
137}
138
139impl SpatialLengthScaleOptimizationTiming {
140    pub fn trial_total_s(self) -> f64 {
141        self.cost_total_s + self.eval_total_s + self.efs_total_s
142    }
143}
144
145#[derive(Clone)]
146pub struct FittedTermCollectionWithSpec {
147    pub fit: UnifiedFitResult,
148    pub design: TermCollectionDesign,
149    pub resolvedspec: TermCollectionSpec,
150    pub adaptive_diagnostics: Option<AdaptiveRegularizationDiagnostics>,
151    pub kappa_timing: Option<SpatialLengthScaleOptimizationTiming>,
152}
153
154include!("design_construction.rs");
155include!("spatial_optimization.rs");
156// #2458: the κ-profile derivative jet, kept out of the 9k-line driver file.
157include!("constant_curvature_kappa_jet.rs");
158// #2747: the constant-curvature smooth's outer objective in its own two
159// coordinates (κ, ln ℓ) — the value path, the ψ jet, the profile that owns both
160// and the bounded solve that mints κ̂. Same reason as the jet above.
161include!("constant_curvature_profile.rs");
162// #2750: the measure-jet representer range is screened against the response
163// before the outer ψ search refines it. Same "bracket cheaply, refine exactly"
164// shape as the κ profile above, and kept out of the driver file for the same
165// reason.
166include!("measure_jet_range_seed.rs");
167// #1063/#2672: the per-term smooth likelihood-ratio test — the constrained
168// refit, the Lawley Bartlett factor and the null law the statistic is scored
169// against. A self-contained inference subsystem that only ever consumed the
170// driver's fit; same reason as the three above.
171include!("smooth_term_lr.rs");
172// #2774: the per-smooth basis-adequacy report — the enrichment each smooth is
173// tested against, and the fit-time advisory a failing term produces. Same shape
174// as the LR test above: a self-contained inference subsystem over the driver's
175// fit, kept out of the driver file for the same reason.
176include!("basis_adequacy.rs");
177
178#[cfg(test)]
179mod test_support {
180    use super::*;
181
182    /// Test-only default-policy constructor. Production callers must supply the
183    /// fit's intrinsic resource policy through `new_with_policy`; keeping this
184    /// adapter inside the test-support module prevents a permissive constructor
185    /// from entering the library surface.
186    pub(super) trait SingleBlockExactJointDesignCacheTestExt<'d>: Sized {
187        fn new(
188            data: ArrayView2<'d, f64>,
189            spec: TermCollectionSpec,
190            design: TermCollectionDesign,
191            spatial_terms: Vec<usize>,
192            rho_dim: usize,
193            dims_per_term: Vec<usize>,
194        ) -> Result<Self, String>;
195    }
196
197    impl<'d> SingleBlockExactJointDesignCacheTestExt<'d> for SingleBlockExactJointDesignCache<'d> {
198        fn new(
199            data: ArrayView2<'d, f64>,
200            spec: TermCollectionSpec,
201            design: TermCollectionDesign,
202            spatial_terms: Vec<usize>,
203            rho_dim: usize,
204            dims_per_term: Vec<usize>,
205        ) -> Result<Self, String> {
206            let policy = gam_runtime::resource::ResourcePolicy::default_library();
207            Self::new_with_policy(
208                data,
209                spec,
210                design,
211                spatial_terms,
212                rho_dim,
213                dims_per_term,
214                &policy,
215            )
216        }
217    }
218}
219
220// #901 re-home: the end-to-end iso-κ joint REML outer-gradient FD oracles on
221// real Duchon/Matérn smooths. Authored in the pre-#1521 monolith, orphaned out
222// of the build by #1601 (its private driver deps live HERE post-carve, not in
223// `gam_terms::smooth` where the `include!` was commented out). The file is a
224// self-contained `#[cfg(test)] mod`, so it adds nothing to the non-test build.
225include!("iso_kappa_reml_gradient_fd_tests.rs");
226// #901 re-home: the Matérn κ-optimizer convergence/monotone gates the issue
227// listed as stalling on the wrong projected-logdet gradient. Same #1601
228// orphaning story — driver deps live HERE post-carve. Self-contained
229// `#[cfg(test)] mod`, so it adds nothing to the non-test build.
230include!("spatial_length_scale_monotone_tests.rs");
231// #1264/#1033 re-home: the production ψ-Gram fast-path skip guard
232// (`reduced_basis_equal` soundness, β̂ vs streamed to 1e-6) and the #1033
233// forced-rotation frontier measurement. Same #1601 orphaning story as the two
234// siblings above — its private driver deps live HERE post-carve, and the
235// monolith `include!` in `gam_terms::smooth::tests` was commented out and never
236// relocated, so both guards compiled into NO binary. Self-contained
237// `#[cfg(test)] mod`, so it adds nothing to the non-test build.
238include!("psi_gram_tensor_fast_path_tests.rs");
239// #901 re-home: the custom-family ADAPTIVE-ψ projected-logdet REML
240// hypergradient + outer-Hessian FD oracle on a real `SpatialAdaptiveExactFamily`
241// — the half of #901 the engine fix (joint_jeffreys_information_depends_on_psi)
242// directly targets, plus the #426 unified-dispatch parity pin. Same #1601
243// orphaning story as the two oracles above; driver deps live HERE post-carve.
244// Self-contained `#[cfg(test)] mod`, so it adds nothing to the non-test build.
245include!("spatial_adaptive_hyper_fd_tests.rs");
246// #1274 re-home: the Matérn n-free penalty re-key topology/byte-identity gates.
247// Authored in the pre-#1521 monolith under `tests/src_modules/smooths/`, they
248// were orphaned by #1601 (the `gam_terms::smooth::tests` `include!` was
249// commented out and the body needs the gam-models-private
250// `FrozenTermCollectionIncrementalRealizer`), so the #1274 guard compiled
251// nowhere. Re-homed HERE where the private realizer lives; self-contained
252// `#[cfg(test)] mod`, so it adds nothing to the non-test build.
253include!("matern_nfree_rekey_topology_tests.rs");
254// #1601 relocation debt: the 88 design-assembly / constraint / IFT-cache
255// regression guards. Same orphaning story as the siblings above — their
256// `build_term_collection_design` / freeze / incremental-realizer / tensor+streamed
257// eval deps live HERE post-#1521 carve, but #1601 commented the include! out of
258// `gam_terms::smooth::tests` "for relocation" that never happened (the parked
259// `tests/src_modules/` tree was `mod`'d into no binary). Self-contained
260// `#[cfg(test)] mod`.
261include!("design_assembly_constraint_tests.rs");
262// #1601 relocation debt: the LAST of the three orphaned smooth test files — 48
263// adaptive / bounded / pure-Duchon / Charbonnier regression guards. Same story:
264// commented out of `gam_terms::smooth::tests` by #1601 "for relocation" and
265// parked in the `tests/src_modules/` tree that compiled into no binary. Re-homed
266// here where its `build_term_collection_design` / freeze / SAS-link-state /
267// joint-hyper FD deps resolve post-#1521 carve. Self-contained `#[cfg(test)] mod`.
268include!("adaptive_bounded_duchon_tests.rs");
269
270// #2425 Half-A instrumentation. Measurement-only probes (they print numbers and
271// assert only that the measurement completed), kept in-tree so the next lane
272// does not have to rebuild this crate to re-derive them. Self-contained
273// `#[cfg(test)] mod`, so it adds nothing to the non-test build.
274// #2458: FD gates for the constant-curvature κ profile derivative jet. The
275// second derivative feeds a stationarity CERTIFICATE, so a wrong one is silent
276// — it moves the bound rather than the fit. Self-contained `#[cfg(test)] mod`.
277include!("constant_curvature_kappa_jet_fd_tests.rs");
278
279// #2747: the curvature criterion must identify κ⋆ at a range it was NOT handed.
280// Three planted curvatures × three planted ranges, because the pre-#2747
281// criterion is correct on the one cell where the truth's range IS the auto
282// heuristic's — the cell the acceptance fixture happens to use.
283include!("constant_curvature_kappa_box_probe_tests.rs");
284
285include!("zz_measure_2425_kappa_tests.rs");
286
287// #2450 criterion-identity instrumentation. A PAIRED A/B over `rho_prior` at
288// one SHA: same data, same seed, same spec, only the prior varies, so the
289// reported difference is the criterion own bias. Measurement-only, same
290// contract as the probe above. Self-contained `#[cfg(test)] mod`.
291include!("zz_measure_2450_rho_prior_criterion_tests.rs");