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");