1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
//! The caller's spatial length-scale window, and the ONE expression that
//! projects onto it.
//!
//! gam#2726: this projection used to be spelled out inline inside each ψ seed
//! constructor in `term_specs.rs`, so the joint `[ρ, ψ]` route seeded ψ from the
//! PROJECTED `length_scale` while the scalar-ρ incumbent it is graded against
//! was fit from the spec's RAW one. On the `length_scale = 1e-3` /
//! `min_length_scale = 1e-2` arm the two routes were therefore evaluating one
//! criterion at two points exactly `ln 10` apart, and the joint route's
//! monotonicity certificate refused with `gap = 98.857` against
//! `accept_tol = 3.873e-5` while asserting `AT THE SAME POINT theta0` in its own
//! message. Sharing θ₀ collapses that gap to `7.185e-11`.
//!
//! Two consequences shape this module:
//!
//! * **One expression.** Both seed constructors and the upstream spec
//! projection derive ψ through [`spatial_term_seed_psi`] →
//! [`project_spatial_length_scale_into_window`], so the routes cannot drift
//! apart again by editing one of them.
//! * **Project once, upstream.** [`project_spatial_length_scales_in_spec`]
//! writes the projected value into the spec BEFORE the baseline fit, so every
//! later application is idempotent rather than a second, independent
//! projection. That matters because there are two projection sites onto the
//! same face (here and the seed's `clamp_to_bounds` against the data-derived
//! search box): removing either one alone changes nothing at runtime, because
//! the other re-projects by exactly `−ln 10`.
use *;
/// The ONE expression that projects a spatial `length_scale` onto the caller's
/// `[min_length_scale, max_length_scale]` window.
/// The ψ̄ = −ln(length_scale) seed for one spatial term, derived through
/// [`project_spatial_length_scale_into_window`]. Shared by
/// [`SpatialLogKappaCoords::from_length_scales`] and
/// [`SpatialLogKappaCoords::from_length_scales_aniso`] so the isotropic and
/// anisotropic routes cannot drift apart (gam#2726).
/// Project every listed spatial term's `length_scale` onto the caller's window
/// IN THE SPEC, once, before anything is fit from that spec.
///
/// gam#2726 repair candidate (b). The alternative — widening the joint ψ window
/// to contain a raw incumbent — was measured (it is the arm that proves the
/// diagnosis) and is deliberately not what ships: it would admit a
/// `length_scale` BELOW the caller's own `min_length_scale`, i.e. overrule an
/// explicit caller bound to make an internal comparison agree. The ρ analogue
/// (#1464/#2454) widens only as far as the engine's own `RHO_BOUND` and never
/// past a caller constraint. Projecting upstream instead keeps the caller's
/// window authoritative and makes the incumbent and the joint seed the same
/// point by construction — which is what the certificate's
/// `AT THE SAME POINT theta0` premise had been asserting in prose.
///
/// Constant-curvature terms are skipped. They DO carry a log-ℓ coordinate
/// (gam#2747), but both of their outer boxes are DERIVED from the term's own
/// geometry rather than configured: the κ interval is the half-margin to the
/// antipodal fold over `data ∪ centers`, and the range window is the span of
/// the center set's own pairwise chart distances. Projecting one of the two onto
/// a caller window that the curvature-inference path cannot see would put the
/// point estimate and its profile CI on two different boxes — the "two objective
/// owners" failure this subsystem already carries a scar from. Terms with no
/// explicit `length_scale` are likewise left alone so `reseed_from_data` keeps
/// ownership of their seed.
///
/// Returns `(term_idx, raw, projected)` for every term that actually moved.