gam_models/transformation_normal/config.rs
1use ndarray::Array1;
2
3#[derive(Clone, Debug)]
4pub struct TransformationNormalConfig {
5 /// B-spline degree for the response-direction deviation basis (default 3).
6 pub response_degree: usize,
7 /// Number of interior knots for the response-direction deviation basis (default 10).
8 pub response_num_internal_knots: usize,
9 /// Difference penalty order for the response-direction roughness penalty (default 2).
10 pub response_penalty_order: usize,
11 /// Additional penalty orders for the response-direction (default \[1\]).
12 pub response_extra_penalty_orders: Vec<usize>,
13 /// Whether to add a global identity (ridge) penalty (default true).
14 pub double_penalty: bool,
15 /// When true, `response_num_internal_knots` is treated as an already-resolved
16 /// effective value: `fit_transformation_normal` uses it verbatim instead of
17 /// re-running `effective_response_num_internal_knots`. This is required by the
18 /// cross-fit Stage-1 calibration, which pins the knot count once at the
19 /// smallest fold complement so `p_resp` (and hence `p₁ = p_resp · p_cov`)
20 /// is fold-invariant; the data-driven complexity cap would otherwise round
21 /// to different counts on each fold's response subsample (workflow.rs §3).
22 pub response_num_internal_knots_pinned: bool,
23 /// A pre-resolved response knot vector, used verbatim by
24 /// `build_response_basis` instead of regenerating one from this fit's own
25 /// response. The knot vector fixes the **certified response support**
26 /// `[y_lo, y_hi]` that every PIT is normalized against, so it is a shared
27 /// object whenever several CTN fits must produce comparable scores.
28 ///
29 /// The cross-fit Stage-1 calibration is that case and it is why this exists
30 /// (gam#2680): it refits the CTN on each fold complement and scores the
31 /// held-out rows, so a fold-local support refuses the fold that holds out a
32 /// response extreme, and — when it does not refuse — assembles the
33 /// out-of-fold score from `K` PITs taken against `K` different truncations,
34 /// which is not one latent scale. `None` means "resolve it from this fit's
35 /// own response", which is right for every single-fit path.
36 pub response_knots_pinned: Option<Array1<f64>>,
37}
38
39impl Default for TransformationNormalConfig {
40 fn default() -> Self {
41 Self {
42 response_degree: 3,
43 response_num_internal_knots: 10,
44 response_penalty_order: 2,
45 response_extra_penalty_orders: vec![1],
46 double_penalty: true,
47 response_num_internal_knots_pinned: false,
48 response_knots_pinned: None,
49 }
50 }
51}
52
53/// Baseline cap for the tensor-product width used by the transformation-normal
54/// response basis. Small datasets should stay compact because the fit
55/// repeatedly factorizes dense penalized Hessians.
56pub(crate) const BASE_TRANSFORMATION_TENSOR_WIDTH: usize = 160;
57
58/// Large samples can support a richer response basis without the aggressive
59/// underfitting forced by the small-sample cap above. This upper cap keeps the
60/// tensor width bounded even when the covariate side is narrow.
61pub(crate) const LARGE_SAMPLE_TRANSFORMATION_TENSOR_WIDTH: usize = 320;
62
63/// E[log |Z|] for Z ~ N(0, 1), used to put local log-absolute residual
64/// projections on the standard-normal scale.
65pub(crate) const STANDARD_NORMAL_MEAN_LOG_ABS: f64 = -0.635_181_422_730_739_1;
66
67/// Strict-feasibility margin for `h' > 0` on the monotonicity grid. Used
68/// both by the fit-time fraction-to-boundary line search (so accepted β
69/// keeps `h'(grid) ≥ EPS`) and by the predict-time monotonicity check
70/// in `inference::predict_input` (which rejects predictions whose minimum
71/// `h'` on the response grid drops below this threshold). Keeping these
72/// in sync prevents the predict path from rejecting fits that the
73/// optimizer accepted as feasible — and vice versa.
74pub const TRANSFORMATION_MONOTONICITY_EPS: f64 = 1.0e-8;
75
76/// Absolute bound for feasible transformation scores on the standard-normal
77/// scale. The CTN likelihood targets `h(Y|x) ~ N(0,1)`; accepting exact-Newton
78/// iterates with finite positive `h'` but astronomical `|h|` lets curvature
79/// diagnostics overflow into meaningless values. This is a numerical runaway
80/// guard, not a statistical plausibility filter: startup seeds can temporarily
81/// land outside practically observable normal quantiles before the line search
82/// moves them back into the likelihood's high-density region.
83pub const TRANSFORMATION_NORMAL_H_ABS_MAX: f64 = 1.0e6;
84
85/// Number of dense-spectral factor columns processed per exact ψψ HVP row pass.
86/// At large-scale CTN dimensions p≈800, this keeps the per-worker accumulator well
87/// under 1 MiB while reducing repeated SCOP row-invariant work by 32× relative
88/// to one-column HVP dispatch.
89pub(crate) const SCOP_PSI_PSI_HVP_TILE_COLS: usize = 32;
90
91/// Exact dense SCOP coefficient Hessian cache limit for the inner `H·v` path.
92///
93/// The large-scale CTN calibration fit has many rows but a moderate coefficient
94/// dimension (for example n=20k, p=264). In that regime repeated PCG products
95/// against the same Hessian should pay the row-streaming chain rule once, then
96/// serve subsequent products as dense BLAS matvecs. Keep the cache restricted to
97/// genuinely moderate p so wide CTN fits remain row-streamed.
98pub(crate) const SCOP_HESSIAN_HVP_DENSE_CACHE_MAX_DIM: usize = 384;
99
100pub(crate) const SCOP_HESSIAN_HVP_DENSE_CACHE_MAX_BYTES: usize = 64 * 1024 * 1024;
101
102/// CTN-scoped ceiling on the custom-family inner exact-Newton cycle budget.
103///
104/// The global `DEFAULT_CUSTOM_FAMILY_INNER_MAX_CYCLES = 1200` exists for the
105/// large-scale survival marginal-slope path, whose inner mode has a long,
106/// rank-deficient KKT tail that genuinely needs hundreds of cycles. CTN is a
107/// different regime: its coefficient block is a *bounded-dimension* Khatri–Rao
108/// tensor (capped by `BASE/LARGE_SAMPLE_TRANSFORMATION_TENSOR_WIDTH`), and the
109/// objective is convex by construction. The convexity is the LIKELIHOOD's, not
110/// the penalty's: the negative log-likelihood is `Σ w (½h² − log h')` with `h`
111/// and `h'` both linear in β, so its exact Hessian is
112/// `Σ w (∇h ∇hᵀ + ∇h' ∇h'ᵀ / h'²)` — two Gram matrices, positive definite
113/// wherever no nonzero β annihilates `h` on every row. (Before gam#2600 this
114/// paragraph credited the `double_penalty` ridge and the roughness penalties,
115/// and it was false in both halves: the endpoint renormalizer made the
116/// likelihood non-convex, and gam#2600's predecessor `faf3f3fde` had already
117/// put the affine transformation in every penalty's null space.) An
118/// exact-Newton iteration on a convex, bounded-dimension block converges in a
119/// handful of cycles; the only way the fit reaches 1200 inner cycles is by
120/// polishing weakly-identified directions that contribute nothing to the
121/// likelihood (the #720 timeout). Scaling the cap with the realized coefficient
122/// dimension keeps a generous margin for a genuinely nonlinear, high-dimensional
123/// transformation while refusing to grind the production large-scale cap on an
124/// easy near-Gaussian shift.
125pub(crate) const CTN_INNER_MAX_CYCLES_BASE: usize = 64;
126
127pub(crate) const CTN_INNER_MAX_CYCLES_PER_DIM: usize = 2;
128
129pub(crate) const CTN_INNER_MAX_CYCLES_CEILING: usize = 400;
130
131/// Numerical floor on a Gram/penalty diagonal scale before it enters the
132/// `likelihood_scale / penalty_scale` ratio that seeds the outer log-λ search.
133/// A genuinely zero diagonal (an all-zero penalty block, or a degenerate
134/// likelihood Gram) would otherwise produce a `0/0` or `x/0` seed; flooring
135/// both scales at a value far below any meaningful curvature keeps the ratio
136/// finite without perturbing well-posed problems.
137pub(crate) const CTN_SEED_SCALE_FLOOR: f64 = 1.0e-8;
138
139/// Lower bound on the cold-start seed log-λ (i.e. λ ≥ 1). Keeps the outer
140/// optimizer out of the under-regularized regime where the CTN inner solve is
141/// structurally rank-deficient (small-n / p > n); the optimizer is free to step
142/// below this once the data support it. See `ctn_penalty_scale_log_lambdas`.
143pub(crate) const CTN_SEED_LOG_LAMBDA_MIN: f64 = 0.0;
144
145/// Upper bound on the cold-start seed log-λ, matching the outer ρ-bound used
146/// across the location-scale families: λ ≈ e¹² caps the seed in the strongly
147/// over-smoothed regime so a tiny penalty scale cannot seed an absurd λ.
148pub(crate) const CTN_SEED_LOG_LAMBDA_MAX: f64 = 12.0;
149
150/// Floor on the warm-start global residual scale `sqrt(weighted_ss / Σw)`.
151/// Guards the degenerate near-perfect-fit case (residuals collapse to numerical
152/// zero) so the per-residual `residual_floor` below — and the subsequent
153/// `ln(|y−μ|)` log-scale target — stay finite. Well below any real response
154/// spread, so it never perturbs a genuine fit.
155pub(crate) const WARMSTART_GLOBAL_SCALE_FLOOR: f64 = 1e-6;
156
157/// Per-residual floor used to form the log-scale warm-start target
158/// `ln(|y−μ|) − E[ln|N(0,1)|]`. Built as `global_scale · WARMSTART_RESIDUAL_REL_FLOOR
159/// + WARMSTART_RESIDUAL_ABS_FLOOR`: the relative term keeps an exactly-fit point
160/// (|y−μ| = 0) from sending `ln(0) → −∞` at 1/1000 of the data scale, and the
161/// absolute term backstops the case where `global_scale` itself sits at its floor.
162pub(crate) const WARMSTART_RESIDUAL_REL_FLOOR: f64 = 1e-3;
163
164pub(crate) const WARMSTART_RESIDUAL_ABS_FLOOR: f64 = 1e-12;
165
166/// Floor on a per-row warm-start scale τ before forming `1/τ` when building the
167/// affine transformation seed targets. A degenerate τ = 0 (a collapsed warm-start
168/// scale block) would otherwise produce a non-finite reciprocal; the floor sits
169/// far below any meaningful scale so it only fires on the degenerate path.
170pub(crate) const WARMSTART_INV_SCALE_FLOOR: f64 = 1e-12;
171
172/// Ridge stabilization floor for the penalized least-squares projections that
173/// produce the default warm-start location and log-scale coefficients. These
174/// seeds only need to land in the right basin (the outer solver refines them),
175/// so a mild ridge that keeps the projection well-posed under a near-rank-
176/// deficient covariate design is preferable to the tighter floor used for the
177/// production inner solve.
178pub(crate) const WARMSTART_PROJECTION_RIDGE_FLOOR: f64 = 1e-8;