Skip to main content

gam_models/transformation_normal/
fit.rs

1use super::*;
2
3#[derive(Clone)]
4pub(crate) struct TransformationExactGeometryCache {
5    pub(crate) key: Vec<u64>,
6    pub(crate) covariate_spec_resolved: TermCollectionSpec,
7    pub(crate) covariate_design: TermCollectionDesign,
8    pub(crate) family: TransformationNormalFamily,
9    pub(crate) blocks: Vec<ParameterBlockSpec>,
10    pub(crate) hyper_layout: SharedCustomFamilyHyperLayout,
11}
12
13#[derive(Default)]
14pub(crate) struct TransformationExactModeBranch {
15    carried_mode: Option<CustomFamilyWarmStart>,
16    anchor: Option<CustomFamilyWarmStart>,
17    frozen: bool,
18}
19
20impl TransformationExactModeBranch {
21    /// Before the branch is frozen, compare a cold solve with the carried mode
22    /// at every value-only objective trial. This is a deterministic
23    /// mode-selection rule based on the profiled criterion, not coefficient
24    /// magnitude or cache distance. Freezing makes the carried candidate
25    /// immutable; cold remains a candidate so a worse anchor can never define
26    /// the profiled surface.
27    pub(crate) fn candidates(
28        &mut self,
29        eval_mode: gam_problem::EvalMode,
30        rho: &Array1<f64>,
31    ) -> (bool, Vec<Option<CustomFamilyWarmStart>>) {
32        let froze = self.prepare(eval_mode);
33        let carried = self
34            .frozen
35            .then_some(&self.anchor)
36            .unwrap_or(&self.carried_mode)
37            .as_ref()
38            .filter(|warm| warm.compatible_with_rho(rho))
39            .cloned();
40        match carried {
41            Some(warm) => (froze, vec![None, Some(warm)]),
42            None => (froze, vec![None]),
43        }
44    }
45
46    /// Freeze the INPUT mode for the first derivative-bearing seed evaluation.
47    /// Freezing before the solve makes that seed evaluation and every later
48    /// evaluation at the same theta restart from bit-identical coefficients.
49    pub(crate) fn prepare(&mut self, eval_mode: gam_problem::EvalMode) -> bool {
50        if self.frozen || matches!(eval_mode, gam_problem::EvalMode::ValueOnly) {
51            return false;
52        }
53        self.anchor = self.carried_mode.take();
54        self.frozen = true;
55        true
56    }
57
58    /// Value-only objective trials are the sole phase allowed to advance the
59    /// carried mode.
60    pub(crate) fn record_value(
61        &mut self,
62        eval_mode: gam_problem::EvalMode,
63        warm_start: CustomFamilyWarmStart,
64    ) {
65        if !self.frozen && matches!(eval_mode, gam_problem::EvalMode::ValueOnly) {
66            self.carried_mode = Some(warm_start);
67        }
68    }
69}
70
71impl TransformationExactGeometryCache {
72    pub(crate) fn update_block_log_lambdas(
73        &mut self,
74        log_lambdas: &Array1<f64>,
75    ) -> Result<(), String> {
76        let spec = self
77            .blocks
78            .first_mut()
79            .ok_or_else(|| "missing transformation block spec".to_string())?;
80        if log_lambdas.len() != spec.initial_log_lambdas.len() {
81            return Err(TransformationNormalError::InvalidInput {
82                reason: format!(
83                    "transformation final fit rho length mismatch: got {}, expected {}",
84                    log_lambdas.len(),
85                    spec.initial_log_lambdas.len()
86                ),
87            }
88            .into());
89        }
90        gam_problem::validate_log_strengths(log_lambdas.iter().copied()).map_err(|error| {
91            TransformationNormalError::InvalidInput {
92                reason: format!("invalid transformation smoothing strength: {error}"),
93            }
94        })?;
95        spec.initial_log_lambdas = log_lambdas.clone();
96        Ok(())
97    }
98}
99
100pub(crate) fn transformation_spatial_geometry_key(
101    spec: &TermCollectionSpec,
102    spatial_terms: &[usize],
103) -> Result<Vec<u64>, String> {
104    let mut key = Vec::new();
105    key.push(spatial_terms.len() as u64);
106    for &term_idx in spatial_terms {
107        let term = spec.smooth_terms.get(term_idx).ok_or_else(|| {
108            format!(
109                "transformation spatial geometry key term index {term_idx} out of range for {} smooth terms",
110                spec.smooth_terms.len()
111            )
112        })?;
113        key.push(term_idx as u64);
114
115        // The CTN exact-family cache is valid only for an identical covariate
116        // geometry. Length-scale and anisotropy scalars are not enough: the
117        // family also embeds frozen centers, input standardization scales,
118        // identifiability transforms, and active penalty topology. Serialize
119        // the already-frozen term and store the exact bytes in the key so a
120        // cache hit means the saved prediction design will replay the same
121        // matrix used by the final inner fit.
122        let payload = serde_json::to_vec(term).map_err(|err| {
123            format!("failed to serialize transformation spatial geometry term {term_idx}: {err}")
124        })?;
125        key.push(payload.len() as u64);
126        for chunk in payload.chunks(8) {
127            let mut bytes = [0u8; 8];
128            for (dst, src) in bytes.iter_mut().zip(chunk.iter().copied()) {
129                *dst = src;
130            }
131            key.push(u64::from_le_bytes(bytes));
132        }
133    }
134    Ok(key)
135}
136
137// ---------------------------------------------------------------------------
138// Top-level fit function
139// ---------------------------------------------------------------------------
140
141/// Result of `fit_transformation_normal`.
142#[derive(Clone)]
143pub struct TransformationNormalFitResult {
144    pub family: TransformationNormalFamily,
145    pub fit: UnifiedFitResult,
146    pub covariate_spec_resolved: TermCollectionSpec,
147    pub covariate_design: TermCollectionDesign,
148    pub score_calibration: TransformationScoreCalibration,
149}
150
151/// Fit a conditional transformation model with N-block spatial length-scale
152/// optimization over the covariate side.
153///
154/// The response-direction basis is built once (it does not depend on κ).
155/// If no spatial length-scale terms are present in the covariate spec, the
156/// model is fit directly. Otherwise, the N-block joint hyper-parameter
157/// optimizer is used with a single block (the covariate spec).
158pub fn fit_transformation_normal(
159    response: &Array1<f64>,
160    weights: &Array1<f64>,
161    offset: &Array1<f64>,
162    covariate_data: ArrayView2<'_, f64>,
163    covariate_spec: &TermCollectionSpec,
164    config: &TransformationNormalConfig,
165    options: &BlockwiseFitOptions,
166    kappa_options: &SpatialLengthScaleOptimizationOptions,
167    warm_start: Option<&TransformationWarmStart>,
168) -> Result<TransformationNormalFitResult, String> {
169    let mut options = options.clone();
170    // CTN advertises profiled outer-Hessian HVP support and supplies the
171    // callback derivative kernel consumed by the unified REML/LAML evaluator.
172    // Keep analytic curvature enabled here: the evaluator routes CTN Hessians
173    // through the matrix-free operator path instead of dense pairwise assembly.
174    let covariate_spec = covariate_spec.clone();
175
176    // 1. Build a bootstrap covariate design first so the response basis can
177    // adapt to the tensor width instead of always using the global default.
178    let boot_design = build_term_collection_design(covariate_data, &covariate_spec)
179        .map_err(|e| format!("failed to build bootstrap covariate design: {e}"))?;
180    let boot_spec = freeze_term_collection_from_design(&covariate_spec, &boot_design)
181        .map_err(|e| format!("failed to freeze bootstrap covariate spatial basis centers: {e}"))?;
182    let mut effective_config = config.clone();
183    // When the caller has already resolved the knot count (cross-fit pins it
184    // once at the smallest fold complement so every fold shares one p_resp),
185    // use it verbatim — re-applying the data-driven complexity cap on this
186    // fold's response subsample would round to a different count and break the
187    // fold-invariant `p₁` the cross-fit OOF assembly requires.
188    if !config.response_num_internal_knots_pinned {
189        effective_config.response_num_internal_knots = effective_response_num_internal_knots(
190            config,
191            response.len(),
192            boot_design.design.ncols(),
193            response.view(),
194        );
195    }
196
197    // 2. Build response basis ONCE — it is independent of κ once the effective
198    // response complexity has been chosen.
199    let (resp_val, resp_deriv, resp_penalties, resp_knots, resp_transform) =
200        build_response_basis(response, &effective_config)?;
201
202    // Scope the custom-family inner exact-Newton cycle budget to CTN's
203    // bounded-dimension, strictly convex (double-penalty) coefficient block.
204    // The realized tensor width is `p_resp · p_cov`; the cap grows with it so a
205    // genuinely high-dimensional nonlinear transformation keeps headroom, but a
206    // near-Gaussian shift can no longer spin the production large-scale cap (#720).
207    // Only ever *lower* the caller's cap so a deliberately tightened budget
208    // (screening / CI overrides) is respected.
209    let realized_p_total = resp_val.ncols().saturating_mul(boot_design.design.ncols());
210    let ctn_inner_cap = CTN_INNER_MAX_CYCLES_BASE
211        .saturating_add(realized_p_total.saturating_mul(CTN_INNER_MAX_CYCLES_PER_DIM))
212        .min(CTN_INNER_MAX_CYCLES_CEILING);
213    options.inner_max_cycles = options.inner_max_cycles.min(ctn_inner_cap);
214
215    // 3. Check whether spatial κ optimization is needed.
216    let spatial_terms = spatial_length_scale_term_indices(&covariate_spec);
217
218    if spatial_terms.is_empty() || !kappa_options.enabled {
219        // ------------------------------------------------------------------
220        // NO κ: build family directly, fit, return.
221        // ------------------------------------------------------------------
222        let cov_design = boot_design;
223        let cov_spec_resolved = boot_spec;
224        let effective_offset = cov_design
225            .compose_offset(offset.view(), "transformation-normal fit")
226            .map_err(|error| error.to_string())?;
227
228        let family = TransformationNormalFamily::from_prebuilt_response_basis(
229            response,
230            resp_val,
231            resp_deriv,
232            resp_penalties,
233            resp_knots.clone(),
234            effective_config.response_degree,
235            resp_transform,
236            weights,
237            &effective_offset,
238            cov_design.design.clone(),
239            cov_design
240                .penalties
241                .iter()
242                .map(|bp| bp.to_penalty_matrix(cov_design.design.ncols()))
243                .collect(),
244            &effective_config,
245            warm_start,
246        )?;
247        let rho0 = family.penalty_scale_log_lambdas()?;
248        let blocks = vec![family.block_spec(&rho0)?];
249        let fit = fit_custom_family(&family, &blocks, &options)
250            .map_err(|e| format!("transformation fit failed: {e}"))?;
251        let (fit, score_calibration) = calibrate_transformation_scores(&family, fit)?;
252
253        return Ok(TransformationNormalFitResult {
254            family,
255            fit,
256            covariate_spec_resolved: cov_spec_resolved,
257            covariate_design: cov_design,
258            score_calibration,
259        });
260    }
261
262    // ------------------------------------------------------------------
263    // YES κ: use the N-block spatial length-scale optimizer (1 block).
264    // ------------------------------------------------------------------
265
266    let kappa0 = SpatialLogKappaCoords::from_length_scales_aniso(
267        &covariate_spec,
268        &spatial_terms,
269        kappa_options,
270    )
271    .reseed_from_data(
272        covariate_data,
273        &covariate_spec,
274        &spatial_terms,
275        kappa_options,
276    )
277    .map_err(|error| error.to_string())?;
278    let kappa_dims = kappa0.dims_per_term().to_vec();
279    let kappa_lower = SpatialLogKappaCoords::lower_bounds_aniso_from_data(
280        covariate_data,
281        &covariate_spec,
282        &spatial_terms,
283        &kappa_dims,
284        kappa_options,
285    )
286    .map_err(|error| error.to_string())?;
287    let kappa_upper = SpatialLogKappaCoords::upper_bounds_aniso_from_data(
288        covariate_data,
289        &covariate_spec,
290        &spatial_terms,
291        &kappa_dims,
292        kappa_options,
293    )
294    .map_err(|error| error.to_string())?;
295    // Project seed onto bounds; spec.length_scale is a hint, not a constraint.
296    let kappa0 = kappa0.clamp_to_bounds(&kappa_lower, &kappa_upper);
297
298    // Check analytic derivative capability.
299    let analytic_psi_available =
300        build_block_spatial_psi_derivatives(covariate_data, &boot_spec, &boot_design)?.is_some();
301
302    // Rebuild from the frozen `boot_spec` so the probe's penalty topology
303    // matches the topology produced by every other build path in this
304    // optimization. The outer optimizer's own bootstrap
305    // (`build_term_collection_designs_and_freeze_joint(data, &[boot_spec])`)
306    // and the geometry cache's `build_term_collection_design(_, &effective_spec)`
307    // both feed the basis builder a frozen `FrozenTransform` identifiability,
308    // while `boot_design` was built from the raw `covariate_spec` with
309    // identifiability computed from scratch. Applying the captured
310    // `FrozenTransform` changes the exact coefficient chart of the penalty
311    // blocks. Without this rebuild, `n_penalties` is taken from the raw build
312    // but every subsequent
313    // evaluator measures the frozen build, and `evaluate_custom_family_joint_hyper`
314    // refuses with a `joint hyper rho dimension mismatch`.
315    let probe_design = build_term_collection_design(covariate_data, &boot_spec)
316        .map_err(|e| format!("failed to rebuild frozen probe covariate design: {e}"))?;
317    let probe_offset = probe_design
318        .compose_offset(offset.view(), "transformation-normal spatial probe")
319        .map_err(|error| error.to_string())?;
320
321    // Build an initial family + blocks for capability probing.
322    let probe_family = TransformationNormalFamily::from_prebuilt_response_basis(
323        response,
324        resp_val.clone(),
325        resp_deriv.clone(),
326        resp_penalties.clone(),
327        resp_knots.clone(),
328        effective_config.response_degree,
329        resp_transform.clone(),
330        weights,
331        &probe_offset,
332        probe_design.design.clone(),
333        probe_design
334            .penalties
335            .iter()
336            .map(|bp| bp.to_penalty_matrix(probe_design.design.ncols()))
337            .collect(),
338        &effective_config,
339        warm_start,
340    )?;
341    let rho0 = probe_family.penalty_scale_log_lambdas()?;
342    let probe_block = probe_family.block_spec(&rho0)?;
343    let n_penalties = probe_block.initial_log_lambdas.len();
344    log::info!(
345        "[transformation-normal] exact joint setup: rho_dim={} log_kappa_dim={} dims_per_term={:?}",
346        n_penalties,
347        kappa0.len(),
348        kappa_dims,
349    );
350    let rho_floor = -12.0;
351    let rho_lower = Array1::<f64>::from_elem(n_penalties, rho_floor);
352    let rho_upper = Array1::<f64>::from_elem(n_penalties, 12.0);
353    let probe_blocks = vec![probe_block.clone()];
354    let (_, cap_hessian) = crate::custom_family::custom_family_outer_derivatives(
355        &probe_family,
356        &probe_blocks,
357        &options,
358    );
359    let analytic_gradient = analytic_psi_available;
360    let analytic_hessian_supported = analytic_gradient && cap_hessian.is_analytic();
361    let analytic_hessian = analytic_hessian_supported;
362    if analytic_hessian {
363        log::info!(
364            "[transformation-normal] CTN exact joint analytic outer Hessian is available for spatial kappa optimization; using exact second-order outer geometry"
365        );
366    }
367
368    let (rho0_min, rho0_max) = if rho0.is_empty() {
369        (0.0, 0.0)
370    } else {
371        (
372            rho0.iter().copied().fold(f64::INFINITY, f64::min),
373            rho0.iter().copied().fold(f64::NEG_INFINITY, f64::max),
374        )
375    };
376    log::info!(
377        "[transformation-normal] skipping baseline custom-family prefit before exact joint optimization \
378         (rho_dim={}, log_kappa_dim={}, rho0_range=[{:.3}, {:.3}]); using CTN warm start and penalty-scale rho seed",
379        n_penalties,
380        kappa0.len(),
381        rho0_min,
382        rho0_max,
383    );
384
385    if !analytic_psi_available {
386        return Err(
387            "transformation-normal spatial length-scale optimization requires analytic spatial psi derivatives"
388                .to_string(),
389        );
390    }
391
392    // The finite-support normalized objective can have multiple coefficient
393    // modes. Value-only trials compare cold and carried modes;
394    // the first derivative-bearing evaluation freezes the selected mode's
395    // INPUT as the branch anchor. Every later trial restarts from that fixed
396    // anchor, making the profile independent of rejected-trial cache history.
397    let exact_mode_branch: RefCell<TransformationExactModeBranch> =
398        RefCell::new(TransformationExactModeBranch::default());
399
400    let joint_setup =
401        ExactJointHyperSetup::new(rho0, rho_lower, rho_upper, kappa0, kappa_lower, kappa_upper);
402
403    // Clone response basis parts for use inside closures.
404    let rv = resp_val.clone();
405    let rd = resp_deriv.clone();
406    let rp = resp_penalties.clone();
407    let rk = resp_knots.clone();
408    let rt = resp_transform.clone();
409    let rdeg = effective_config.response_degree;
410    let cfg = effective_config.clone();
411    let ws = warm_start.cloned();
412
413    // Helper: build family from prebuilt response basis + covariate design.
414    let make_family =
415        |cov_design: &TermCollectionDesign| -> Result<TransformationNormalFamily, String> {
416            let effective_offset = cov_design
417                .compose_offset(offset.view(), "transformation-normal spatial fit")
418                .map_err(|error| error.to_string())?;
419            TransformationNormalFamily::from_prebuilt_response_basis(
420                response,
421                rv.clone(),
422                rd.clone(),
423                rp.clone(),
424                rk.clone(),
425                rdeg,
426                rt.clone(),
427                weights,
428                &effective_offset,
429                cov_design.design.clone(),
430                cov_design
431                    .penalties
432                    .iter()
433                    .map(|bp| bp.to_penalty_matrix(cov_design.design.ncols()))
434                    .collect(),
435                &cfg,
436                ws.as_ref(),
437            )
438        };
439
440    let block_specs_slice = [boot_spec.clone()];
441    let block_term_indices_slice = [spatial_terms.clone()];
442    let exact_geometry_cache: RefCell<Option<TransformationExactGeometryCache>> =
443        RefCell::new(None);
444    let spatial_terms_for_cache = spatial_terms.clone();
445
446    let ensure_exact_geometry = |spec: &TermCollectionSpec,
447                                 design: &TermCollectionDesign,
448                                 rho: &Array1<f64>,
449                                 hyper_values: &Array1<f64>|
450     -> Result<(), String> {
451        let effective_spec = freeze_term_collection_from_design(spec, design)
452            .map_err(|e| format!("failed to freeze transformation geometry key: {e}"))?;
453        let key = transformation_spatial_geometry_key(&effective_spec, &spatial_terms_for_cache)?;
454        let needs_rebuild = exact_geometry_cache
455            .borrow()
456            .as_ref()
457            .map(|cached| cached.key != key)
458            .unwrap_or(true);
459        if !needs_rebuild {
460            let mut cache = exact_geometry_cache.borrow_mut();
461            let cached = cache
462                .as_mut()
463                .ok_or_else(|| "missing transformation exact geometry cache".to_string())?;
464            if cached.hyper_layout.values().len() != hyper_values.len()
465                || cached
466                    .hyper_layout
467                    .values()
468                    .iter()
469                    .zip(hyper_values)
470                    .any(|(cached, current)| cached.to_bits() != current.to_bits())
471            {
472                return Err(
473                    "transformation exact geometry key reused across distinct hypercoordinate values"
474                        .to_string(),
475                );
476            }
477            return cached.update_block_log_lambdas(rho);
478        }
479
480        let geom_start = std::time::Instant::now();
481        let exact_design = build_term_collection_design(covariate_data, &effective_spec)
482            .map_err(|e| format!("failed to rebuild frozen transformation geometry: {e}"))?;
483        let family = make_family(&exact_design)?;
484        let cov_psi_derivs =
485            build_block_spatial_psi_derivatives(covariate_data, &effective_spec, &exact_design)?
486                .ok_or_else(|| {
487                    "missing covariate spatial psi derivatives for transformation model".to_string()
488                })?;
489        let tensor_derivs = build_tensor_psi_derivatives(&family, &cov_psi_derivs)?;
490
491        log::debug!(
492            "[transformation-normal] rebuilt exact geometry cache for {} spatial terms in {:.3}s",
493            spatial_terms_for_cache.len(),
494            geom_start.elapsed().as_secs_f64(),
495        );
496
497        exact_geometry_cache.replace(Some(TransformationExactGeometryCache {
498            key,
499            covariate_spec_resolved: effective_spec,
500            covariate_design: exact_design,
501            blocks: vec![family.block_spec(rho)?],
502            family,
503            hyper_layout: Arc::new(CustomFamilyHyperLayout::new(
504                vec![tensor_derivs],
505                Vec::new(),
506                hyper_values.clone(),
507            )?),
508        }));
509        Ok(())
510    };
511
512    let exact_mode_candidates = |eval_mode: gam_problem::EvalMode,
513                                 rho: &Array1<f64>|
514     -> Vec<Option<CustomFamilyWarmStart>> {
515        let (froze, candidates) = exact_mode_branch.borrow_mut().candidates(eval_mode, rho);
516        if froze {
517            log::info!(
518                "[transformation-normal] froze deterministic exact coefficient-mode branch at the first derivative-bearing outer seed evaluation"
519            );
520        }
521        candidates
522    };
523
524    log::info!(
525        "[transformation-normal] entering exact joint outer optimization \
526         (analytic_gradient={}, analytic_hessian={})",
527        analytic_gradient,
528        analytic_hessian,
529    );
530    // Outer derivative policy (P2.3): consult the family's CTN-specific
531    // override so the cost gate uses the Khatri–Rao row-streamed shape
532    // (`O(n · (rho + psi) · p)` gradient; `min(dense, mfree)` Hessian)
533    // rather than the generic `coefficient_*_cost × K` default.
534    let outer_derivative_policy =
535        probe_family.outer_derivative_policy(&probe_blocks, joint_setup.log_kappa_dim(), &options);
536    let solved = optimize_spatial_length_scale_exact_joint(
537        covariate_data,
538        &block_specs_slice,
539        &block_term_indices_slice,
540        kappa_options,
541        &joint_setup,
542        gam_solve::seeding::SeedRiskProfile::Gaussian,
543        analytic_gradient,
544        analytic_hessian,
545        // Transformation-normal has β-dependent H (through 1/h'²), so the
546        // EFS Wood-Fasiolo PSD invariant fails. Keep fixed-point disabled while
547        // exposing the exact outer Hessian to ARC: hiding it forces this
548        // three-coordinate problem onto BFGS, whose Strong-Wolfe probes each
549        // repeat a full CTN inner solve and caused every large-scale lane to
550        // exhaust the 2400-second command budget before marginal-slope began.
551        true,
552        None,
553        outer_derivative_policy,
554        // fit_fn
555        |theta,
556         specs: &[TermCollectionSpec],
557         designs: &[TermCollectionDesign],
558         provenance: SpatialFitProvenance<'_, CustomFamilyJointHyperModeSelection>| {
559            let rho = theta.slice(s![..joint_setup.rho_dim()]).to_owned();
560            let hyper_values = theta.slice(s![joint_setup.rho_dim()..]).to_owned();
561            ensure_exact_geometry(&specs[0], &designs[0], &rho, &hyper_values)?;
562            let mut cache_ref = exact_geometry_cache.borrow_mut();
563            let geometry = cache_ref
564                .as_mut()
565                .ok_or_else(|| "missing transformation exact geometry cache".to_string())?;
566            let final_options = crate::outer_subsample::exact_outer_options_for_row_set(
567                &options,
568                &gam_problem::outer_subsample::RowSet::All,
569            );
570            let fit = match provenance {
571                SpatialFitProvenance::NoOuterOptimization => {
572                    let warm_starts =
573                        exact_mode_candidates(gam_problem::EvalMode::ValueOnly, &rho);
574                    let selection = evaluate_custom_family_joint_hyper_best_mode_shared(
575                        &geometry.family,
576                        &geometry.blocks,
577                        &final_options,
578                        &rho,
579                        Arc::clone(&geometry.hyper_layout),
580                        &warm_starts,
581                        gam_problem::EvalMode::ValueOnly,
582                    )
583                    .map_err(|e| format!("transformation fixed mode profile: {e}"))?;
584                    log::info!(
585                        "[transformation-normal] user-fixed coefficient mode selected candidate={} objective={:.16e}",
586                        selection.selected_candidate,
587                        selection.result.objective,
588                    );
589                    fit_custom_family_user_fixed_log_lambdas_from_mode_selection(
590                        &geometry.family,
591                        &geometry.blocks,
592                        &final_options,
593                        selection,
594                    )
595                }
596                SpatialFitProvenance::Certified { outer, mode: selection } => {
597                    log::info!(
598                        "[transformation-normal] consuming certified terminal coefficient mode candidate={} objective={:.16e} without profile replay",
599                        selection.selected_candidate,
600                        selection.result.objective,
601                    );
602                    fit_custom_family_fixed_log_lambdas_from_mode_selection(
603                        &geometry.family,
604                        &geometry.blocks,
605                        &final_options,
606                        selection,
607                        theta,
608                        outer,
609                    )
610                }
611            }
612            .map_err(|e| format!("transformation fit_fn: {e}"))?;
613            if let Some(block) = fit.block_states.first() {
614                *geometry
615                    .family
616                    .row_quantity_cache
617                    .lock()
618                    .expect("CTN row quantity cache mutex poisoned") = None;
619                let final_rows = geometry.family.row_quantities(&block.beta)?;
620                let max_abs_h = final_rows
621                    .h
622                    .iter()
623                    .copied()
624                    .map(f64::abs)
625                    .fold(0.0, f64::max);
626                let cov_chunk = geometry
627                    .family
628                    .covariate_design
629                    .try_row_chunk(0..response.len())
630                    .map_err(|err| {
631                        format!("final CTN covariate design validation failed: {err}")
632                    })?;
633                let max_abs_cov = cov_chunk.iter().copied().map(f64::abs).fold(0.0, f64::max);
634                log::info!(
635                    "[transformation-normal] final fixed-rho CTN validation: max_abs_h={:.6e} max_abs_covariate_basis={:.6e}",
636                    max_abs_h,
637                    max_abs_cov
638                );
639            }
640            Ok(TransformationNormalFitResult {
641                family: geometry.family.clone(),
642                fit,
643                covariate_spec_resolved: geometry.covariate_spec_resolved.clone(),
644                covariate_design: geometry.covariate_design.clone(),
645                score_calibration: TransformationScoreCalibration::finite_support_pit(),
646            })
647        },
648        // exact_fn
649        |theta,
650         specs: &[TermCollectionSpec],
651         designs: &[TermCollectionDesign],
652         eval_mode,
653         row_set| {
654            let rho = theta.slice(s![..joint_setup.rho_dim()]).to_owned();
655            let hyper_values = theta.slice(s![joint_setup.rho_dim()..]).to_owned();
656            ensure_exact_geometry(&specs[0], &designs[0], &rho, &hyper_values)?;
657            let mut cache_ref = exact_geometry_cache.borrow_mut();
658            let geometry = cache_ref
659                .as_mut()
660                .ok_or_else(|| "missing transformation exact geometry cache".to_string())?;
661            let warm_starts = exact_mode_candidates(eval_mode, &rho);
662            let competing_modes = warm_starts.len() > 1;
663            // `row_set` is the outer driver's authoritative measure. Rebuild
664            // the family-facing option on every evaluation so a pilot mask
665            // cannot survive the driver's rotation back to full data.
666            let eval_options =
667                crate::outer_subsample::exact_outer_options_for_row_set(&options, row_set);
668            let selection = evaluate_custom_family_joint_hyper_best_mode_shared(
669                &geometry.family,
670                &geometry.blocks,
671                &eval_options,
672                &rho,
673                Arc::clone(&geometry.hyper_layout),
674                &warm_starts,
675                eval_mode,
676            )
677            .map_err(|e| format!("transformation exact joint mode profile: {e}"))?;
678            for (candidate_idx, rejection) in selection.rejected_candidates.iter().enumerate() {
679                if let Some(rejection) = rejection {
680                    log::warn!(
681                        "[transformation-normal] rejected exact coefficient-mode candidate mode_candidate={candidate_idx}: {rejection}"
682                    );
683                }
684            }
685            if competing_modes {
686                for (candidate_idx, objective) in selection.screened_objectives.iter().enumerate() {
687                    if let Some(objective) = objective {
688                        let source = if candidate_idx == 0 {
689                            "cold"
690                        } else {
691                            "carried"
692                        };
693                        log::info!(
694                            "[transformation-normal] exact coefficient-mode screen source={} objective={:.16e}",
695                            source,
696                            objective,
697                        );
698                    }
699                }
700                let selected_source = if selection.selected_candidate == 0 {
701                    "cold"
702                } else {
703                    "carried"
704                };
705                log::info!(
706                    "[transformation-normal] selected exact coefficient mode source={} objective={:.16e}",
707                    selected_source,
708                    selection.result.objective,
709                );
710            }
711            let objective = selection.result.objective;
712            let gradient = selection.result.gradient.clone();
713            let outer_hessian = selection.result.outer_hessian.clone();
714            exact_mode_branch
715                .borrow_mut()
716                .record_value(eval_mode, selection.result.warm_start.clone());
717
718            Ok(ExactJointEvaluation {
719                objective,
720                gradient,
721                hessian: outer_hessian,
722                mode: selection,
723            })
724        },
725        |_theta,
726         _specs: &[TermCollectionSpec],
727         _designs: &[TermCollectionDesign],
728         _row_set| {
729            Err::<ExactJointEfsEvaluation<crate::custom_family::CustomFamilyJointHyperModeSelection>, String>("transformation-normal EFS callback invoked even though fixed-point optimization is disabled for beta-dependent exact curvature".to_string())
730        },
731        |_beta: &Array1<f64>| Ok(gam_solve::rho_optimizer::SeedOutcome::NoSlot),
732    )?;
733
734    let mut fit = solved.fit;
735    let (calibrated_fit, score_calibration) =
736        calibrate_transformation_scores(&fit.family, fit.fit.clone())?;
737    fit.fit = calibrated_fit;
738    fit.score_calibration = score_calibration;
739    Ok(fit)
740}