Skip to main content

gam_solve/
constrained_posterior.rs

1//! First two moments of the constrained (truncated) Laplace posterior.
2//!
3//! # What the posterior is when inequality constraints are active
4//!
5//! A shape- or box-constrained fit restricts the PRIOR SUPPORT to the feasible
6//! set `C = {β : Aβ ≥ b}` (a user asserting monotonicity is asserting that the
7//! non-monotone coefficient vectors have prior mass zero). The posterior is
8//! therefore the penalized likelihood restricted to `C` and renormalized:
9//!
10//! ```text
11//! π(β | y) ∝ exp(−ℓ_p(β)) · 1_C(β)
12//! ```
13//!
14//! Expanding `ℓ_p` at the constrained mode `β̂` — where the KKT conditions give
15//! `g := ∇ℓ_p(β̂) = A_actᵀλ` with `λ ≥ 0` and `H := ∇²ℓ_p(β̂)` — and completing
16//! the square gives, to Laplace order,
17//!
18//! ```text
19//! π ≈ N(β; β_unc, Σ) truncated to C,   β_unc = β̂ − Σ·∇ℓ_p(β̂),   Σ = φ·H⁻¹
20//! ```
21//!
22//! The Gaussian is centred at the UNCONSTRAINED centre `β_unc`, not at the
23//! boundary mode: truncating a Gaussian does not move its pre-truncation mean,
24//! so a law centred at the KKT mode is a different (half-normal-shaped)
25//! distribution. This is the same law `sample_truncated_gaussian_posterior`
26//! draws from, and for the same reason.
27//!
28//! # The exact decomposition this module computes
29//!
30//! Let `A` be the retained constraint rows (`q × p`), `W = A Σ Aᵀ` and
31//! `G = Σ Aᵀ W⁻¹`, and let `P = I − G A` be the `Σ⁻¹`-orthogonal projector onto
32//! `null(A)`. Split the deviation `d = β − β_unc` as `d = P d + G(A d)` and set
33//! `t = P d`, `u = A β − b = (A β_unc − b) + A d`. Under the UNTRUNCATED law:
34//!
35//! * `Cov(t, u) = P Σ Aᵀ = Σ Aᵀ − Σ Aᵀ W⁻¹ (A Σ Aᵀ) = 0`, so `t` and `u` are
36//!   independent;
37//! * `E[t] = 0` and `Cov(t) = P Σ Pᵀ = Z(Zᵀ Σ⁻¹ Z)⁻¹Zᵀ` for any basis `Z` of
38//!   `null(A)`;
39//! * `u ~ N(A β_unc − b, W)`.
40//!
41//! Feasibility is exactly `u ≥ 0`, which constrains ONLY `u`. Because `t` is
42//! independent of `u`, conditioning leaves `t` untouched, so the truncated
43//! moments are exactly
44//!
45//! ```text
46//! E_π[β]   = β_unc + G·(E[u] − E_untrunc[u])
47//! Cov_π[β] = Σ − G·(W − Cov[u])·Gᵀ
48//! ```
49//!
50//! with `(E[u], Cov[u])` the first two moments of the `q`-dimensional Gaussian
51//! `N(A β_unc − b, W)` restricted to the orthant `u ≥ 0`.
52//!
53//! # Why the two obvious answers are both wrong
54//!
55//! Reading the covariance formula at its two endpoints:
56//!
57//! * `Cov[u] := W` (no truncation) returns `Σ` — the full unconstrained
58//!   covariance. This is the answer that ignores the constraint entirely, and
59//!   it over-states the spread along every constrained direction.
60//! * `Cov[u] := 0` returns `Σ − G W Gᵀ = P Σ Pᵀ`, the active-face reduction
61//!   `Z(ZᵀHZ)⁻¹Zᵀ`, which reports EXACTLY ZERO variance for a fully pinned
62//!   coordinate. That is the `λ → ∞` limit: it is correct only for a
63//!   constraint whose multiplier is infinite.
64//!
65//! An EQUALITY / gauge / identifiability constraint genuinely deletes a degree
66//! of freedom and zero variance is right for it — but those are absorbed into
67//! the basis in this codebase, so the deleted direction is not a coordinate at
68//! all. An INEQUALITY does not delete a direction, it halves one: the posterior
69//! along the constraint normal is supported on a half-line, its mode sits at
70//! the endpoint, and its variance does not. In the scalar case `X ~ N(μ,σ²)`
71//! truncated to `[0,∞)` with `α = −μ/σ` and `λ(α) = φ(α)/Φ̄(α)`,
72//! `Var(X) = σ²(1 + αλ − λ²)`, which is `0.3634σ²` when the mode sits exactly
73//! on the bound and decays as `σ²/α²` only as the multiplier diverges.
74//!
75//! Since truncating a Gaussian to a convex set can only shrink its covariance,
76//! `P Σ Pᵀ ≺ Cov_π[β] ≺ Σ` strictly at every finite multiplier.
77//!
78//! # No tightness predicate
79//!
80//! Nothing here classifies a row as "active". A row enters through its
81//! standardized slack `s_j = (a_jᵀβ_unc − b_j)/sd_j` and its contribution
82//! varies smoothly with it, so a row at slack `1e-9` and a row at slack `0`
83//! give nearly the same answer instead of differing by a full `σ²`. The only
84//! cut on TIGHTNESS is dropping rows whose truncated mass `Φ̄(s_j)` is below
85//! double-precision resolution, a bound read off `f64::EPSILON` rather than
86//! tuned.
87//!
88//! # Which rows the answer is built from
89//!
90//! There is a second cut, and it is about independence rather than tightness. A
91//! shape constraint is normally imposed at every data row — a monotone survival
92//! baseline arrives as one derivative-guard row per observed exit time — so the
93//! system can carry far more rows than the coefficient block has columns, and
94//! `W = A Σ Aᵀ` is then rank-deficient by construction. Its `q × q` inverse is
95//! not a quantity double precision has.
96//!
97//! So the correction is built on a RETAINED FACE. Rows are walked in ascending
98//! standardized slack and one joins only while its constraint normal is
99//! independent enough of those already accepted for the lift `G = Σ Aᵀ W⁻¹` to
100//! be solved to the accuracy the moments are reported to; the assembled face is
101//! then checked as a whole, because per-row independence is necessary and not
102//! sufficient, and its least independent row is dropped until the check passes.
103//!
104//! The two halves of that cut cost different amounts, and it is worth being
105//! exact about which is which.
106//!
107//! * The PER-ROW floor is nearly free. A row it refuses lies within `O(θ)` of
108//!   the span of the rows already accepted, with `θ` below `5e-7` radians at
109//!   the floor, and the accepted row it is nearly parallel to has the SMALLER
110//!   standardized slack because the walk is ordered by slack — so its wall is
111//!   the binding one and the refused row imposes nothing new. The exception is
112//!   a row ANTI-parallel to an accepted one, which carries no new direction
113//!   while genuinely halving the support; that one is kept, as the far wall of
114//!   a two-sided bound, rather than dropped.
115//!
116//! * The WHOLE-FACE check is not free, and no `O(θ)` argument covers it. It
117//!   fires exactly when every row cleared the floor and the face is still worse
118//!   conditioned than any of its rows, and the direction it then drops can sit
119//!   at a genuinely resolvable angle — `pivot/diagonal = 1e-3` is `θ ≈ 0.03`
120//!   radians, not `5e-7`. What is dropped there is a real constraint, and the
121//!   reported truncation is the one carried by the retained face.
122//!
123//! That trade is deliberate and it is the honest one available. The moments are
124//! computed for a BOX in the retained coordinates; a face that kept mutually
125//! dependent rows would cut the same region along diagonals, which is a general
126//! polyhedron and not something `box_truncated_moments` computes — and the lift
127//! that carries the answer back out of those coordinates cannot be formed at
128//! all when `W` is numerically singular. The alternatives are therefore a
129//! subset-truncated posterior or none, not a subset-truncated posterior or an
130//! exact one. Which rows survived is reported (`log::info!`) whenever the
131//! whole-face check dropped anything, so the choice is visible on the fit that
132//! made it rather than inferable from this file.
133
134use gam_math::probability::{
135    normal_cdf, normal_logsf, signed_probit_logcdf_and_mills_ratio, standard_normal_quantile,
136    standard_normal_quantile_from_log_cdf,
137};
138use gam_problem::LinearInequalityConstraints;
139use ndarray::{Array1, Array2, ArrayView2};
140use serde::{Deserialize, Serialize};
141
142/// Relative accuracy demanded of the orthant-moment cubature, measured against
143/// the PRE-TRUNCATION scale `sd_i = sqrt(W_ii)` so the criterion is invariant
144/// to how the constraint rows happen to be scaled.
145///
146/// The target is set by what the number is FOR. The cubature resolves `Δ`, the
147/// variance the truncation removes, and the reported variance is `Σ − GΔGᵀ`
148/// with the removed part never exceeding the total. A relative error `ε` in `Δ`
149/// therefore moves a reported variance by at most `ε` relative, and a reported
150/// standard error by at most `ε/2`. At `1e-3` no reported interval half-width
151/// can move in its fourth significant digit — far below the Laplace
152/// approximation's own error, and far below any resolution a credible interval
153/// carries. Demanding more is not free: the orthant integrand is unbounded at
154/// the cube boundary (the second moment grows like `log 1/(1−x)`), so the
155/// quasi-Monte-Carlo rate is closer to `N⁻¹` than the `N⁻²` a bounded
156/// integrand would give, and every extra digit costs two decades of nodes.
157const ORTHANT_MOMENT_RELATIVE_TOLERANCE: f64 = 1e-3;
158
159/// Cubature point count at the first pass. Each subsequent pass EXTENDS the
160/// node set to twice its length — the Kronecker sequence is a prefix sequence,
161/// so refinement reuses every node already evaluated — until the moments agree
162/// to [`ORTHANT_MOMENT_RELATIVE_TOLERANCE`]. This is a starting point rather
163/// than a budget.
164const ORTHANT_MOMENT_INITIAL_POINTS: usize = 1 << 11;
165
166/// Node count past which the cubature is declared non-convergent and the caller
167/// gets an error rather than an unconverged covariance. Silently reporting the
168/// last iterate would ship an uncertified number into every interval built from
169/// this fit.
170const ORTHANT_MOMENT_MAXIMUM_POINTS: usize = 1 << 20;
171
172/// The low-rank correction that turns the unconstrained Laplace covariance into
173/// the truncated-posterior covariance.
174///
175/// Carrying the factored form rather than a dense `p × p` matrix lets a
176/// consumer that never materializes `Σ` (the factorized inference path, the
177/// prediction backends) apply the same correction with `q` extra solves:
178/// `xᵀΣ_π x = xᵀΣx − ‖Δ^{1/2} Gᵀ x‖²`.
179#[derive(Clone, Debug, Serialize, Deserialize)]
180pub struct ConstrainedPosteriorCorrection {
181    /// `G = Σ Aᵀ W⁻¹`, `p × q`.
182    pub lift: Array2<f64>,
183    /// `Δ = W − Cov[u] ⪰ 0`, `q × q`: the variance the truncation removes from
184    /// the constraint-normal coordinates.
185    pub removed_normal_variance: Array2<f64>,
186    /// `E[u] − E_untrunc[u]`, `q`: how far truncation moves the posterior mean
187    /// in constraint-normal coordinates. Positive componentwise for a
188    /// half-line coordinate — the posterior mean is interior even when the mode
189    /// is not — and of either sign for a coordinate that also carries an upper
190    /// limit, where the far face pulls the mean back down.
191    pub normal_mean_shift: Array1<f64>,
192    /// Indices, into the caller's constraint system, of the rows retained.
193    pub rows: Vec<usize>,
194    /// Upper limit on each retained coordinate: `u_k ≤ normal_upper_limits[k]`,
195    /// with `f64::INFINITY` where the coordinate is a half-line.
196    ///
197    /// A two-sided coefficient bound `l ≤ β_j ≤ u` arrives as two rows whose
198    /// normals are exactly anti-parallel. The second carries no constraint-normal
199    /// DIRECTION the first does not already carry, so the rank filter drops it —
200    /// correctly, as a direction. It is still a constraint, and this is where it
201    /// is kept (#2523).
202    ///
203    /// `#[serde(default)]` with an empty vector reading as "every retained
204    /// coordinate is a half-line" is the encoding of a model saved before upper
205    /// limits existed, which is exactly what those models meant. Live
206    /// constructions always carry one entry per retained row.
207    ///
208    /// `+∞` is the COMMON case here — every one-sided shape or box constraint
209    /// contributes one — and JSON has no literal for it. Without an explicit
210    /// codec `serde_json` writes the entry as `null` and `Vec<f64>` then refuses
211    /// its own output on the way back in, which made every shape-constrained fit
212    /// that retains a face unloadable (#2601). `serde_extended_real::vec_f64`
213    /// makes `null` mean `+∞` in both directions; that is byte-identical to what
214    /// was already being written, so models saved before the codec existed read
215    /// back with the meaning they always had.
216    #[serde(default, with = "gam_problem::serde_extended_real::vec_f64")]
217    pub normal_upper_limits: Vec<f64>,
218}
219
220impl ConstrainedPosteriorCorrection {
221    /// `Σ ← Σ − G Δ Gᵀ`, in place. The correction is rank `q`, so this never
222    /// allocates a second `p × p` matrix next to the one being corrected.
223    pub fn apply_to_covariance_in_place(&self, covariance: &mut Array2<f64>) {
224        let scaled = self.lift.dot(&self.removed_normal_variance);
225        let p = covariance.nrows();
226        for i in 0..p {
227            for j in 0..=i {
228                let removed = scaled.row(i).dot(&self.lift.row(j));
229                covariance[[i, j]] -= removed;
230                if i != j {
231                    covariance[[j, i]] = covariance[[i, j]];
232                }
233            }
234        }
235    }
236
237    /// `Σ_π = Σ − G Δ Gᵀ`.
238    pub fn apply_to_covariance(&self, covariance: &Array2<f64>) -> Array2<f64> {
239        let mut corrected = covariance.clone();
240        self.apply_to_covariance_in_place(&mut corrected);
241        corrected
242    }
243
244    /// The same `Σ_π`, assembled as a SUM OF TWO GRAMS instead of as a
245    /// subtraction — so its diagonal cannot be a cancellation and cannot come
246    /// out negative (#2705 group A).
247    ///
248    /// `Σ − GΔGᵀ` is the difference of two nearly equal numbers exactly where
249    /// the answer matters most. A coordinate the constraint PINS has essentially
250    /// all of its variance removed: on `y ~ s(x, shape=convex)` the measured
251    /// entry went from `Σ_ii = 2.30e-2` to `6.23e-13` — eleven digits gone — and
252    /// on the neighbouring sqrt fixture the same subtraction lands at
253    /// `−3.09e-15`, which is not a small variance but a rounding residue with a
254    /// sign. Everything downstream (`se_from_covariance`, the dense SE loop)
255    /// then has to argue about whether that sign is real.
256    ///
257    /// Split the correction at `Δ = W − C`, `C = Cov[u] ⪰ 0` the truncated
258    /// constraint-normal covariance, and the same quantity is two Grams:
259    ///
260    /// ```text
261    ///     Σ − GΔGᵀ = (Σ − G W Gᵀ) + G C Gᵀ = P Σ Pᵀ + G C Gᵀ,   P = I − G A.
262    /// ```
263    ///
264    /// With `Σ = L Lᵀ` and `C = L_C L_Cᵀ` that is `(P L)(P L)ᵀ + (G L_C)(G L_C)ᵀ`,
265    /// and every diagonal entry is a sum of squares. The cancellation does not
266    /// disappear — it moves INSIDE `P L`, where each entry carries an absolute
267    /// error `O(ε‖L‖)` and is then SQUARED, so a pinned coordinate's variance
268    /// picks up `O(p ε² Σ_ii)` instead of `O(ε Σ_ii)`: sixteen orders smaller,
269    /// and non-negative by construction rather than by luck.
270    ///
271    /// `covariance` must be the SPD matrix this correction was built for, and
272    /// `constraints` the system its `rows` index. Both are exactly what the
273    /// caller already holds where a dense `Σ` exists at all.
274    ///
275    /// `C` is a cubature result, so it can carry a small negative eigenvalue —
276    /// `certify_removed_variance` admits `Δ_ii` up to `slack·W_ii` past `W_ii`.
277    /// Eigenvalues inside that certified band are read as the zero they are
278    /// approximating; anything below it is refused, because a `C` that is
279    /// materially indefinite is a broken moment computation and not a rounding
280    /// question.
281    pub fn truncated_covariance_psd(
282        &self,
283        covariance: &Array2<f64>,
284        constraints: &LinearInequalityConstraints,
285    ) -> Result<Array2<f64>, String> {
286        use gam_linalg::faer_ndarray::{FaerCholesky, FaerEigh};
287
288        let p = covariance.nrows();
289        if covariance.ncols() != p {
290            return Err(format!(
291                "truncated covariance needs a square Σ, got {}x{}",
292                covariance.nrows(),
293                covariance.ncols()
294            ));
295        }
296        if self.lift.nrows() != p {
297            return Err(format!(
298                "truncated covariance: the lift has {} rows against a {p}x{p} Σ",
299                self.lift.nrows()
300            ));
301        }
302        if constraints.a.ncols() != p {
303            return Err(format!(
304                "truncated covariance: the constraint system has {} columns against a {p}x{p} Σ",
305                constraints.a.ncols()
306            ));
307        }
308        let q = self.rows.len();
309        if self.lift.ncols() != q || self.removed_normal_variance.dim() != (q, q) {
310            return Err(format!(
311                "truncated covariance: {q} retained row(s) against a lift of {} column(s) and a \
312                 removed-variance block of {:?}",
313                self.lift.ncols(),
314                self.removed_normal_variance.dim()
315            ));
316        }
317        let mut retained = Array2::<f64>::zeros((q, p));
318        for (position, &row) in self.rows.iter().enumerate() {
319            if row >= constraints.a.nrows() {
320                return Err(format!(
321                    "truncated covariance: retained row {row} is outside the {}-row constraint \
322                     system it indexes",
323                    constraints.a.nrows()
324                ));
325            }
326            retained.row_mut(position).assign(&constraints.a.row(row));
327        }
328
329        // `W = A Σ Aᵀ` on the retained face, recomputed from the very Σ being
330        // corrected so `C = W − Δ` cannot mix two different covariances.
331        let sigma_at = covariance.dot(&retained.t());
332        let mut w = retained.dot(&sigma_at);
333        gam_linalg::matrix::symmetrize_in_place(&mut w);
334        let mut truncated_normal = &w - &self.removed_normal_variance;
335        gam_linalg::matrix::symmetrize_in_place(&mut truncated_normal);
336
337        let (eigenvalues, eigenvectors) = truncated_normal
338            .eigh(faer::Side::Lower)
339            .map_err(|error| format!("truncated constraint-normal covariance eigendecomposition: {error:?}"))?;
340        // The cubature certifies `Δ` to `ORTHANT_MOMENT_RELATIVE_TOLERANCE`
341        // relative to the PRE-TRUNCATION scale, so that same band — carried on
342        // the largest pre-truncation variance, times the face dimension the
343        // certificate is stated over — is the resolution at which an eigenvalue
344        // of `C = W − Δ` can be called negative.
345        let pre_truncation_scale = (0..q).fold(0.0_f64, |worst, index| worst.max(w[[index, index]]));
346        let negative_floor = -ORTHANT_MOMENT_RELATIVE_TOLERANCE * (q as f64) * pre_truncation_scale;
347        let mut normal_factor = Array2::<f64>::zeros((q, q));
348        for index in 0..q {
349            let eigenvalue = eigenvalues[index];
350            if !eigenvalue.is_finite() {
351                return Err(format!(
352                    "truncated constraint-normal covariance has a non-finite eigenvalue at {index}"
353                ));
354            }
355            if eigenvalue < negative_floor {
356                return Err(format!(
357                    "the truncated constraint-normal covariance is materially indefinite: \
358                     eigenvalue {eigenvalue:.6e} at {index} is below the cubature's own \
359                     resolution {negative_floor:.6e} (pre-truncation scale \
360                     {pre_truncation_scale:.6e} over {q} retained row(s))"
361                ));
362            }
363            let scale = eigenvalue.max(0.0).sqrt();
364            for row in 0..q {
365                normal_factor[[row, index]] = eigenvectors[[row, index]] * scale;
366            }
367        }
368
369        let sigma_factor = covariance
370            .cholesky(faer::Side::Lower)
371            .map_err(|error| {
372                format!("truncated covariance requires an SPD Σ to factor: {error:?}")
373            })?
374            .lower_triangular();
375        // `P L = L − G(A L)`, `O(p²q)` rather than the `O(p³)` a materialized
376        // `P` would cost.
377        let projected_factor = &sigma_factor - &self.lift.dot(&retained.dot(&sigma_factor));
378        let normal_lift = self.lift.dot(&normal_factor);
379
380        let mut truncated = projected_factor.dot(&projected_factor.t());
381        truncated += &normal_lift.dot(&normal_lift.t());
382        gam_linalg::matrix::symmetrize_in_place(&mut truncated);
383        // State the diagonal's non-negativity rather than inheriting it from
384        // whatever order a GEMM happens to accumulate in — the same reason
385        // `smoothing_correction_gram` writes its own diagonal.
386        for index in 0..p {
387            let projected_row = projected_factor.row(index);
388            let normal_row = normal_lift.row(index);
389            truncated[[index, index]] =
390                projected_row.dot(&projected_row) + normal_row.dot(&normal_row);
391        }
392        Ok(truncated)
393    }
394
395    /// `diag(G Δ Gᵀ)` — the per-coefficient variance the truncation removes,
396    /// for consumers that only ever build the covariance diagonal.
397    pub fn removed_variance_diagonal(&self) -> Array1<f64> {
398        let scaled = self.lift.dot(&self.removed_normal_variance);
399        let p = self.lift.nrows();
400        let mut diagonal = Array1::<f64>::zeros(p);
401        for i in 0..p {
402            diagonal[i] = scaled.row(i).dot(&self.lift.row(i));
403        }
404        diagonal
405    }
406
407    /// The absolute per-coefficient uncertainty this correction contributes to
408    /// `diag(Σ − GΔGᵀ)`.
409    ///
410    /// `Δ` is a cubature result, not an exact quantity: it is certified to
411    /// `ORTHANT_MOMENT_RELATIVE_TOLERANCE` relative (`certify_removed_variance`),
412    /// and `(GΔGᵀ)_ii = g_iᵀ Δ g_i` is monotone in `Δ` in the PSD order, so a
413    /// relative error `ε` in `Δ` moves the removed variance by at most
414    /// `ε · (GΔGᵀ)_ii`. That product — and NOT the floating-point backward error
415    /// of the subtraction — is the resolution at which `diag(Σ − GΔGᵀ)` can be
416    /// read.
417    ///
418    /// This accessor exists so that `ORTHANT_MOMENT_RELATIVE_TOLERANCE` is
419    /// declared once and converted into a consumer-facing allowance once. #2705
420    /// group A is fits refused because the consumer (`se_from_covariance`,
421    /// budget `16·n·eps` ≈ 1e-14 relative) and the producer (this cubature,
422    /// budget 1e-3 relative) hold two independent budgets for one number, ~11
423    /// orders apart, with nothing carrying the producer's across the boundary.
424    pub fn diagonal_uncertainty(&self) -> Array1<f64> {
425        self.removed_variance_diagonal() * ORTHANT_MOMENT_RELATIVE_TOLERANCE
426    }
427
428    /// `E_π[β] = β_unc + G·(E[u] − E_untrunc[u])`.
429    pub fn posterior_mean(&self, unconstrained_center: &Array1<f64>) -> Array1<f64> {
430        unconstrained_center + &self.lift.dot(&self.normal_mean_shift)
431    }
432
433    /// Upper limit per retained coordinate, materializing the legacy encoding of
434    /// [`Self::normal_upper_limits`].
435    pub fn upper_limits(&self) -> Vec<f64> {
436        if self.normal_upper_limits.is_empty() {
437            vec![f64::INFINITY; self.rows.len()]
438        } else {
439            self.normal_upper_limits.clone()
440        }
441    }
442}
443
444/// Live properness evidence carried by a declined cone-posterior moment route.
445#[derive(Clone, Debug, Serialize, Deserialize)]
446pub enum ConePropernessEvidence {
447    Certificate(crate::cone_reduction::ConeProperness),
448    CertificationFailed { reason: String },
449}
450
451impl ConePropernessEvidence {
452    pub fn is_proper(&self) -> Option<bool> {
453        match self {
454            Self::Certificate(certificate) => certificate.is_proper(),
455            Self::CertificationFailed { .. } => None,
456        }
457    }
458
459    pub fn summary(&self) -> String {
460        match self {
461            Self::Certificate(certificate) => certificate.summary(),
462            Self::CertificationFailed { reason } => format!(
463                "cone-truncated posterior properness could not be certified: {reason}"
464            ),
465        }
466    }
467
468    fn validate(&self, ambient_dimension: usize, constraint_count: usize) -> Result<(), String> {
469        match self {
470            Self::CertificationFailed { reason } => {
471                if reason.trim().is_empty() {
472                    return Err(
473                        "cone properness certification failure has an empty reason".to_string(),
474                    );
475                }
476            }
477            Self::Certificate(certificate) => {
478                if certificate.reduced.dim() != (constraint_count, constraint_count) {
479                    return Err(format!(
480                        "cone properness reduced precision has shape {:?}, expected ({constraint_count}, {constraint_count})",
481                        certificate.reduced.dim(),
482                    ));
483                }
484                if certificate.reduced.iter().any(|value| !value.is_finite())
485                    || certificate
486                        .copositive_minimum
487                        .is_some_and(|value| !value.is_finite())
488                {
489                    return Err(
490                        "cone properness certificate contains a non-finite value".to_string(),
491                    );
492                }
493                let total = |inertia: crate::cone_reduction::Inertia| {
494                    inertia.positive + inertia.zero + inertia.negative
495                };
496                if total(certificate.ambient_inertia) != ambient_dimension
497                    || total(certificate.reduced_inertia) != constraint_count
498                    || total(certificate.lineality_inertia)
499                        != ambient_dimension.saturating_sub(constraint_count)
500                {
501                    return Err(format!(
502                        "cone properness inertia dimensions disagree with ambient p={ambient_dimension} and face q={constraint_count}"
503                    ));
504                }
505                if certificate.ambient_inertia.positive
506                    != certificate.reduced_inertia.positive
507                        + certificate.lineality_inertia.positive
508                    || certificate.ambient_inertia.zero
509                        != certificate.reduced_inertia.zero
510                            + certificate.lineality_inertia.zero
511                    || certificate.ambient_inertia.negative
512                        != certificate.reduced_inertia.negative
513                            + certificate.lineality_inertia.negative
514                {
515                    return Err(
516                        "cone properness certificate violates Haynsworth inertia additivity"
517                            .to_string(),
518                    );
519                }
520                if certificate.is_proper() == Some(false) {
521                    return Err(
522                        "a proved-improper cone posterior cannot be stored as a moment decline"
523                            .to_string(),
524                    );
525                }
526            }
527        }
528        Ok(())
529    }
530}
531
532/// Why a converged constrained fit has no reportable posterior moments.
533#[derive(Clone, Debug, Serialize, Deserialize)]
534pub struct ConePosteriorMomentDecline {
535    pub ambient_precision_failure: String,
536    pub properness: ConePropernessEvidence,
537}
538
539impl ConePosteriorMomentDecline {
540    pub fn summary(&self) -> String {
541        format!(
542            "ambient covariance route declined ({}); {}",
543            self.ambient_precision_failure,
544            self.properness.summary(),
545        )
546    }
547}
548
549/// Required wire discriminator for the formerly-overloaded `None` state.
550#[derive(Clone, Debug, Serialize, Deserialize)]
551pub enum ConstrainedPosteriorMomentStatus {
552    Available,
553    Declined(ConePosteriorMomentDecline),
554}
555
556/// Persisted identity of an inequality-truncated Laplace posterior.
557///
558/// `mode` is always the feasible optimizer solution. When moments are
559/// available, the ambient centre and correction define the posterior-mean
560/// estimand. When they are declined, the cone, mode, and properness evidence
561/// survive without fabricating either unavailable moment.
562#[derive(Clone, Debug, Serialize, Deserialize)]
563pub struct ConstrainedPosteriorGeometry {
564    /// Exact inequality system `Aβ ≥ b` in the same coefficient frame as the
565    /// locations, correction lift, and ambient precision.
566    pub constraints: LinearInequalityConstraints,
567    /// Feasible optimizer solution; never a substitute for a declined posterior mean.
568    pub mode: Array1<f64>,
569    unconstrained_center: Option<Array1<f64>>,
570    /// Present only when `moment_status` is `Available`; `None` then means the
571    /// constraint is invisible at f64 resolution.
572    correction: Option<ConstrainedPosteriorCorrection>,
573    /// Whether reportable moments exist, with typed evidence when they do not.
574    pub moment_status: ConstrainedPosteriorMomentStatus,
575}
576
577impl ConstrainedPosteriorGeometry {
578    pub fn with_moments(
579        constraints: LinearInequalityConstraints,
580        mode: Array1<f64>,
581        unconstrained_center: Array1<f64>,
582        correction: Option<ConstrainedPosteriorCorrection>,
583    ) -> Self {
584        Self {
585            constraints,
586            mode,
587            unconstrained_center: Some(unconstrained_center),
588            correction,
589            moment_status: ConstrainedPosteriorMomentStatus::Available,
590        }
591    }
592
593    pub fn with_decline(
594        constraints: LinearInequalityConstraints,
595        mode: Array1<f64>,
596        decline: ConePosteriorMomentDecline,
597    ) -> Self {
598        Self {
599            constraints,
600            mode,
601            unconstrained_center: None,
602            correction: None,
603            moment_status: ConstrainedPosteriorMomentStatus::Declined(decline),
604        }
605    }
606
607    pub fn decline(&self) -> Option<&ConePosteriorMomentDecline> {
608        match &self.moment_status {
609            ConstrainedPosteriorMomentStatus::Available => None,
610            ConstrainedPosteriorMomentStatus::Declined(decline) => Some(decline),
611        }
612    }
613
614    pub fn unconstrained_center(&self) -> Result<&Array1<f64>, String> {
615        match &self.moment_status {
616            ConstrainedPosteriorMomentStatus::Available => self
617                .unconstrained_center
618                .as_ref()
619                .ok_or_else(|| {
620                    "available constrained posterior is missing its ambient centre".to_string()
621                }),
622            ConstrainedPosteriorMomentStatus::Declined(decline) => Err(format!(
623                "constrained posterior has no ambient centre because its moments were declined: {}",
624                decline.summary(),
625            )),
626        }
627    }
628
629    pub fn correction(&self) -> Result<Option<&ConstrainedPosteriorCorrection>, String> {
630        match &self.moment_status {
631            ConstrainedPosteriorMomentStatus::Available => Ok(self.correction.as_ref()),
632            ConstrainedPosteriorMomentStatus::Declined(decline) => Err(format!(
633                "constrained posterior has no moment correction because its moments were declined: {}",
634                decline.summary(),
635            )),
636        }
637    }
638
639    pub fn available_parts_mut(
640        &mut self,
641    ) -> Option<(&mut Array1<f64>, Option<&mut ConstrainedPosteriorCorrection>)> {
642        match &self.moment_status {
643            ConstrainedPosteriorMomentStatus::Available => Some((
644                self.unconstrained_center.as_mut()?,
645                self.correction.as_mut(),
646            )),
647            ConstrainedPosteriorMomentStatus::Declined(_) => None,
648        }
649    }
650
651    pub fn posterior_mean(&self) -> Result<Array1<f64>, String> {
652        let center = self.unconstrained_center()?;
653        Ok(self
654            .correction()?
655            .map(|correction| correction.posterior_mean(center))
656            .unwrap_or_else(|| center.clone()))
657    }
658
659    pub fn validate_for_dimension(&self, dimension: usize) -> Result<(), String> {
660        if self.constraints.a.ncols() != dimension
661            || self.constraints.a.nrows() != self.constraints.b.len()
662        {
663            return Err(format!(
664                "constrained posterior inequalities have shape {}x{} with {} bounds, expected {dimension} columns",
665                self.constraints.a.nrows(),
666                self.constraints.a.ncols(),
667                self.constraints.b.len()
668            ));
669        }
670        if self.mode.len() != dimension {
671            return Err(format!(
672                "constrained posterior mode has length {}, expected {dimension}",
673                self.mode.len(),
674            ));
675        }
676        if self
677            .mode
678            .iter()
679            .chain(self.unconstrained_center.iter().flat_map(|center| center.iter()))
680            .chain(self.constraints.a.iter())
681            .chain(self.constraints.b.iter())
682            .any(|value| !value.is_finite())
683        {
684            return Err("constrained posterior geometry contains a non-finite value".to_string());
685        }
686        match &self.moment_status {
687            ConstrainedPosteriorMomentStatus::Available => {
688                if self
689                    .unconstrained_center
690                    .as_ref()
691                    .is_none_or(|center| center.len() != dimension)
692                {
693                    return Err(format!(
694                        "available constrained posterior centre has length {:?}, expected {dimension}",
695                        self.unconstrained_center.as_ref().map(Array1::len),
696                    ));
697                }
698            }
699            ConstrainedPosteriorMomentStatus::Declined(decline) => {
700                if self.unconstrained_center.is_some() || self.correction.is_some() {
701                    return Err(
702                        "declined constrained posterior must not carry fabricated ambient moments"
703                            .to_string(),
704                    );
705                }
706                if decline.ambient_precision_failure.trim().is_empty() {
707                    return Err(
708                        "constrained posterior moment decline has an empty ambient-precision reason"
709                            .to_string(),
710                    );
711                }
712                decline.properness.validate(dimension, self.constraints.a.nrows())?;
713            }
714        }
715        if let Some(correction) = self.correction.as_ref() {
716            let q = correction.lift.ncols();
717            if correction.lift.nrows() != dimension {
718                return Err(format!(
719                    "constrained posterior lift has {} rows, expected {dimension}",
720                    correction.lift.nrows()
721                ));
722            }
723            if correction.removed_normal_variance.dim() != (q, q)
724                || correction.normal_mean_shift.len() != q
725                || correction.rows.len() != q
726            {
727                return Err(format!(
728                    "constrained posterior normal geometry is inconsistent: lift={}x{q}, removed={:?}, mean={}, rows={}",
729                    correction.lift.nrows(),
730                    correction.removed_normal_variance.dim(),
731                    correction.normal_mean_shift.len(),
732                    correction.rows.len()
733                ));
734            }
735            let mut unique_rows = correction.rows.clone();
736            unique_rows.sort_unstable();
737            unique_rows.dedup();
738            if unique_rows.len() != q
739                || unique_rows
740                    .iter()
741                    .any(|&row| row >= self.constraints.a.nrows())
742            {
743                return Err(format!(
744                    "constrained posterior retained rows {:?} are not unique valid indices for {} inequalities",
745                    correction.rows,
746                    self.constraints.a.nrows()
747                ));
748            }
749            if correction
750                .lift
751                .iter()
752                .chain(correction.removed_normal_variance.iter())
753                .chain(correction.normal_mean_shift.iter())
754                .any(|value| !value.is_finite())
755            {
756                return Err(
757                    "constrained posterior correction contains a non-finite value".to_string()
758                );
759            }
760            if !correction.normal_upper_limits.is_empty()
761                && correction.normal_upper_limits.len() != q
762            {
763                return Err(format!(
764                    "constrained posterior carries {} upper limits for {q} retained rows",
765                    correction.normal_upper_limits.len()
766                ));
767            }
768            // `+∞` is the half-line coordinate and is admissible; anything at or
769            // below the wall would make the retained region empty.
770            if correction
771                .normal_upper_limits
772                .iter()
773                .any(|limit| !(*limit > 0.0))
774            {
775                return Err(format!(
776                    "constrained posterior upper limits must be positive, got {:?}",
777                    correction.normal_upper_limits
778                ));
779            }
780        }
781        Ok(())
782    }
783}
784
785/// Equal-tailed interval for one linear projection of an inequality-truncated
786/// Gaussian posterior.
787///
788/// `ambient_covariance` is the pre-truncation covariance `Σ` in the active
789/// coefficient frame and `contrast` defines the scalar `cᵀβ`.  The affine
790/// shift of a saved coefficient gauge is deliberately not accepted here:
791/// callers add that deterministic shift to both returned endpoints.
792///
793/// The decomposition in this module makes the projection
794///
795/// ```text
796/// cᵀβ = cᵀβ_unc + cᵀt + (Gᵀc)ᵀ(u - E_untrunc[u]),
797/// ```
798///
799/// where `cᵀt` is an independent scalar Gaussian and `u` is the retained
800/// orthant-truncated Gaussian.  The interval therefore comes from the quantiles
801/// of that convolution, not from `posterior_mean ± z·posterior_sd`.
802/// The decomposition every scalar-projection consumer in this module needs, in
803/// one place.
804///
805/// Two consumers read it — the equal-tailed interval and
806/// [`constrained_projection_law`] — and it is exactly the kind of derivation
807/// that is individually reasonable and quietly different when written twice.
808/// That is the failure genus this sweep is about (#2385), so it is written once.
809struct TruncatedProjection {
810    /// `E_π[cᵀβ]`.
811    posterior_mean: f64,
812    /// Retained constraint-normal coordinates `u`: centre, covariance, walls.
813    normal_center: Array1<f64>,
814    normal_covariance: Array2<f64>,
815    upper_limits: Vec<f64>,
816    /// `Gᵀc`: how a displacement of `u` moves the projection.
817    projection_lift: Array1<f64>,
818    /// `Var(cᵀt)`, the tangent component that stays Gaussian and independent of
819    /// `u`. Exactly zero when `c` is carried entirely by the retained
820    /// constraint normals, which is the case a product cone on one block gives.
821    residual_variance: f64,
822}
823
824struct ProjectionDecomposition {
825    ambient_mean: f64,
826    ambient_variance: f64,
827    /// `None` exactly when the geometry carries no correction: the truncation
828    /// is invisible at f64 resolution and the projection is the ambient normal.
829    truncated: Option<TruncatedProjection>,
830}
831
832fn decompose_projection(
833    ambient_covariance: &Array2<f64>,
834    geometry: &ConstrainedPosteriorGeometry,
835    contrast: &Array1<f64>,
836) -> Result<ProjectionDecomposition, String> {
837    let p = contrast.len();
838    geometry.validate_for_dimension(p)?;
839    if ambient_covariance.dim() != (p, p) {
840        return Err(format!(
841            "constrained projection needs a {p}x{p} ambient covariance, got {:?}",
842            ambient_covariance.dim()
843        ));
844    }
845    if ambient_covariance.iter().any(|value| !value.is_finite())
846        || contrast.iter().any(|value| !value.is_finite())
847    {
848        return Err(
849            "constrained projection received a non-finite covariance or contrast".to_string(),
850        );
851    }
852
853    let ambient_mean = contrast.dot(geometry.unconstrained_center()?);
854    let sigma_c = ambient_covariance.dot(contrast);
855    let ambient_variance = contrast.dot(&sigma_c);
856    let covariance_scale = ambient_covariance
857        .diag()
858        .iter()
859        .map(|value| value.abs())
860        .fold(f64::MIN_POSITIVE, f64::max);
861    let contrast_scale = contrast.dot(contrast).max(f64::MIN_POSITIVE);
862    let variance_floor = (p.max(1) as f64) * f64::EPSILON * covariance_scale * contrast_scale;
863    if ambient_variance < -variance_floor || !ambient_variance.is_finite() {
864        return Err(format!(
865            "constrained projection has invalid ambient variance {ambient_variance:.6e}"
866        ));
867    }
868    let ambient_variance = ambient_variance.max(0.0);
869
870    let Some(correction) = geometry.correction()? else {
871        return Ok(ProjectionDecomposition {
872            ambient_mean,
873            ambient_variance,
874            truncated: None,
875        });
876    };
877
878    let q = correction.rows.len();
879    let mut normal_center = Array1::<f64>::zeros(q);
880    let mut normal_covariance = Array2::<f64>::zeros((q, q));
881    let mut sigma_a = Array2::<f64>::zeros((p, q));
882    for (position, &row) in correction.rows.iter().enumerate() {
883        let a = geometry.constraints.a.row(row);
884        normal_center[position] =
885            a.dot(geometry.unconstrained_center()?) - geometry.constraints.b[row];
886        sigma_a
887            .column_mut(position)
888            .assign(&ambient_covariance.dot(&a));
889    }
890    for i in 0..q {
891        let ai = geometry.constraints.a.row(correction.rows[i]);
892        for j in 0..=i {
893            let value = ai.dot(&sigma_a.column(j));
894            normal_covariance[[i, j]] = value;
895            normal_covariance[[j, i]] = value;
896        }
897    }
898
899    let projection_lift = correction.lift.t().dot(contrast);
900    let normal_component_variance = projection_lift.dot(&normal_covariance.dot(&projection_lift));
901    let residual_variance = ambient_variance - normal_component_variance;
902    let residual_floor = (p.max(q).max(1) as f64)
903        * f64::EPSILON
904        * ambient_variance
905            .max(normal_component_variance)
906            .max(f64::MIN_POSITIVE);
907    if residual_variance < -residual_floor || !residual_variance.is_finite() {
908        return Err(format!(
909            "constrained projection decomposition produced residual variance \
910             {residual_variance:.6e} from ambient {ambient_variance:.6e}"
911        ));
912    }
913    let residual_variance = residual_variance.max(0.0);
914    let posterior_mean = ambient_mean + projection_lift.dot(&correction.normal_mean_shift);
915    let upper_limits = correction.upper_limits();
916    if upper_limits.len() != q {
917        return Err(format!(
918            "constrained projection: {q} retained rows carry {} upper limits",
919            upper_limits.len()
920        ));
921    }
922    Ok(ProjectionDecomposition {
923        ambient_mean,
924        ambient_variance,
925        truncated: Some(TruncatedProjection {
926            posterior_mean,
927            normal_center,
928            normal_covariance,
929            upper_limits,
930            projection_lift,
931            residual_variance,
932        }),
933    })
934}
935
936/// The LAW of one scalar projection `cᵀβ` of an inequality-truncated Gaussian
937/// posterior — not two of its quantiles, and not a normal fitted to its first
938/// two moments.
939///
940/// This is what the module's own decomposition produces:
941///
942/// ```text
943/// cᵀβ = cᵀβ_unc + (Gᵀc)ᵀ(u - E_untrunc[u]) + cᵀt,
944/// ```
945///
946/// a discrete mixture over the retained orthant's cubature nodes convolved with
947/// one independent Gaussian. Two properties a normal cannot have, and both are
948/// why this exists (#2446):
949///
950/// * **Every node is feasible.** The nodes are points of the retained orthant,
951///   so the law puts no mass on coefficient vectors the fit excluded. The
952///   normal with the same first two moments does — measurably, a few percent of
953///   its mass — because the pushforward of a cone-truncated joint through `cᵀ`
954///   is not a normal for `q > 1`.
955/// * **Its error is a RATE, not a floor.** Matching two moments is exact for a
956///   normal and wrong by a fixed amount for this law, so no extra work reduces
957///   it. The node sum converges with the cubature.
958///
959/// A consumer that integrates a SMOOTH functional barely notices the first
960/// property. One that integrates an indicator — any quantile, any exceedance
961/// probability — reads the location of mass at first order, and for it the
962/// moment-matched normal is not admissible at all.
963pub struct ConstrainedProjectionLaw {
964    /// `(location, weight)` per cubature node, weights summing to one. A
965    /// geometry with no correction is one node of weight one at the ambient
966    /// mean.
967    pub nodes: Vec<(f64, f64)>,
968    /// Variance of the independent Gaussian each node is convolved with. Zero
969    /// when the contrast is carried entirely by the retained constraint
970    /// normals, and then the mixture IS the whole law.
971    pub residual_variance: f64,
972}
973
974impl ConstrainedProjectionLaw {
975    /// `E[cᵀβ]` under this law.
976    pub fn mean(&self) -> f64 {
977        self.nodes
978            .iter()
979            .map(|(location, weight)| location * weight)
980            .sum()
981    }
982
983    /// `Var(cᵀβ)` under this law: the mixture's own spread plus the tangent.
984    pub fn variance(&self) -> f64 {
985        let mean = self.mean();
986        let spread = self
987            .nodes
988            .iter()
989            .map(|(location, weight)| weight * (location - mean) * (location - mean))
990            .sum::<f64>();
991        spread + self.residual_variance
992    }
993}
994
995/// Build [`ConstrainedProjectionLaw`] for `cᵀβ` from the persisted geometry and
996/// the ambient covariance, using the SAME cubature the module's moments and
997/// intervals use.
998pub fn constrained_projection_law(
999    ambient_covariance: &Array2<f64>,
1000    geometry: &ConstrainedPosteriorGeometry,
1001    contrast: &Array1<f64>,
1002) -> Result<ConstrainedProjectionLaw, String> {
1003    let decomposition = decompose_projection(ambient_covariance, geometry, contrast)?;
1004    let Some(truncated) = decomposition.truncated else {
1005        return Ok(ConstrainedProjectionLaw {
1006            nodes: vec![(decomposition.ambient_mean, 1.0)],
1007            residual_variance: decomposition.ambient_variance,
1008        });
1009    };
1010    let nodes = converged_projection_nodes(
1011        &truncated.normal_center,
1012        &truncated.normal_covariance,
1013        &truncated.upper_limits,
1014        &truncated.projection_lift,
1015        decomposition.ambient_mean,
1016    )?;
1017    Ok(ConstrainedProjectionLaw {
1018        nodes: nodes
1019            .into_iter()
1020            .map(|node| (node.conditional_mean, node.weight))
1021            .collect(),
1022        residual_variance: truncated.residual_variance,
1023    })
1024}
1025
1026/// One point of the JOINT rule over an inequality-truncated Gaussian posterior:
1027/// a feasible constraint-normal coordinate and the tangent coordinates drawn
1028/// from the same low-discrepancy point.
1029#[derive(Clone, Debug)]
1030pub struct ConstrainedPosteriorJointPoint {
1031    /// `u = Aβ − b` on the retained rows. Inside the retained region by
1032    /// construction — the separation-of-variables map only ever produces points
1033    /// of `[0, upper]`, so a consumer integrating against these points puts
1034    /// exactly zero mass on coefficient vectors the fit excluded.
1035    pub normal_coordinates: Array1<f64>,
1036    /// Independent standard-normal coordinates for the tangent block, length
1037    /// `tangent_dimension`. The tangent of an inequality-truncated Gaussian is
1038    /// exactly Gaussian and exactly independent of `u`: conditioning on `Aβ`
1039    /// leaves `N(β_unc + G(u − E_untrunc[u]), Σ − GAΣ)` whatever the truncation
1040    /// does to `u`, which is what lets one rule carry both blocks.
1041    pub tangent: Array1<f64>,
1042    /// Normalized weight. The weights sum to one.
1043    pub weight: f64,
1044}
1045
1046/// A single low-discrepancy rule over the constraint-normal AND tangent
1047/// coordinates of an inequality-truncated Gaussian.
1048///
1049/// Why this exists (#2679). A consumer that integrates a nonlinear functional
1050/// of `β` over this posterior has, until now, had two options, and both are
1051/// wrong for a different reason:
1052///
1053/// * Integrate the moment-matched NORMAL. Its error is a FLOOR — the
1054///   pushforward of a cone-truncated joint is not normal for `q > 1`, so
1055///   matching two moments cannot be improved by any amount of extra work — and
1056///   it puts a measurable fraction of its mass outside the cone.
1057/// * Nest a Gaussian tensor rule inside every node of the truncated cubature.
1058///   That is exact, and it costs `nodes × outer × inner` evaluations per
1059///   evaluation point, which is not a production integration rule.
1060///
1061/// This is the third option: `points` points of ONE rule in dimension
1062/// `q + tangent_dimension`. The first `q` lattice coordinates run the same
1063/// separation-of-variables map, tilt and weighting the module's own cubature
1064/// uses — so the `u`-marginal is bit-identical to it at equal point counts —
1065/// and the remaining coordinates carry standard normals for the tangent. The
1066/// per-evaluation-point cost is `points`, with no factor of the cubature's node
1067/// count in it.
1068///
1069/// The price is real and is the caller's to gate: a lattice rule on the smooth
1070/// tangent block does not have the spectral accuracy of the Gauss-Hermite
1071/// tensor rule it replaces. A consumer must measure itself against a reference
1072/// built from the DENSITY rather than from this rule before using it.
1073pub fn constrained_posterior_joint_cubature(
1074    normal_center: &Array1<f64>,
1075    normal_covariance: &Array2<f64>,
1076    upper_limits: &[f64],
1077    tangent_dimension: usize,
1078    points: usize,
1079) -> Result<Vec<ConstrainedPosteriorJointPoint>, String> {
1080    let q = normal_center.len();
1081    if q == 0 {
1082        return Err("joint constrained cubature needs at least one constraint normal".to_string());
1083    }
1084    if normal_covariance.dim() != (q, q) || upper_limits.len() != q {
1085        return Err(format!(
1086            "joint constrained cubature geometry mismatch: centre={q}, covariance={:?}, \
1087             upper limits={}",
1088            normal_covariance.dim(),
1089            upper_limits.len()
1090        ));
1091    }
1092    if points == 0 {
1093        return Err("joint constrained cubature needs a positive point count".to_string());
1094    }
1095    if upper_limits.iter().any(|limit| !(*limit > 0.0)) {
1096        return Err(format!(
1097            "joint constrained cubature: every upper limit must sit strictly above its wall, \
1098             got {upper_limits:?}"
1099        ));
1100    }
1101    let factor = gam_linalg::triangular::cholesky_factor_in_place(
1102        normal_covariance.view(),
1103        gam_linalg::triangular::CholeskyGuard::FiniteStrict,
1104    )
1105    .ok_or_else(|| {
1106        "joint constrained cubature: the constraint-normal covariance is not numerically \
1107         positive definite"
1108            .to_string()
1109    })?;
1110    let generator = kronecker_generator(q + tangent_dimension);
1111    let tilt = minimax_tilt(normal_center, upper_limits, factor.view());
1112    let mut accumulator = JointCubatureAccumulator {
1113        points: Vec::with_capacity(points),
1114    };
1115    accumulate_orthant_nodes(
1116        &mut accumulator,
1117        normal_center,
1118        upper_limits,
1119        None,
1120        factor.view(),
1121        &generator,
1122        tilt.as_ref(),
1123        tangent_dimension,
1124        0,
1125        points,
1126    )?;
1127    accumulator.normalized()
1128}
1129
1130/// Sink that keeps whole joint points rather than accumulating their moments.
1131struct JointCubatureAccumulator {
1132    /// `weight` carries the UNNORMALIZED log weight until [`Self::normalized`]
1133    /// rescales it: the log scale spans hundreds of decades on a deeply pinned
1134    /// face, so no weight is exponentiated before the maximum is known.
1135    points: Vec<ConstrainedPosteriorJointPoint>,
1136}
1137
1138impl JointCubatureAccumulator {
1139    fn normalized(self) -> Result<Vec<ConstrainedPosteriorJointPoint>, String> {
1140        let max_log_weight = self
1141            .points
1142            .iter()
1143            .map(|point| point.weight)
1144            .fold(f64::NEG_INFINITY, f64::max);
1145        if !max_log_weight.is_finite() {
1146            return Err("joint constrained cubature accumulated no finite node weight".to_string());
1147        }
1148        let weight_sum = self
1149            .points
1150            .iter()
1151            .map(|point| (point.weight - max_log_weight).exp())
1152            .sum::<f64>();
1153        if !(weight_sum.is_finite() && weight_sum > 0.0) {
1154            return Err(format!(
1155                "joint constrained cubature has invalid normalized weight sum {weight_sum:?}"
1156            ));
1157        }
1158        let mut points = self.points;
1159        for point in points.iter_mut() {
1160            point.weight = (point.weight - max_log_weight).exp() / weight_sum;
1161        }
1162        Ok(points)
1163    }
1164}
1165
1166impl OrthantNodeSink for JointCubatureAccumulator {
1167    fn push(&mut self, log_weight: f64, point: &Array1<f64>) {
1168        self.push_joint(log_weight, point, &[]);
1169    }
1170
1171    fn push_joint(&mut self, log_weight: f64, point: &Array1<f64>, tangent: &[f64]) {
1172        self.points.push(ConstrainedPosteriorJointPoint {
1173            normal_coordinates: point.clone(),
1174            tangent: Array1::from_vec(tangent.to_vec()),
1175            weight: log_weight,
1176        });
1177    }
1178}
1179
1180/// Equal-tailed interval for one linear projection of an inequality-truncated
1181/// Gaussian posterior.
1182///
1183/// `ambient_covariance` is the pre-truncation covariance `Σ` in the active
1184/// coefficient frame and `contrast` defines the scalar `cᵀβ`.  The affine
1185/// shift of a saved coefficient gauge is deliberately not accepted here:
1186/// callers add that deterministic shift to both returned endpoints.
1187///
1188/// The interval comes from the quantiles of the convolution
1189/// `decompose_projection` produces, not from `posterior_mean ± z·posterior_sd`.
1190pub fn constrained_projection_equal_tailed_interval(
1191    ambient_covariance: &Array2<f64>,
1192    geometry: &ConstrainedPosteriorGeometry,
1193    contrast: &Array1<f64>,
1194    level: f64,
1195) -> Result<(f64, f64), String> {
1196    if !(level.is_finite() && level > 0.0 && level < 1.0) {
1197        return Err(format!(
1198            "constrained projection interval level must lie in (0, 1), got {level}"
1199        ));
1200    }
1201    let decomposition = decompose_projection(ambient_covariance, geometry, contrast)?;
1202    let ambient_mean = decomposition.ambient_mean;
1203    let ambient_variance = decomposition.ambient_variance;
1204    let alpha = 0.5 * (1.0 - level);
1205
1206    let Some(truncated) = decomposition.truncated else {
1207        let sd = ambient_variance.sqrt();
1208        if sd == 0.0 {
1209            return Ok((ambient_mean, ambient_mean));
1210        }
1211        let z = standard_normal_quantile(1.0 - alpha)
1212            .map_err(|error| format!("constrained projection normal quantile: {error}"))?;
1213        return Ok((ambient_mean - z * sd, ambient_mean + z * sd));
1214    };
1215
1216    let TruncatedProjection {
1217        posterior_mean,
1218        normal_center,
1219        normal_covariance,
1220        upper_limits,
1221        projection_lift,
1222        residual_variance,
1223    } = truncated;
1224    let q = normal_center.len();
1225    if q == 1 && residual_variance == 0.0 && projection_lift[0] != 0.0 {
1226        let scalar_quantile = |probability: f64| -> Result<f64, String> {
1227            let normal_probability = if projection_lift[0] > 0.0 {
1228                probability
1229            } else {
1230                1.0 - probability
1231            };
1232            let value = scalar_truncated_quantile(
1233                normal_center[0],
1234                normal_covariance[[0, 0]],
1235                upper_limits[0],
1236                normal_probability,
1237            )?;
1238            Ok(ambient_mean + projection_lift[0] * (value - normal_center[0]))
1239        };
1240        return Ok((scalar_quantile(alpha)?, scalar_quantile(1.0 - alpha)?));
1241    }
1242    let nodes = converged_projection_nodes(
1243        &normal_center,
1244        &normal_covariance,
1245        &upper_limits,
1246        &projection_lift,
1247        ambient_mean,
1248    )?;
1249    let lower = projection_quantile(
1250        &nodes,
1251        residual_variance,
1252        alpha,
1253        posterior_mean,
1254        ambient_variance.sqrt(),
1255    )?;
1256    let upper = projection_quantile(
1257        &nodes,
1258        residual_variance,
1259        1.0 - alpha,
1260        posterior_mean,
1261        ambient_variance.sqrt(),
1262    )?;
1263    Ok((lower, upper))
1264}
1265
1266/// Quantile of `N(mean, variance)` restricted to `[0, upper]`.
1267fn scalar_truncated_quantile(
1268    mean: f64,
1269    variance: f64,
1270    upper: f64,
1271    probability: f64,
1272) -> Result<f64, String> {
1273    if !(variance.is_finite() && variance > 0.0) {
1274        return Err(format!(
1275            "scalar truncated quantile needs positive finite variance, got {variance:?}"
1276        ));
1277    }
1278    if !(probability.is_finite() && probability > 0.0 && probability < 1.0) {
1279        return Err(format!(
1280            "scalar truncated quantile probability must lie in (0, 1), got {probability}"
1281        ));
1282    }
1283    if !(upper > 0.0) {
1284        return Err(format!(
1285            "scalar truncated quantile needs the upper limit above the wall, got {upper:?}"
1286        ));
1287    }
1288    let sd = variance.sqrt();
1289    let alpha = -mean / sd;
1290    if !upper.is_finite() {
1291        // P(Z > z | Z >= alpha) = (1-p) P(Z >= alpha). Work entirely in
1292        // log-survival space so a deeply pinned face never forms `1-Phi(alpha)`.
1293        let log_tail = (1.0 - probability).ln() + normal_logsf(alpha);
1294        let z = -standard_normal_quantile_from_log_cdf(log_tail)
1295            .map_err(|error| format!("scalar truncated quantile: {error}"))?;
1296        return Ok(mean + sd * z);
1297    }
1298    let beta = (upper - mean) / sd;
1299    // Same reflection as the moments: put the retained slab in the upper tail so
1300    // its mass is a difference of directly-evaluated tail probabilities. Under
1301    // the reflection the probability runs the other way.
1302    let reflect = alpha + beta < 0.0;
1303    let (low, high, probability) = if reflect {
1304        (-beta, -alpha, 1.0 - probability)
1305    } else {
1306        (alpha, beta, probability)
1307    };
1308    let log_tail_low = normal_logsf(low);
1309    let removed = normal_logsf(high) - log_tail_low;
1310    // `Φ̄(z) = Φ̄(low)·(1 − p(1 − e^removed))`, the inversion the cubature uses.
1311    let log_tail = log_tail_low + (-probability * -removed.exp_m1()).ln_1p();
1312    let z = -standard_normal_quantile_from_log_cdf(log_tail)
1313        .map_err(|error| format!("scalar truncated quantile: {error}"))?;
1314    let z = z.clamp(low, high);
1315    // Reflected, `z` standardizes `−X` about `−mean`, so `X = mean − sd·z`.
1316    Ok(if reflect {
1317        mean - sd * z
1318    } else {
1319        mean + sd * z
1320    })
1321}
1322
1323/// Build the truncated-posterior correction for a fit carrying linear
1324/// inequality constraints, or `None` when no constraint row is close enough to
1325/// the posterior centre to move the answer at double precision.
1326///
1327/// * `covariance` — `Σ`, the PRE-TRUNCATION posterior covariance on the same
1328///   coefficient frame as `constraints` and `unconstrained_center`. This is the
1329///   dispersion-scaled `φ·H⁻¹`: truncation is a statement about the posterior's
1330///   own spread, so it must be applied in the scaled metric, not to `H⁻¹`.
1331/// * `unconstrained_center` — `β_unc = β̂ − Σ·∇ℓ_p(β̂)`.
1332/// * `constraints` — `A β ≥ b`.
1333///
1334/// `None` is returned when every row's standardized slack exceeds the
1335/// resolution horizon, which includes the case of a fit whose constraints are
1336/// all inactive. Callers must then report `Σ` unchanged, bit for bit.
1337pub fn constrained_posterior_correction_from_covariance(
1338    covariance: &Array2<f64>,
1339    unconstrained_center: &Array1<f64>,
1340    constraints: &LinearInequalityConstraints,
1341) -> Result<Option<ConstrainedPosteriorCorrection>, String> {
1342    let p = covariance.nrows();
1343    if covariance.ncols() != p {
1344        return Err(format!(
1345            "constrained posterior correction needs a square covariance, got {}x{}",
1346            covariance.nrows(),
1347            covariance.ncols()
1348        ));
1349    }
1350    if constraints.a.ncols() != p {
1351        return Err(format!(
1352            "constrained posterior correction: covariance is {p}x{p} but the constraint \
1353             system has {} columns",
1354            constraints.a.ncols()
1355        ));
1356    }
1357    let sigma_times_at = covariance.dot(&constraints.a.t());
1358    constrained_posterior_correction(sigma_times_at.view(), unconstrained_center, constraints)
1359}
1360
1361/// Same correction for a caller that never materializes `Σ`.
1362///
1363/// Everything the decomposition needs from the covariance is the `p × m` block
1364/// `Σ Aᵀ` — column `j` is `Σ a_j`, `W_ij = a_iᵀ(Σ a_j)`, and the lift is
1365/// `(Σ Aᵀ)W⁻¹` — so a factorized inference path supplies `m` solves instead of
1366/// a `p × p` inverse.
1367pub fn constrained_posterior_correction(
1368    sigma_times_constraint_transpose: ArrayView2<'_, f64>,
1369    unconstrained_center: &Array1<f64>,
1370    constraints: &LinearInequalityConstraints,
1371) -> Result<Option<ConstrainedPosteriorCorrection>, String> {
1372    let p = sigma_times_constraint_transpose.nrows();
1373    if sigma_times_constraint_transpose.ncols() != constraints.a.nrows() {
1374        return Err(format!(
1375            "constrained posterior correction: the constraint system has {} rows but \
1376             Sigma·Aᵀ has {} columns",
1377            constraints.a.nrows(),
1378            sigma_times_constraint_transpose.ncols()
1379        ));
1380    }
1381    if unconstrained_center.len() != p {
1382        return Err(format!(
1383            "constrained posterior correction: Sigma·Aᵀ has {p} rows but the centre has \
1384             length {}",
1385            unconstrained_center.len()
1386        ));
1387    }
1388    if constraints.a.ncols() != p {
1389        return Err(format!(
1390            "constrained posterior correction: Sigma·Aᵀ has {p} rows but the constraint \
1391             system has {} columns",
1392            constraints.a.ncols()
1393        ));
1394    }
1395
1396    let candidates = constraint_face_candidates(
1397        sigma_times_constraint_transpose,
1398        unconstrained_center,
1399        constraints,
1400    )?;
1401    if candidates.is_empty() {
1402        return Ok(None);
1403    }
1404
1405    // The retention floor is the accuracy the retained face must deliver, and
1406    // it asks for exactly the accuracy this module reports its moments to. It
1407    // is necessary and not sufficient (see [`assemble_retained_face`]), so when
1408    // the assembled face's lift misses the identity that defines it, a row has
1409    // to come out and the face has to be rebuilt.
1410    //
1411    // THE LADDER IS INDEXED BY THE FACE, NOT BY A FLOOR (#2714). Two rules have
1412    // been tried here and both failed for the same underlying reason — the
1413    // quantity being searched over was a real number that only stands in for
1414    // the face:
1415    //
1416    // 1. `demanded_accuracy /= departure/tolerance` read its step size off the
1417    //    PER-ROW error model `departure ≈ ε·diagonal/pivot`, which holds only
1418    //    when the face is no worse conditioned than its worst row — i.e. never,
1419    //    in the only case the ladder runs in. One pass could carry the floor
1420    //    from `1e-3` past every intermediate face to below `f64::EPSILON`.
1421    //
1422    // 2. Stepping to `max_r d_r`, with `d_r = (k+1)·ε·diagonal_r/pivot_r` the
1423    //    floor at which accepted row `r` drops, is exact in real arithmetic —
1424    //    the test becomes `pivot > pivot` — and is NOT exact in floating point.
1425    //    `d_r` is a rounded quotient and the retention test recomputes
1426    //    `(k+1)·ε·diagonal_r/d_r`, a second rounded quotient; that round trip
1427    //    lands strictly below `pivot_r` for about 5% of `(k, diagonal, pivot)`
1428    //    triples, so the row the step was aimed at is retained, the rebuilt
1429    //    face is bit-identical, `max_r d_r` recomputes to the value it already
1430    //    has, and the walk stops descending. Inverting a floating-point
1431    //    comparison to name the next face cannot be made reliable by rounding
1432    //    the quotient the other way either: the retention test is the
1433    //    definition of the face, and only the test can decide it.
1434    //
1435    // So the walk carries the face itself. A rejected face names the accepted
1436    // row whose `pivot/diagonal` is smallest — the most nearly dependent one,
1437    // which is both the row rule 2 was trying to reach and the row that
1438    // contributes least constraint information — and that row is EXCLUDED by
1439    // index, together with any opposite face folded into it, before the face is
1440    // rebuilt at the unchanged floor. What leaves is a DIRECTION, not a row:
1441    // promoting an excluded row's anti-parallel partner in its place would
1442    // report the slacker of two walls as a one-sided bound.
1443    //
1444    // Termination is now structural rather than numerical. The excluded set
1445    // gains at least one row per pass, never re-adds one (an excluded row is
1446    // skipped before it can be accepted or folded), and is a subset of the
1447    // candidates — so there are at most `candidates.len()` passes; the first
1448    // unexcluded row always clears the floor (`accepted = 0` makes
1449    // `pivot = diagonal` and the floor `ε·diagonal/1e-3`), so a nonempty
1450    // unexcluded set always yields a nonempty face; and the last such face is a
1451    // single row, whose `1×1` lift is exact and whose departure gate cannot
1452    // fail. No float comparison is inverted anywhere on that argument.
1453    //
1454    // Excluding at the UNCHANGED floor is also strictly less lossy than rule 2,
1455    // which tightened the floor for every surviving row as a side effect of
1456    // dropping one. Every face rule 2 could reach is still reachable here — run
1457    // the walk excluding precisely the rows that floor rejected, and each
1458    // surviving row clears the looser floor a fortiori — while faces that only
1459    // a tighter floor would have destroyed are kept.
1460    let demanded_accuracy = ORTHANT_MOMENT_RELATIVE_TOLERANCE;
1461    let mut first_pass = true;
1462    let mut faces_tried = 0usize;
1463    let mut ladder: Vec<LadderRung> = Vec::new();
1464    let mut excluded: Vec<usize> = Vec::new();
1465    let mut last_refused: Option<RefusedFace> = None;
1466    while excluded.len() <= candidates.len() {
1467        let Some(face) = assemble_retained_face(
1468            &candidates,
1469            demanded_accuracy,
1470            constraints,
1471            unconstrained_center,
1472            &excluded,
1473        )?
1474        else {
1475            if first_pass {
1476                return Ok(None);
1477            }
1478            report_terminal_refusal(last_refused.as_ref(), &ladder, excluded.len());
1479            return Err(format!(
1480                "no constraint face survives the accuracy its own lift must deliver: excluding \
1481                 {} of {} candidate row(s) at the retention floor {demanded_accuracy:.3e} left \
1482                 no retained row after {faces_tried} face(s){}",
1483                excluded.len(),
1484                candidates.len(),
1485                render_ladder(&ladder, candidates.len())
1486            ));
1487        };
1488        first_pass = false;
1489        faces_tried += 1;
1490        // `G = Σ Aᵀ W⁻¹` solved through the factor built above, one column of
1491        // `Gᵀ` at a time: `W Gᵀ_col = (Σ Aᵀ)ᵀ_col`.
1492        let lift = cholesky_solve_right(&face.factor, &face.sigma_at)?;
1493        let departure = lift_identity_departure(&lift, constraints, &face.rows)?;
1494        ladder.push(LadderRung {
1495            excluded: excluded.len(),
1496            retained: face.rows.len(),
1497            departure,
1498        });
1499        if departure > ORTHANT_MOMENT_RELATIVE_TOLERANCE {
1500            last_refused = Some(RefusedFace {
1501                rows: face.rows.clone(),
1502                w: face.w.clone(),
1503            });
1504            if face.rows.len() == 1 {
1505                report_terminal_refusal(last_refused.as_ref(), &ladder, excluded.len());
1506                return Err(format!(
1507                    "a single retained constraint row still misses the identity that defines \
1508                     its lift: max|A G - I| = {departure:.6e} exceeds \
1509                     {ORTHANT_MOMENT_RELATIVE_TOLERANCE:.1e}, which one row cannot be \
1510                     ill-conditioned enough to cause{}",
1511                    render_ladder(&ladder, candidates.len())
1512                ));
1513            }
1514            // Drop the most nearly dependent accepted DIRECTION — that row and
1515            // any opposite face folded into it — and rebuild. A face with two
1516            // or more rows always names one, so the empty case is a statement
1517            // about `assemble_retained_face`, not a fallback.
1518            if face.least_independent_direction.is_empty() {
1519                report_terminal_refusal(last_refused.as_ref(), &ladder, excluded.len());
1520                return Err(format!(
1521                    "the constraint face misses the identity that defines its lift \
1522                     (max|A G - I| = {departure:.6e} against \
1523                     {ORTHANT_MOMENT_RELATIVE_TOLERANCE:.1e}) over {} retained row(s), and \
1524                     names no least-independent direction to drop, after {faces_tried} \
1525                     face(s){}",
1526                    face.rows.len(),
1527                    render_ladder(&ladder, candidates.len())
1528                ));
1529            }
1530            excluded.extend_from_slice(&face.least_independent_direction);
1531            continue;
1532        }
1533
1534        let q = face.rows.len();
1535        let mut normal_center = Array1::<f64>::zeros(q);
1536        for (position, &row_index) in face.rows.iter().enumerate() {
1537            normal_center[position] =
1538                constraints.a.row(row_index).dot(unconstrained_center) - constraints.b[row_index];
1539        }
1540
1541        let (normal_mean, normal_covariance) = box_truncated_moments(
1542            &normal_center,
1543            &face.upper,
1544            &face.w,
1545            face.factor.view(),
1546        )?;
1547
1548        let mut removed = &face.w - &normal_covariance;
1549        gam_linalg::matrix::symmetrize_in_place(&mut removed);
1550        certify_removed_variance(&removed, &face.w)?;
1551
1552        if !excluded.is_empty() {
1553            // The walk ran, so the reported truncation is carried by a SUBSET
1554            // of the rows the user asked for. That is a fact about the answer,
1555            // not about the solve — the dropped rows are within `O(θ)` of the
1556            // span of the retained ones, but "within `O(θ)`" is a claim the
1557            // reader is entitled to see stated on their own fit.
1558            log::info!(
1559                "[CONSTRAINED-FACE] {} of {} candidate constraint row(s) retained after \
1560                 dropping {} nearly dependent direction(s) over {faces_tried} face(s); the \
1561                 retained lift satisfies its identity to {departure:.3e}",
1562                face.rows.len(),
1563                candidates.len(),
1564                excluded.len()
1565            );
1566        }
1567
1568        return Ok(Some(ConstrainedPosteriorCorrection {
1569            lift,
1570            removed_normal_variance: removed,
1571            normal_mean_shift: normal_mean - normal_center,
1572            rows: face.rows,
1573            normal_upper_limits: face.upper,
1574        }));
1575    }
1576    report_terminal_refusal(last_refused.as_ref(), &ladder, excluded.len());
1577    Err(format!(
1578        "the constraint-normal lift never reached the accuracy it is certified to: \
1579         {faces_tried} constraint face(s) were tried and every candidate row was excluded. \
1580         The walk drops one accepted direction per pass and a single-row face's lift is \
1581         exact, so this says the single-row face was never reached — which it must be.{}",
1582        render_ladder(&ladder, candidates.len())
1583    ))
1584}
1585
1586/// Print the evidence a terminal refusal of the retention walk rests on: the
1587/// last face it refused, at full precision, and the trail that got there.
1588fn report_terminal_refusal(
1589    last_refused: Option<&RefusedFace>,
1590    ladder: &[LadderRung],
1591    excluded: usize,
1592) {
1593    let Some(face) = last_refused else {
1594        return;
1595    };
1596    let departure = ladder.last().map_or(f64::NAN, |rung| rung.departure);
1597    log_refused_face(face, departure, excluded);
1598}
1599
1600/// One rung of the retention walk: how many rows had been excluded when it ran,
1601/// the face that produced, and how far that face's lift missed the identity
1602/// that defines it.
1603///
1604/// Recorded because every terminal refusal in this walk is a statement about a
1605/// TRAJECTORY — "the walk never reached a face whose lift is accurate" — and a
1606/// message that reports only its last rung cannot be told apart from one that
1607/// took a single wrong step. #2714's witness reached the terminal message after
1608/// exactly one rung under the pre-`92332c45c` rule, and the message said
1609/// nothing that would have distinguished that from forty.
1610struct LadderRung {
1611    excluded: usize,
1612    retained: usize,
1613    departure: f64,
1614}
1615
1616/// Render the ladder trail for a terminal message, newest rung last.
1617///
1618/// The walk can take one pass per candidate row, and a message carrying two
1619/// hundred rungs is one nobody reads, so the ends are printed and the middle is
1620/// counted. The ends are what carry the diagnosis: the first rung is the face
1621/// the retention floor produced on its own and the last is the one the walk
1622/// died on. The middle is not summarizable beyond its count — the RETAINED
1623/// count is not monotone, because excluding a nearly dependent row raises the
1624/// pivots of the rows after it and can admit ones the floor had refused — but
1625/// the `excluded` column is, and it is the walk's actual index.
1626fn render_ladder(ladder: &[LadderRung], candidates: usize) -> String {
1627    const SHOWN_AT_EACH_END: usize = 4;
1628    let mut rendered = format!(" [walk over {candidates} candidate row(s):");
1629    let render_rung = |rung: &LadderRung, into: &mut String| {
1630        into.push_str(&format!(
1631            " (excluded={} retained={} departure={:.3e})",
1632            rung.excluded, rung.retained, rung.departure
1633        ));
1634    };
1635    if ladder.len() <= 2 * SHOWN_AT_EACH_END + 1 {
1636        for rung in ladder {
1637            render_rung(rung, &mut rendered);
1638        }
1639    } else {
1640        for rung in &ladder[..SHOWN_AT_EACH_END] {
1641            render_rung(rung, &mut rendered);
1642        }
1643        rendered.push_str(&format!(
1644            " ... {} further rung(s) ...",
1645            ladder.len() - 2 * SHOWN_AT_EACH_END
1646        ));
1647        for rung in &ladder[ladder.len() - SHOWN_AT_EACH_END..] {
1648            render_rung(rung, &mut rendered);
1649        }
1650    }
1651    rendered.push(']');
1652    rendered
1653}
1654
1655/// Emit a refused face itself, at full precision, on the diagnostic channel.
1656///
1657/// Called ONLY from the walk's terminal error paths, not per rung. Dropping
1658/// rows is the walk working, not the walk failing — a `q × q` matrix printed on
1659/// every rung would be a megabyte of warnings for a correction that then
1660/// succeeds — while a terminal refusal is unreproducible without the face,
1661/// because the walk's decisions are a function of `W` alone and the fit that
1662/// produced `W` costs three quarters of an hour to reach this line on the
1663/// #2714 witness. Printing it there turns that refusal into a unit fixture.
1664fn log_refused_face(face: &RefusedFace, departure: f64, excluded: usize) {
1665    if !log::log_enabled!(log::Level::Warn) {
1666        return;
1667    }
1668    let q = face.rows.len();
1669    let mut rendered = String::new();
1670    for i in 0..q {
1671        for j in 0..q {
1672            rendered.push_str(&format!("{:.17e},", face.w[[i, j]]));
1673        }
1674    }
1675    log::warn!(
1676        "[CONSTRAINED-FACE] refused excluded={excluded} departure={departure:.6e} \
1677         q={q} rows={:?} w=[{rendered}]",
1678        face.rows
1679    );
1680}
1681
1682/// The last face the walk refused, kept so a terminal error can print it.
1683///
1684/// Only `rows` and `W` are retained: they are what makes the refusal
1685/// reproducible, and keeping the whole [`RetainedFace`] alive across a pass
1686/// would hold its `p × q` block and its factor for a walk that may run once per
1687/// candidate row.
1688struct RefusedFace {
1689    rows: Vec<usize>,
1690    w: Array2<f64>,
1691}
1692
1693/// The constraint rows that can still move a moment, in the order the rank
1694/// filter walks them: `(row index, standardized slack, Σ a_row)`.
1695///
1696/// A row whose remaining feasible mass `Φ̄(s)` is below `f64::EPSILON` cannot
1697/// change any moment at double precision, so the horizon is read off the machine
1698/// epsilon rather than chosen.
1699///
1700/// The order is by standardized slack, and that is a STATISTICAL choice which
1701/// stays: two near-parallel rows with different offsets are not the same
1702/// constraint, and the tighter one dominates, so retaining the slacker of a pair
1703/// would quietly relax the constraint by a multiple of its own standard
1704/// deviation. Ordering by pivot magnitude instead would reveal the face's
1705/// conditioning but pay exactly that cost — which is why
1706/// [`assemble_retained_face`]'s per-row floor is necessary and not sufficient,
1707/// and why the caller has to check the assembled face.
1708///
1709/// Named and extracted so a test can build the same candidate list the
1710/// production walk builds, rather than a second implementation of it.
1711fn constraint_face_candidates(
1712    sigma_times_constraint_transpose: ArrayView2<'_, f64>,
1713    unconstrained_center: &Array1<f64>,
1714    constraints: &LinearInequalityConstraints,
1715) -> Result<Vec<(usize, f64, Array1<f64>)>, String> {
1716    let slack_horizon = -standard_normal_quantile(f64::EPSILON)
1717        .map_err(|error| format!("resolution horizon for the constraint slack: {error}"))?;
1718    let mut candidates: Vec<(usize, f64, Array1<f64>)> = Vec::new();
1719    for row_index in 0..constraints.a.nrows() {
1720        let row = constraints.a.row(row_index).to_owned();
1721        let sigma_row = sigma_times_constraint_transpose
1722            .column(row_index)
1723            .to_owned();
1724        let variance = row.dot(&sigma_row);
1725        if !(variance.is_finite() && variance > 0.0) {
1726            // The constraint normal has no posterior spread at all: the fit
1727            // cannot move along it, so the truncation removes nothing.
1728            continue;
1729        }
1730        let slack = (row.dot(unconstrained_center) - constraints.b[row_index]) / variance.sqrt();
1731        if !slack.is_finite() {
1732            return Err(format!(
1733                "constraint row {row_index} produced a non-finite standardized slack"
1734            ));
1735        }
1736        if slack < slack_horizon {
1737            candidates.push((row_index, slack, sigma_row));
1738        }
1739    }
1740    candidates.sort_by(|left, right| {
1741        left.1
1742            .partial_cmp(&right.1)
1743            .unwrap_or(std::cmp::Ordering::Equal)
1744            .then_with(|| left.0.cmp(&right.0))
1745    });
1746    Ok(candidates)
1747}
1748
1749/// One assembled constraint face: the retained rows in acceptance order, the
1750/// lower Cholesky factor of `W = A Σ Aᵀ` built while retaining them, `W` itself,
1751/// and the `Σ Aᵀ` block restricted to those rows.
1752struct RetainedFace {
1753    rows: Vec<usize>,
1754    factor: Array2<f64>,
1755    w: Array2<f64>,
1756    sigma_at: Array2<f64>,
1757    /// Upper limit per retained coordinate, `f64::INFINITY` for a half-line.
1758    upper: Vec<f64>,
1759    /// The retained row whose `pivot/diagonal` is smallest, together with every
1760    /// refused row whose far wall was folded into it. Empty for an empty face.
1761    ///
1762    /// `pivot/diagonal` is the squared sine of the angle between a row's
1763    /// constraint normal and the span of the rows accepted before it, in the
1764    /// `Σ` metric, so the minimizer is the retained row that is most nearly
1765    /// dependent on the others — the one carrying the least constraint
1766    /// information and the most of the face's ill-conditioning. It is what the
1767    /// retention walk drops when the assembled face's lift misses its own
1768    /// identity, and it is reported BY INDEX so that decision is a set
1769    /// operation rather than the inversion of a floating-point comparison
1770    /// (#2714).
1771    ///
1772    /// The FOLDED rows travel with it because the walk drops a DIRECTION, not a
1773    /// row. A row anti-parallel to an accepted one is refused as a direction and
1774    /// keeps its wall as that row's upper limit (see
1775    /// [`record_opposed_face_limit`]); if the accepted row is then dropped for
1776    /// being nearly dependent, its opposite face is the same nearly dependent
1777    /// direction and is no more liftable. Leaving it behind would let the next
1778    /// pass accept it in the dropped row's place — with the SLACKER of the two
1779    /// walls, since the walk is ordered by slack, and with nothing to fold its
1780    /// own far wall into — turning a two-sided bound into a one-sided one on
1781    /// the wrong side.
1782    ///
1783    /// The first accepted row has an empty forward-substitution column, hence
1784    /// `pivot == diagonal` and a ratio of exactly `1`, which is the maximum the
1785    /// ratio can take; combined with the walk's tie-break toward the later (and
1786    /// therefore slacker) row, that makes the tightest wall the one row a face
1787    /// of two or more can never drop.
1788    least_independent_direction: Vec<usize>,
1789}
1790
1791/// Greedy pivoted-Cholesky rank filter on `W = A Σ Aᵀ`, walking the candidates
1792/// in their slack order and skipping any whose constraint-row index appears in
1793/// `excluded`.
1794///
1795/// The factor is built incrementally here and handed back, so the face is
1796/// factorized exactly once: a second factorization of the same `W` under a
1797/// different guard would let one matrix be judged by two standards, and near the
1798/// retention floor those two standards disagree.
1799///
1800/// `excluded` is the caller's record of the rows its previous faces already
1801/// judged too nearly dependent to lift accurately. It is a plain index list
1802/// rather than a set because it is walked once per candidate and never holds
1803/// more entries than the face had rows.
1804fn assemble_retained_face(
1805    candidates: &[(usize, f64, Array1<f64>)],
1806    demanded_accuracy: f64,
1807    constraints: &LinearInequalityConstraints,
1808    unconstrained_center: &Array1<f64>,
1809    excluded: &[usize],
1810) -> Result<Option<RetainedFace>, String> {
1811    let columns = constraints.a.ncols();
1812    // Each of `cross[k]`, `W_kk` and `diagonal` is one length-`p` inner product
1813    // of a constraint row against a `Σ` column, so each carries the standard
1814    // `γ_p ≈ p·ε` dot-product rounding; the anti-parallel test below compares
1815    // two products of such quantities, which propagates to about `4(p+1)ε`.
1816    // Nothing here is fitted to a fixture: it is the resolution at which "the
1817    // same direction, reversed" stops being decidable in double precision.
1818    let antiparallel_tolerance = 4.0 * (columns as f64 + 1.0) * f64::EPSILON;
1819    let mut rows: Vec<usize> = Vec::new();
1820    let mut least_independent: Option<(usize, f64)> = None;
1821    let mut sigma_a_columns: Vec<Array1<f64>> = Vec::new();
1822    let mut upper: Vec<f64> = Vec::new();
1823    // Per accepted position, the refused rows whose far wall was folded into
1824    // it. Parallel to `rows` and `upper`.
1825    let mut folded: Vec<Vec<usize>> = Vec::new();
1826    let mut w_accepted = Array2::<f64>::zeros((0, 0));
1827    let mut factor = Array2::<f64>::zeros((0, 0));
1828    for (row_index, _, sigma_row) in candidates {
1829        if excluded.contains(row_index) {
1830            continue;
1831        }
1832        let row = constraints.a.row(*row_index);
1833        let accepted = rows.len();
1834        let diagonal = row.dot(sigma_row);
1835        let mut cross = Array1::<f64>::zeros(accepted);
1836        for (position, column) in sigma_a_columns.iter().enumerate() {
1837            cross[position] = row.dot(column);
1838        }
1839        // Forward-substitute the new column through the accepted factor.
1840        let mut new_column = Array1::<f64>::zeros(accepted);
1841        for i in 0..accepted {
1842            let mut sum = cross[i];
1843            for k in 0..i {
1844                sum -= factor[[i, k]] * new_column[k];
1845            }
1846            new_column[i] = sum / factor[[i, i]];
1847        }
1848        let pivot = diagonal - new_column.dot(&new_column);
1849        // `pivot / diagonal` is the squared sine of the angle between this row's
1850        // constraint normal and the span of the rows accepted before it, in the
1851        // `Σ` metric. The lift `G = Σ Aᵀ W⁻¹` is solved through this same factor,
1852        // so its relative error grows like `ε · diagonal / pivot`. A floor at the
1853        // bare DETECTABILITY limit — `pivot ≈ ε · diagonal`, i.e. "reject only a
1854        // row that is dependent to the last bit" — therefore retains rows whose
1855        // lift carries no correct digits: measured 1.3e-2 relative error against
1856        // an exact rational reference at `pivot = 2 ε · diagonal`.
1857        //
1858        // So the floor is the one that keeps the retained face's own numerical
1859        // error under the accuracy demanded of it, and dropping instead costs
1860        // `O(θ)` with `θ` the angle between the two normals — below `5e-7`
1861        // radians at the first pass's floor — so a dropped row imposes no
1862        // constraint the retained one does not already impose.
1863        //
1864        // This is necessary and NOT sufficient, which is why the caller checks
1865        // the assembled face and excludes its least independent row when it
1866        // falls short: `min pivot / diagonal` is the smallest pivot of the
1867        // correlation matrix and bounds its smallest eigenvalue only when the
1868        // elimination is ordered by pivot magnitude. This walk is ordered by
1869        // slack, so every row can clear the floor while the face as a whole
1870        // does not.
1871        let rank_floor = (accepted + 1) as f64 * f64::EPSILON * diagonal / demanded_accuracy;
1872        if !(pivot.is_finite() && pivot > rank_floor) {
1873            // Redundant AS A DIRECTION. That is not the same as redundant as a
1874            // CONSTRAINT, and the two come apart exactly at an anti-parallel
1875            // row: `l ≤ β_j ≤ u` arrives as `e_jᵀβ ≥ l` and `−e_jᵀβ ≥ −u`, and
1876            // the second adds no constraint-normal direction while halving the
1877            // support. Dropping it reports a one-sided posterior for a
1878            // two-sided bound (#2523).
1879            //
1880            // A row PARALLEL to an accepted one is genuinely implied, and the
1881            // slack ordering is what makes that true rather than hoped: rows are
1882            // walked by ascending standardized slack and `sd` scales with the
1883            // row, so the accepted row's `slack/sd` is the smaller, which is
1884            // exactly the statement that its wall is the binding one. Those
1885            // still drop, unchanged.
1886            if let Some(position) = record_opposed_face_limit(
1887                *row_index,
1888                &cross,
1889                diagonal,
1890                &AcceptedFace {
1891                    w_accepted: &w_accepted,
1892                    rows: &rows,
1893                    constraints,
1894                    unconstrained_center,
1895                    antiparallel_tolerance,
1896                },
1897                &mut upper,
1898            )? {
1899                folded[position].push(*row_index);
1900            }
1901            continue;
1902        }
1903        let mut grown = Array2::<f64>::zeros((accepted + 1, accepted + 1));
1904        grown
1905            .slice_mut(ndarray::s![..accepted, ..accepted])
1906            .assign(&factor);
1907        for i in 0..accepted {
1908            grown[[accepted, i]] = new_column[i];
1909        }
1910        grown[[accepted, accepted]] = pivot.sqrt();
1911        factor = grown;
1912
1913        let mut grown_w = Array2::<f64>::zeros((accepted + 1, accepted + 1));
1914        grown_w
1915            .slice_mut(ndarray::s![..accepted, ..accepted])
1916            .assign(&w_accepted);
1917        for i in 0..accepted {
1918            grown_w[[accepted, i]] = cross[i];
1919            grown_w[[i, accepted]] = cross[i];
1920        }
1921        grown_w[[accepted, accepted]] = diagonal;
1922        w_accepted = grown_w;
1923
1924        rows.push(*row_index);
1925        sigma_a_columns.push(sigma_row.clone());
1926        upper.push(f64::INFINITY);
1927        folded.push(Vec::new());
1928        // The independence ratio of the row just accepted. `diagonal > 0` is
1929        // guaranteed by `constraint_face_candidates`, and `pivot > rank_floor > 0`
1930        // by the branch above, so the ratio is a finite positive number and the
1931        // comparison never sees a NaN.
1932        //
1933        // `<=` keeps the LAST minimizer, and the rows arrive in ascending slack
1934        // order, so a tie is broken toward the slacker row. That matters at the
1935        // one tie that is structural rather than accidental: the first accepted
1936        // row has an empty `new_column`, hence `pivot == diagonal` and a ratio
1937        // of exactly `1`, so on a face whose rows are all mutually orthogonal
1938        // in the `Σ` metric every ratio is `1` and a `<` rule would drop the
1939        // TIGHTEST wall.
1940        let independence = pivot / diagonal;
1941        if least_independent.is_none_or(|(_, best)| independence <= best) {
1942            least_independent = Some((rows.len() - 1, independence));
1943        }
1944    }
1945    if rows.is_empty() {
1946        return Ok(None);
1947    }
1948
1949    let q = rows.len();
1950    let p = sigma_a_columns[0].len();
1951    let mut sigma_at = Array2::<f64>::zeros((p, q));
1952    for (position, column) in sigma_a_columns.iter().enumerate() {
1953        sigma_at.column_mut(position).assign(column);
1954    }
1955    let least_independent_direction = match least_independent {
1956        Some((position, _)) => {
1957            let mut direction = vec![rows[position]];
1958            direction.extend_from_slice(&folded[position]);
1959            direction
1960        }
1961        None => Vec::new(),
1962    };
1963    Ok(Some(RetainedFace {
1964        rows,
1965        factor,
1966        w: w_accepted,
1967        sigma_at,
1968        upper,
1969        least_independent_direction,
1970    }))
1971}
1972
1973/// The accepted face an anti-parallel test reads: the retained rows with their
1974/// `W` block, the constraint system those rows index into, the centre the walls
1975/// are measured from, and the correlation at which two normals count as opposed.
1976///
1977/// These five never vary independently at the call site — they all describe one
1978/// accepted face — so they travel as one borrow rather than as five parameters
1979/// whose covariance the signature does not state.
1980struct AcceptedFace<'a> {
1981    w_accepted: &'a Array2<f64>,
1982    rows: &'a [usize],
1983    constraints: &'a LinearInequalityConstraints,
1984    unconstrained_center: &'a Array1<f64>,
1985    antiparallel_tolerance: f64,
1986}
1987
1988/// Keep the far wall of a two-sided bound that the rank filter has just refused
1989/// as a direction.
1990///
1991/// The refused row `a_r` carries no constraint-normal direction beyond the
1992/// accepted ones. When it is the OPPOSITE face of one of them — `a_r = −γ a_k`
1993/// for some `γ > 0`, up to a remainder with no posterior variance — it still
1994/// bounds that coordinate, from above:
1995///
1996/// ```text
1997/// a_rᵀβ ≥ b_r   ⟺   a_kᵀβ ≤ (ν − b_r)/γ   ⟺   u_k ≤ δ/γ,
1998/// ```
1999///
2000/// with `u_k = a_kᵀβ − b_k`, `ν = (a_r + γ a_k)ᵀβ` (almost surely constant,
2001/// precisely because its posterior variance is the pivot that just failed) and
2002/// `δ = (a_rᵀβ_unc − b_r) + γ(a_kᵀβ_unc − b_k)`.
2003///
2004/// The test is run in the `Σ` metric on quantities the filter has already
2005/// formed: `a_r = −γ a_k` makes the correlation `cross_k/√(W_kk·diagonal)`
2006/// exactly `−1`, and `γ = −cross_k/W_kk`. Running it in that metric rather than
2007/// on the raw rows is not a convenience — two rows that differ by a direction
2008/// with no posterior spread impose the same constraint on this posterior, and
2009/// the `Σ` metric is what sees that.
2010///
2011/// Rows that are refused for any other reason are left dropped, which is what
2012/// they were before. That is a narrower repair than "every dependent row", and
2013/// deliberately so: a row depending on two or more accepted normals at once cuts
2014/// the face along a diagonal, which no per-coordinate limit can represent.
2015///
2016/// Returns the accepted POSITION the wall was folded into, so the caller can
2017/// keep the pair together: the folded row is the opposite face of that accepted
2018/// row's direction, and if the retention walk later drops the direction the far
2019/// wall has to go with it rather than be promoted in its place (#2714).
2020fn record_opposed_face_limit(
2021    row_index: usize,
2022    cross: &Array1<f64>,
2023    diagonal: f64,
2024    face: &AcceptedFace<'_>,
2025    upper: &mut [f64],
2026) -> Result<Option<usize>, String> {
2027    let mut opposed: Option<(usize, f64, f64)> = None;
2028    for position in 0..face.rows.len() {
2029        let w_kk = face.w_accepted[[position, position]];
2030        let scale = (w_kk * diagonal).sqrt();
2031        if !(scale.is_finite() && scale > 0.0) {
2032            continue;
2033        }
2034        let correlation = cross[position] / scale;
2035        if correlation + 1.0 > face.antiparallel_tolerance {
2036            continue;
2037        }
2038        let gamma = -cross[position] / w_kk;
2039        if !(gamma.is_finite() && gamma > 0.0) {
2040            continue;
2041        }
2042        // Two accepted rows cannot both be anti-parallel to this one without
2043        // being parallel to each other, which the filter already refused; take
2044        // the most opposed and let the identity gate catch a face that is not.
2045        if opposed.is_none_or(|(_, best, _)| correlation < best) {
2046            opposed = Some((position, correlation, gamma));
2047        }
2048    }
2049    let Some((position, _, gamma)) = opposed else {
2050        return Ok(None);
2051    };
2052    let accepted_row = face.rows[position];
2053    let delta = (face.constraints.a.row(row_index).dot(face.unconstrained_center)
2054        - face.constraints.b[row_index])
2055        + gamma
2056            * (face.constraints.a.row(accepted_row).dot(face.unconstrained_center)
2057                - face.constraints.b[accepted_row]);
2058    let limit = delta / gamma;
2059    if !(limit.is_finite() && limit > 0.0) {
2060        return Err(format!(
2061            "constraint rows {accepted_row} and {row_index} bound the same coefficient \
2062             direction from opposite sides with no width between them (upper limit \
2063             {limit:.6e} above the lower wall): the retained region is empty or a single \
2064             point, which is an equality constraint and not a posterior this module can \
2065             report moments for"
2066        ));
2067    }
2068    if limit < upper[position] {
2069        upper[position] = limit;
2070    }
2071    Ok(Some(position))
2072}
2073
2074/// `max |A G - I|` over the retained rows.
2075///
2076/// `G = Σ Aᵀ W⁻¹` satisfies `A G = I` on those rows EXACTLY, because
2077/// `A (Σ Aᵀ) = W` by construction of `W`. The departure from that identity is
2078/// therefore not a modelling approximation: it is precisely the accuracy the
2079/// retained face's conditioning destroyed, measured on the object that is
2080/// actually used rather than on a proxy for it. Across a sweep of near-parallel
2081/// constraint normals it tracked the true error in `G` — against an exact
2082/// rational reference — to three significant digits at every angle.
2083fn lift_identity_departure(
2084    lift: &Array2<f64>,
2085    constraints: &LinearInequalityConstraints,
2086    rows: &[usize],
2087) -> Result<f64, String> {
2088    let q = rows.len();
2089    let mut departure = 0.0_f64;
2090    for (i, &row_index) in rows.iter().enumerate() {
2091        let row = constraints.a.row(row_index);
2092        for j in 0..q {
2093            let entry = row.dot(&lift.column(j));
2094            let target = if i == j { 1.0 } else { 0.0 };
2095            let deviation = (entry - target).abs();
2096            if !deviation.is_finite() {
2097                return Err(format!(
2098                    "the constraint-normal lift is not finite at retained row {row_index}, \
2099                     constraint-normal coordinate {j}"
2100                ));
2101            }
2102            departure = departure.max(deviation);
2103        }
2104    }
2105    Ok(departure)
2106}
2107
2108/// Solve `X W = B` for `X` given the lower Cholesky factor `L` of the symmetric
2109/// `W = L Lᵀ`, i.e. return `B W⁻¹`. `W` is symmetric so `X = (W⁻¹ Bᵀ)ᵀ`.
2110fn cholesky_solve_right(factor: &Array2<f64>, b: &Array2<f64>) -> Result<Array2<f64>, String> {
2111    let q = factor.nrows();
2112    if b.ncols() != q {
2113        return Err(format!(
2114            "constraint-normal solve: factor is {q}x{q} but the right-hand side has {} columns",
2115            b.ncols()
2116        ));
2117    }
2118    let rows = b.nrows();
2119    let mut out = Array2::<f64>::zeros((rows, q));
2120    let mut work = Array1::<f64>::zeros(q);
2121    for r in 0..rows {
2122        for i in 0..q {
2123            let mut sum = b[[r, i]];
2124            for k in 0..i {
2125                sum -= factor[[i, k]] * work[k];
2126            }
2127            work[i] = sum / factor[[i, i]];
2128        }
2129        for i in (0..q).rev() {
2130            let mut sum = work[i];
2131            for k in (i + 1)..q {
2132                sum -= factor[[k, i]] * out[[r, k]];
2133            }
2134            out[[r, i]] = sum / factor[[i, i]];
2135        }
2136    }
2137    Ok(out)
2138}
2139
2140/// Refuse a correction that is not a genuine variance REMOVAL. `Δ = W − Cov[u]`
2141/// must be positive semidefinite (truncation cannot inflate a Gaussian's
2142/// covariance) and must not exceed `W` (it cannot remove more variance than
2143/// there was). Either failure means the cubature returned something that is not
2144/// the moment of a distribution, which is a numerical failure and not a number
2145/// to report.
2146fn certify_removed_variance(removed: &Array2<f64>, w: &Array2<f64>) -> Result<(), String> {
2147    let q = removed.nrows();
2148    // Scale-free bound: both `Δ` and `W − Δ = Cov[u]` are checked against the
2149    // cubature's own accuracy in the pre-truncation metric.
2150    let slack = ORTHANT_MOMENT_RELATIVE_TOLERANCE * (q as f64);
2151    for i in 0..q {
2152        let scale = w[[i, i]];
2153        if removed[[i, i]] < -slack * scale {
2154            return Err(format!(
2155                "truncated orthant moments inflated the constraint-normal variance at index {i} \
2156                 (removed {:.6e} against scale {scale:.6e}); truncation cannot increase a \
2157                 Gaussian covariance",
2158                removed[[i, i]]
2159            ));
2160        }
2161        if removed[[i, i]] > (1.0 + slack) * scale {
2162            return Err(format!(
2163                "truncated orthant moments removed more variance than exists at index {i} \
2164                 (removed {:.6e} against scale {scale:.6e})",
2165                removed[[i, i]]
2166            ));
2167        }
2168        for j in 0..q {
2169            if !removed[[i, j]].is_finite() {
2170                return Err(format!(
2171                    "truncated orthant moments produced a non-finite entry at ({i},{j})"
2172                ));
2173            }
2174        }
2175    }
2176    Ok(())
2177}
2178
2179/// First two moments of `u ~ N(mean, covariance)` restricted to the box
2180/// `0 ≤ u ≤ upper`, where `upper_i = f64::INFINITY` makes coordinate `i` the
2181/// half-line the orthant is built from.
2182///
2183/// One dimension has the closed form and is evaluated exactly. Higher
2184/// dimensions use the Genz separation-of-variables transformation, under which
2185/// EVERY moment is an integral of the same integrand over the unit cube — so a
2186/// single cubature delivers the normalizing probability, the mean and the second
2187/// moment together, instead of the `O(q²)` separate orthant probabilities the
2188/// Tallis face/edge recursion would need. The transformation is already a
2189/// product of intervals; a finite upper limit changes only where each interval
2190/// ends, which is why a box costs the same cubature as an orthant.
2191fn box_truncated_moments(
2192    mean: &Array1<f64>,
2193    upper: &[f64],
2194    covariance: &Array2<f64>,
2195    factor: ArrayView2<'_, f64>,
2196) -> Result<(Array1<f64>, Array2<f64>), String> {
2197    let q = mean.len();
2198    if covariance.nrows() != q || covariance.ncols() != q {
2199        return Err(format!(
2200            "truncated moments: mean has length {q} but the covariance is {}x{}",
2201            covariance.nrows(),
2202            covariance.ncols()
2203        ));
2204    }
2205    if upper.len() != q {
2206        return Err(format!(
2207            "truncated moments: mean has length {q} but {} upper limits were supplied",
2208            upper.len()
2209        ));
2210    }
2211    if upper.iter().any(|limit| !(*limit > 0.0)) {
2212        return Err(format!(
2213            "truncated moments: every upper limit must sit strictly above its wall, got {upper:?}"
2214        ));
2215    }
2216    if q == 1 {
2217        return scalar_truncated_moments(mean[0], covariance[[0, 0]], upper[0]);
2218    }
2219    if factor.nrows() != q || factor.ncols() != q {
2220        return Err(format!(
2221            "orthant moments: the constraint-normal covariance is {q}x{q} but the Cholesky \
2222             factor supplied with it is {}x{}",
2223            factor.nrows(),
2224            factor.ncols()
2225        ));
2226    }
2227
2228    let generator = kronecker_generator(q);
2229    // One tilt for the whole face: it depends only on the geometry, not on the
2230    // node, so it is solved once and reused by every refinement pass.
2231    let tilt = minimax_tilt(mean, upper, factor);
2232    let mut accumulator = OrthantAccumulator::new(q);
2233    let mut evaluated = 0usize;
2234    let mut previous: Option<(Array1<f64>, Array2<f64>)> = None;
2235    loop {
2236        let target = if evaluated == 0 {
2237            ORTHANT_MOMENT_INITIAL_POINTS
2238        } else {
2239            evaluated * 2
2240        };
2241        accumulate_orthant_nodes(
2242            &mut accumulator,
2243            mean,
2244            upper,
2245            None,
2246            factor,
2247            &generator,
2248            tilt.as_ref(),
2249            0,
2250            evaluated,
2251            target,
2252        )?;
2253        evaluated = target;
2254        let current = accumulator.moments()?;
2255        if let Some(ref last) = previous
2256            && moment_relative_change(last, &current, covariance)
2257                <= ORTHANT_MOMENT_RELATIVE_TOLERANCE
2258        {
2259            return Ok(current);
2260        }
2261        if evaluated >= ORTHANT_MOMENT_MAXIMUM_POINTS {
2262            let change = previous
2263                .as_ref()
2264                .map(|last| moment_relative_change(last, &current, covariance))
2265                .unwrap_or(f64::INFINITY);
2266            // Report the FACE, not only the verdict on it. "Did not converge"
2267            // names a number without the geometry that decided it, and the two
2268            // properties that actually govern this integrand's difficulty are
2269            // measurable in three lines: how deep the walls sit in standardized
2270            // units, and how correlated the constraint normals are. Measured
2271            // on synthetic faces (`orthant_depth_measure_2601_tests`), depth
2272            // alone is harmless — an 11-dimensional face 4 sd deep with
2273            // UNCORRELATED normals converges in 16k nodes — while intermediate
2274            // correlation at the same depth does not converge at 2^20 (#2601).
2275            let depth: Vec<f64> = (0..q)
2276                .map(|i| -mean[i] / covariance[[i, i]].sqrt())
2277                .collect();
2278            let depth_min = depth.iter().copied().fold(f64::INFINITY, f64::min);
2279            let depth_max = depth.iter().copied().fold(f64::NEG_INFINITY, f64::max);
2280            let mut corr_max: f64 = 0.0;
2281            for i in 0..q {
2282                for j in 0..i {
2283                    let denominator = (covariance[[i, i]] * covariance[[j, j]]).sqrt();
2284                    if denominator > 0.0 {
2285                        corr_max = corr_max.max((covariance[[i, j]] / denominator).abs());
2286                    }
2287                }
2288            }
2289            log::debug!(
2290                "[orthant-face] q={q} mean={:?} covariance={:?}",
2291                mean.as_slice().map(<[f64]>::to_vec),
2292                covariance.as_slice().map(<[f64]>::to_vec),
2293            );
2294            return Err(format!(
2295                "truncated moments for a {q}-dimensional constraint face did not converge: \
2296                 relative moment change {change:.3e} still exceeds \
2297                 {ORTHANT_MOMENT_RELATIVE_TOLERANCE:.1e} at {evaluated} cubature nodes \
2298                 (wall depth {depth_min:.2}..{depth_max:.2} sd, \
2299                 max |correlation| between constraint normals {corr_max:.3})"
2300            ));
2301        }
2302        previous = Some(current);
2303    }
2304}
2305
2306/// Running log-scaled first and second moment accumulator for the cubature.
2307///
2308/// Node weights span hundreds of decades between a barely-truncated face and a
2309/// deeply pinned one, so the accumulators carry an explicit log scale and are
2310/// rescaled whenever a heavier node arrives. Accumulating the weights directly
2311/// would underflow the whole face to zero and leave the normalized moments as
2312/// `0/0`.
2313struct OrthantAccumulator {
2314    log_scale: f64,
2315    weight_sum: f64,
2316    weighted_mean: Array1<f64>,
2317    weighted_second: Array2<f64>,
2318}
2319
2320trait OrthantNodeSink {
2321    fn push(&mut self, log_weight: f64, point: &Array1<f64>);
2322
2323    /// Same node, with the TANGENT block of the joint rule attached.
2324    ///
2325    /// `tangent` holds the standard-normal coordinates drawn from the lattice
2326    /// dimensions past the constraint-normal block, and is empty whenever the
2327    /// caller asked for none. The default drops them, so every sink that only
2328    /// wants the truncated marginal is unaffected — and with
2329    /// `tangent_dimension = 0` the arithmetic reaching [`Self::push`] is
2330    /// bit-identical to the rule before the joint block existed, because the
2331    /// Kronecker generator of a larger dimension has the smaller one as its
2332    /// exact prefix.
2333    fn push_joint(&mut self, log_weight: f64, point: &Array1<f64>, tangent: &[f64]) {
2334        // A sink that never asked for a tangent block must never be handed
2335        // one. Dropping the coordinates instead would make a joint rule read as
2336        // a marginal rule with the same node count, which is a wrong answer
2337        // that looks exactly like a right one.
2338        assert!(
2339            tangent.is_empty(),
2340            "a sink with no joint tangent block was handed {} tangent coordinates",
2341            tangent.len()
2342        );
2343        self.push(log_weight, point);
2344    }
2345}
2346
2347impl OrthantAccumulator {
2348    fn new(q: usize) -> Self {
2349        Self {
2350            log_scale: f64::NEG_INFINITY,
2351            weight_sum: 0.0,
2352            weighted_mean: Array1::zeros(q),
2353            weighted_second: Array2::zeros((q, q)),
2354        }
2355    }
2356
2357    fn push(&mut self, log_weight: f64, point: &Array1<f64>) {
2358        let q = point.len();
2359        if log_weight > self.log_scale {
2360            let rescale = (self.log_scale - log_weight).exp();
2361            self.weight_sum *= rescale;
2362            self.weighted_mean *= rescale;
2363            self.weighted_second *= rescale;
2364            self.log_scale = log_weight;
2365        }
2366        let weight = (log_weight - self.log_scale).exp();
2367        self.weight_sum += weight;
2368        for i in 0..q {
2369            self.weighted_mean[i] += weight * point[i];
2370            for j in 0..=i {
2371                self.weighted_second[[i, j]] += weight * point[i] * point[j];
2372            }
2373        }
2374    }
2375
2376    fn moments(&self) -> Result<(Array1<f64>, Array2<f64>), String> {
2377        if !(self.weight_sum.is_finite() && self.weight_sum > 0.0) {
2378            return Err(format!(
2379                "orthant cubature accumulated no feasible mass (weight sum {:?}); the \
2380                 constraint face has no representable interior",
2381                self.weight_sum
2382            ));
2383        }
2384        let q = self.weighted_mean.len();
2385        let mean = &self.weighted_mean / self.weight_sum;
2386        let mut covariance = Array2::<f64>::zeros((q, q));
2387        for i in 0..q {
2388            for j in 0..=i {
2389                let centered = self.weighted_second[[i, j]] / self.weight_sum - mean[i] * mean[j];
2390                covariance[[i, j]] = centered;
2391                covariance[[j, i]] = centered;
2392            }
2393        }
2394        Ok((mean, covariance))
2395    }
2396}
2397
2398impl OrthantNodeSink for OrthantAccumulator {
2399    fn push(&mut self, log_weight: f64, point: &Array1<f64>) {
2400        OrthantAccumulator::push(self, log_weight, point);
2401    }
2402}
2403
2404/// One affine wall, already expressed in the cubature's own standardized
2405/// coordinates.
2406///
2407/// The box ceiling the cubature already carries is a wall whose normal is a
2408/// single coordinate, so it constrains that coordinate directly. A general wall
2409/// `aᵀu ≤ c` does not: after `u = mean + L z` it reads `(Lᵀa)ᵀz ≤ c − aᵀmean`,
2410/// which constrains the LAST coordinate its transformed normal touches, given
2411/// the ones drawn before it. That is the whole difference between the two, and
2412/// it is why the limit has to be computed per node rather than once per
2413/// coordinate.
2414///
2415/// `pivot` is that last touched coordinate. The wall says nothing about any
2416/// coordinate before it and must not be consulted there.
2417#[derive(Clone, Debug)]
2418pub struct StandardizedCeiling {
2419    /// `Lᵀa`, in the cubature's standardized coordinates.
2420    coefficients: Array1<f64>,
2421    /// `c − aᵀmean`.
2422    bound: f64,
2423    pivot: usize,
2424}
2425
2426impl StandardizedCeiling {
2427    /// Build the standardized form of `normal · u ≤ bound` for the law
2428    /// `u ~ N(mean, L Lᵀ)`.
2429    ///
2430    /// Refuses a wall whose transformed normal vanishes: such a wall constrains
2431    /// no coordinate of the cubature and is either redundant or infeasible, and
2432    /// the two cannot be told apart from the normal alone.
2433    pub fn new(
2434        normal: &Array1<f64>,
2435        bound: f64,
2436        mean: &Array1<f64>,
2437        factor: ArrayView2<'_, f64>,
2438    ) -> Result<Self, String> {
2439        let q = mean.len();
2440        if normal.len() != q {
2441            return Err(format!(
2442                "affine ceiling: the normal has length {} but the law has {q} coordinates",
2443                normal.len()
2444            ));
2445        }
2446        let mut coefficients = Array1::<f64>::zeros(q);
2447        for j in 0..q {
2448            let mut total = 0.0;
2449            for k in j..q {
2450                total += factor[[k, j]] * normal[k];
2451            }
2452            coefficients[j] = total;
2453        }
2454        let scale = coefficients
2455            .iter()
2456            .fold(0.0f64, |worst, value| worst.max(value.abs()));
2457        if !(scale.is_finite() && scale > 0.0) {
2458            return Err(format!(
2459                "affine ceiling: the standardized normal vanished (scale {scale:?}); the wall                  constrains no cubature coordinate"
2460            ));
2461        }
2462        let floor = 8.0 * f64::EPSILON * scale;
2463        let pivot = (0..q)
2464            .rev()
2465            .find(|j| coefficients[*j].abs() > floor)
2466            .ok_or_else(|| "affine ceiling: no coordinate clears the pivot floor".to_string())?;
2467        let offset = normal.dot(mean);
2468        if !(bound - offset).is_finite() {
2469            return Err(format!(
2470                "affine ceiling: the standardized bound is not finite (bound {bound:?},                  offset {offset:?})"
2471            ));
2472        }
2473        Ok(Self {
2474            coefficients,
2475            bound: bound - offset,
2476            pivot,
2477        })
2478    }
2479
2480    /// The wall's limit on coordinate `pivot` given the coordinates before it,
2481    /// as `(lower, upper)` additions — a positive pivot coefficient caps the
2482    /// coordinate from above, a negative one raises its floor.
2483    fn limit(&self, z: &Array1<f64>) -> (f64, f64) {
2484        let mut remaining = self.bound;
2485        for j in 0..self.pivot {
2486            remaining -= self.coefficients[j] * z[j];
2487        }
2488        let coefficient = self.coefficients[self.pivot];
2489        let limit = remaining / coefficient;
2490        if coefficient > 0.0 {
2491            (f64::NEG_INFINITY, limit)
2492        } else {
2493            (limit, f64::INFINITY)
2494        }
2495    }
2496}
2497
2498/// Evaluate Genz nodes `first..last` of the Kronecker sequence and fold them
2499/// into `accumulator`.
2500/// Exponential tilt for the separation-of-variables proposal, chosen at the
2501/// saddle point of the log-weight.
2502///
2503/// ## Why a tilt at all
2504///
2505/// The Genz estimator draws `z_i` from the standard normal truncated to its
2506/// conditional interval `[wall_i, oo)` and weights the node by that interval's
2507/// mass. When the constraint normals are CORRELATED, `wall_i` depends strongly
2508/// on the coordinates drawn before it, so the product of masses swings from
2509/// node to node. Measured on the face #2601's `monotone_decreasing` fit
2510/// produces: the log-weights span 26 decades and the effective sample size is
2511/// 589 of 65,536 nodes -- 0.9%. The estimator is then plain Monte Carlo with
2512/// `N_eff = ESS` no matter the node budget, which reproduces its observed error
2513/// to within a few percent (`1/sqrt(ESS)`), and no point set can repair that:
2514/// the PROPOSAL is wrong, not the quadrature.
2515///
2516/// Sampling `z_i ~ N(mu_i, 1)` truncated to the same interval and reweighting by
2517/// `exp(mu_i^2/2 - mu_i z_i)` leaves the estimator EXACTLY unbiased for any
2518/// `mu`, so the choice is a pure conditioning decision carrying no correctness
2519/// risk -- only variance.
2520///
2521/// ## Which tilt, and one that was measured and REJECTED
2522///
2523/// The obvious candidate is the dominating point: the mode of the truncated
2524/// density in `z` coordinates, i.e. the projection of the origin onto
2525/// `{z : mean + L z >= 0}`. That is the classical importance-sampling shift for
2526/// a convex region, and it is WRONG here -- measured, on this face, ESS
2527/// *fell* from 589 to 3.1 and the weight range grew from 26 decades to 65.
2528/// The reason is structural: the SOV proposal is already conditionally
2529/// truncated, so it only ever produces feasible points and its conditional mean
2530/// already sits above the wall. Shifting it by a further `mu > 0` pushes every
2531/// draw deep into the tail and the likelihood ratio `exp(mu^2/2 - mu z)` then
2532/// swings harder than the mass product it was meant to flatten.
2533///
2534/// What the weight actually is, is
2535///
2536/// ```text
2537/// psi(z, mu) = sum_i [ log Phibar(wall_i(z) - mu_i) + mu_i^2/2 - mu_i z_i ]
2538/// ```
2539///
2540/// so the tilt that flattens it is the one that makes `psi` STATIONARY -- the
2541/// saddle point in `(z, mu)` jointly (Botev, JRSS-B 2017, minimax tilting).
2542/// Differentiating gives a closed pair of conditions, with `rho(t) =
2543/// phi(t)/Phibar(t)` the normal hazard:
2544///
2545/// ```text
2546/// (A)   z_i  =  mu_i + rho(wall_i(z) - mu_i)
2547/// (B)   mu_k =  sum_{i>k} rho(wall_i(z) - mu_i) * L_ik / L_ii
2548/// ```
2549///
2550/// (A) places each coordinate at the conditional mean its own tilted, truncated
2551/// law would give; (B) sets each tilt to cancel, to first order, the movement
2552/// that coordinate induces in every LATER wall -- which is exactly the
2553/// correlation-driven fluctuation that destroyed the effective sample size.
2554/// `mu_q = 0` falls out of (B): the last coordinate moves no wall.
2555///
2556/// Solved by Gauss-Seidel with damping. Convergence is not required for
2557/// correctness -- any iterate is an unbiased tilt -- so the iteration is capped
2558/// and its last iterate used.
2559///
2560/// Returns `None` when the region already contains the origin (`mean >= 0`:
2561/// the walls are slack, the weights are already flat, and the untilted rule is
2562/// the right one) or when any coordinate carries a finite ceiling. A
2563/// two-sided coordinate is bounded on both sides, so its mass cannot swing the
2564/// way a half-line's can; leaving it untilted costs a little variance and keeps
2565/// the hazard evaluation on the one branch that is unconditionally stable.
2566fn minimax_tilt(
2567    mean: &Array1<f64>,
2568    upper: &[f64],
2569    factor: ArrayView2<'_, f64>,
2570) -> Option<Array1<f64>> {
2571    let q = mean.len();
2572    if q == 0 || factor.nrows() != q || factor.ncols() != q || upper.len() != q {
2573        return None;
2574    }
2575    if upper.iter().any(|limit| limit.is_finite()) {
2576        return None;
2577    }
2578    if mean.iter().all(|&m| m >= 0.0) {
2579        return None;
2580    }
2581    for i in 0..q {
2582        if !(factor[[i, i]] > 0.0) {
2583            return None;
2584        }
2585    }
2586
2587    // `rho(t) = phi(t)/Phibar(t)`, evaluated through logs so a deep-tail wall
2588    // does not form `0/0`. `normal_logsf` keeps the upper tail that `1 - Phi`
2589    // destroys.
2590    let hazard = |t: f64| -> f64 {
2591        let log_density = -0.5 * t * t - 0.5 * (std::f64::consts::TAU).ln();
2592        let log_tail = normal_logsf(t);
2593        if !log_tail.is_finite() {
2594            // Beyond the representable tail the hazard is asymptotically `t`
2595            // (Mills), which is the correct limit and stays finite.
2596            return t.max(0.0);
2597        }
2598        (log_density - log_tail).exp()
2599    };
2600
2601    const MAX_PASSES: usize = 200;
2602    // Under-relaxation of the Gauss-Seidel moment sweep, not a matrix ridge:
2603    // the hazard map `rho(t) = phi(t)/Phibar(t)` is steep in the deep tail, so
2604    // an undamped fixed-point sweep oscillates between the walls instead of
2605    // contracting. Taking half of each proposed update is the standard
2606    // successive-under-relaxation choice and leaves the FIXED POINT unchanged
2607    // (a converged sweep satisfies the same stationarity equations), so it can
2608    // only affect how many of the `MAX_PASSES` sweeps are needed.
2609    const DAMPING: f64 = 0.5;
2610    let mut mu = Array1::<f64>::zeros(q);
2611    let mut z = Array1::<f64>::zeros(q);
2612    let mut rho = Array1::<f64>::zeros(q);
2613    for _ in 0..MAX_PASSES {
2614        // (A), forward: each wall depends only on the coordinates before it, so
2615        // one Gauss-Seidel sweep resolves the whole chain.
2616        for i in 0..q {
2617            let mut bound = -mean[i];
2618            for j in 0..i {
2619                bound -= factor[[i, j]] * z[j];
2620            }
2621            let wall = bound / factor[[i, i]];
2622            let value = hazard(wall - mu[i]);
2623            if !value.is_finite() {
2624                return None;
2625            }
2626            rho[i] = value;
2627            z[i] = mu[i] + value;
2628        }
2629        // (B), backward.
2630        let mut moved = 0.0f64;
2631        for k in 0..q {
2632            let mut sum = 0.0;
2633            for i in (k + 1)..q {
2634                sum += rho[i] * factor[[i, k]] / factor[[i, i]];
2635            }
2636            let target = sum;
2637            let updated = mu[k] + DAMPING * (target - mu[k]);
2638            moved = moved.max((updated - mu[k]).abs());
2639            mu[k] = updated;
2640        }
2641        if moved <= 1e-12 {
2642            break;
2643        }
2644    }
2645    if mu.iter().any(|value| !value.is_finite()) {
2646        return None;
2647    }
2648    Some(mu)
2649}
2650
2651fn accumulate_orthant_nodes<S: OrthantNodeSink>(
2652    accumulator: &mut S,
2653    mean: &Array1<f64>,
2654    upper: &[f64],
2655    ceiling: Option<&StandardizedCeiling>,
2656    factor: ArrayView2<'_, f64>,
2657    generator: &[f64],
2658    // `tilt`: per-coordinate exponential tilt of the proposal
2659    // (`dominating_point_tilt`). `None` is the untilted estimator, exactly as
2660    // before. Any tilt leaves the estimator unbiased; it changes only how
2661    // evenly the node weights are spread.
2662    tilt: Option<&Array1<f64>>,
2663    // `tangent_dimension`: how many extra STANDARD-NORMAL coordinates each node
2664    // carries, drawn from lattice dimensions `q..q + tangent_dimension` of the
2665    // SAME point. This is what makes the joint rule one rule: the truncated
2666    // constraint-normal block and the Gaussian tangent block share a single
2667    // low-discrepancy point, so a consumer integrating over both pays one
2668    // evaluation per point instead of the product of two rules.
2669    tangent_dimension: usize,
2670    first: usize,
2671    last: usize,
2672) -> Result<(), String> {
2673    let q = mean.len();
2674    if generator.len() < q + tangent_dimension {
2675        return Err(format!(
2676            "orthant cubature needs a {}-dimensional generator for {q} constraint normals and \
2677             {tangent_dimension} tangent coordinates, got {}",
2678            q + tangent_dimension,
2679            generator.len()
2680        ));
2681    }
2682    let mut z = Array1::<f64>::zeros(q);
2683    let mut point = Array1::<f64>::zeros(q);
2684    let mut tangent = vec![0.0f64; tangent_dimension];
2685    for node in first..last {
2686        let offset = node as f64 + 0.5;
2687        let mut log_weight = 0.0f64;
2688        for i in 0..q {
2689            // Everything below runs in the TILTED coordinate `z_i - mu_i`: the
2690            // conditional interval shifts down by `mu_i` and the arithmetic is
2691            // untouched, so a zero tilt is bit-identical to the untilted rule.
2692            let mu = tilt.map(|t| t[i]).unwrap_or(0.0);
2693            let mut bound = -mean[i];
2694            for j in 0..i {
2695                bound -= factor[[i, j]] * z[j];
2696            }
2697            let mut wall = bound / factor[[i, i]] - mu;
2698            // The affine wall, if it pivots here, is a second candidate limit on
2699            // the SAME interval. Merging it before the interval is resolved is
2700            // what keeps one path through the reflection and the mass: from here
2701            // down, an affine-bounded coordinate and a box-bounded one are the
2702            // same arithmetic on the same `[wall, ceiling]`.
2703            let mut affine_ceiling = f64::INFINITY;
2704            if let Some(wall_rule) = ceiling
2705                && wall_rule.pivot == i
2706            {
2707                let (raised, capped) = wall_rule.limit(&z);
2708                if raised - mu > wall {
2709                    wall = raised - mu;
2710                }
2711                affine_ceiling = capped - mu;
2712            }
2713            // Tent-periodized Kronecker lattice. The raw sequence leaves the
2714            // integrand non-periodic across the cube face, which costs the
2715            // lattice rule most of its rate; folding `x ↦ 1 − |2x − 1|`
2716            // preserves the uniform measure and periodizes it.
2717            let lattice = {
2718                let raw = offset * generator[i];
2719                let fractional = raw - raw.floor();
2720                1.0 - (2.0 * fractional - 1.0).abs()
2721            };
2722            if !upper[i].is_finite() && !affine_ceiling.is_finite() {
2723                let log_tail = normal_logsf(wall);
2724                if !log_tail.is_finite() {
2725                    // The remaining feasible mass along this coordinate
2726                    // underflowed to zero: the node contributes nothing and
2727                    // cannot be renormalized, so drop it rather than propagate a
2728                    // NaN.
2729                    log_weight = f64::NEG_INFINITY;
2730                    break;
2731                }
2732                log_weight += log_tail;
2733                // `Φ̄(z_i) = (1 − x_i)·Φ̄(lower)` inverted on the upper tail, so
2734                // a deeply pinned coordinate never forms `1 − Φ(·)` in
2735                // probability space. Both factors can round to one (an inactive
2736                // coordinate at the very edge of the lattice cell), which would
2737                // ask for `Φ̄⁻¹(1)`; the smallest representable log-probability
2738                // answers that with the far-left endpoint, which is what the
2739                // region actually is there.
2740                let log_fraction = (1.0 - lattice).max(f64::MIN_POSITIVE).ln();
2741                let log_upper_tail = log_fraction + log_tail;
2742                let resolved = if log_upper_tail < 0.0 {
2743                    log_upper_tail
2744                } else {
2745                    -f64::MIN_POSITIVE
2746                };
2747                let shifted = -standard_normal_quantile_from_log_cdf(resolved)
2748                    .map_err(|error| format!("orthant cubature coordinate {i}: {error}"))?;
2749                z[i] = shifted + mu;
2750                // Likelihood ratio of the standard normal to the tilted one:
2751                // `phi(z)/phi(z - mu) = exp(mu^2/2 - mu z)`. Zero tilt adds zero.
2752                log_weight += 0.5 * mu * mu - mu * z[i];
2753                continue;
2754            }
2755
2756            // Bounded coordinate. The conditional interval is `[wall, ceiling]`
2757            // and `ceiling − wall = upper_i / L_ii` exactly, so the width never
2758            // goes through a subtraction of two conditional means.
2759            // The box width stays subtraction-free exactly as before; the
2760            // affine limit is an independent candidate and the tighter one wins.
2761            let boxed = if upper[i].is_finite() {
2762                wall + upper[i] / factor[[i, i]]
2763            } else {
2764                f64::INFINITY
2765            };
2766            let ceiling = boxed.min(affine_ceiling);
2767            if !(ceiling > wall) {
2768                // The two walls have crossed: this node's feasible interval is
2769                // empty, which is a property of the region and not a failure.
2770                log_weight = f64::NEG_INFINITY;
2771                break;
2772            }
2773            // Reflect an interval that sits in the LOWER tail. Both `Φ̄` values
2774            // are then within rounding of one and their difference — the
2775            // interval's entire mass — would be computed as a cancellation
2776            // between them. Under `z ↦ −z` the same interval is `[−ceiling,
2777            // −wall]` with both endpoints in the upper tail, where `Φ̄` is
2778            // evaluated directly. This is the regime a two-sided bound reaches
2779            // whenever the unconstrained fit lands beyond the far wall, which is
2780            // exactly when such a bound is worth declaring.
2781            let reflect = wall + ceiling < 0.0;
2782            let (low, high) = if reflect {
2783                (-ceiling, -wall)
2784            } else {
2785                (wall, ceiling)
2786            };
2787            let log_tail_low = normal_logsf(low);
2788            let log_tail_high = normal_logsf(high);
2789            if !log_tail_low.is_finite() {
2790                log_weight = f64::NEG_INFINITY;
2791                break;
2792            }
2793            // `removed ≤ 0` is the log of the fraction of the half-line's mass
2794            // that the far wall takes away.
2795            let removed = log_tail_high - log_tail_low;
2796            let log_mass = log_tail_low + log1mexp(removed);
2797            if !log_mass.is_finite() {
2798                // The slab is narrower than double precision can resolve at this
2799                // conditional position; it carries no representable mass.
2800                log_weight = f64::NEG_INFINITY;
2801                break;
2802            }
2803            log_weight += log_mass;
2804            // `Φ̄(z) = Φ̄(low)·(1 − x(1 − e^removed))`: the same upper-tail
2805            // inversion as the half-line, with the retained fraction shortened
2806            // to the slab.
2807            let retained = (-lattice * (-removed.exp_m1())).ln_1p();
2808            let log_upper_tail = log_tail_low + retained;
2809            let resolved = if log_upper_tail < 0.0 {
2810                log_upper_tail
2811            } else {
2812                -f64::MIN_POSITIVE
2813            };
2814            let sampled = -standard_normal_quantile_from_log_cdf(resolved)
2815                .map_err(|error| format!("truncated cubature coordinate {i}: {error}"))?;
2816            // The inversion is exact in probability space, so an excursion past
2817            // either endpoint is rounding in `Φ̄⁻¹` alone; the node belongs to
2818            // the interval by construction and is placed there.
2819            let clamped = sampled.clamp(low, high);
2820            z[i] = if reflect { -clamped } else { clamped } + mu;
2821            log_weight += 0.5 * mu * mu - mu * z[i];
2822        }
2823        if !log_weight.is_finite() {
2824            continue;
2825        }
2826        for i in 0..q {
2827            let mut value = mean[i];
2828            for j in 0..=i {
2829                value += factor[[i, j]] * z[j];
2830            }
2831            point[i] = value;
2832        }
2833        // Tangent block. The tent fold `x ↦ 1 − |2·frac − 1|` is measure
2834        // preserving on the unit interval, so the folded coordinate is still
2835        // uniform and `Φ⁻¹` of it is still standard normal. Both tails are
2836        // inverted from the SMALL side's own logarithm — `1 − lattice` is
2837        // `|2·frac − 1|` exactly, formed without a subtraction — so a
2838        // coordinate at either end of the cell never goes through `1 − Φ`.
2839        for (slot, tangent_value) in tangent.iter_mut().enumerate() {
2840            let raw = offset * generator[q + slot];
2841            let fractional = raw - raw.floor();
2842            let upper_side = (2.0 * fractional - 1.0).abs();
2843            let lattice = 1.0 - upper_side;
2844            *tangent_value = if lattice <= 0.5 {
2845                standard_normal_quantile_from_log_cdf(lattice.max(f64::MIN_POSITIVE).ln())
2846                    .map_err(|error| format!("joint cubature tangent coordinate {slot}: {error}"))?
2847            } else {
2848                -standard_normal_quantile_from_log_cdf(upper_side.max(f64::MIN_POSITIVE).ln())
2849                    .map_err(|error| format!("joint cubature tangent coordinate {slot}: {error}"))?
2850            };
2851        }
2852        accumulator.push_joint(log_weight, &point, &tangent);
2853    }
2854    Ok(())
2855}
2856
2857#[derive(Clone, Copy)]
2858struct WeightedProjectionNode {
2859    conditional_mean: f64,
2860    weight: f64,
2861}
2862
2863struct ProjectionNodeAccumulator<'a> {
2864    moments: OrthantAccumulator,
2865    normal_center: &'a Array1<f64>,
2866    projection_lift: &'a Array1<f64>,
2867    ambient_mean: f64,
2868    nodes: Vec<(f64, f64)>,
2869}
2870
2871impl<'a> ProjectionNodeAccumulator<'a> {
2872    fn new(
2873        normal_center: &'a Array1<f64>,
2874        projection_lift: &'a Array1<f64>,
2875        ambient_mean: f64,
2876    ) -> Self {
2877        Self {
2878            moments: OrthantAccumulator::new(normal_center.len()),
2879            normal_center,
2880            projection_lift,
2881            ambient_mean,
2882            nodes: Vec::new(),
2883        }
2884    }
2885
2886    fn normalized_nodes(self) -> Result<Vec<WeightedProjectionNode>, String> {
2887        let max_log_weight = self
2888            .nodes
2889            .iter()
2890            .map(|(_, log_weight)| *log_weight)
2891            .fold(f64::NEG_INFINITY, f64::max);
2892        if !max_log_weight.is_finite() {
2893            return Err(
2894                "orthant projection cubature accumulated no finite node weight".to_string(),
2895            );
2896        }
2897        let weight_sum = self
2898            .nodes
2899            .iter()
2900            .map(|(_, log_weight)| (*log_weight - max_log_weight).exp())
2901            .sum::<f64>();
2902        if !(weight_sum.is_finite() && weight_sum > 0.0) {
2903            return Err(format!(
2904                "orthant projection cubature has invalid normalized weight sum {weight_sum:?}"
2905            ));
2906        }
2907        Ok(self
2908            .nodes
2909            .into_iter()
2910            .map(|(conditional_mean, log_weight)| WeightedProjectionNode {
2911                conditional_mean,
2912                weight: (log_weight - max_log_weight).exp() / weight_sum,
2913            })
2914            .collect())
2915    }
2916}
2917
2918impl OrthantNodeSink for ProjectionNodeAccumulator<'_> {
2919    fn push(&mut self, log_weight: f64, point: &Array1<f64>) {
2920        self.moments.push(log_weight, point);
2921        let conditional_mean = self.ambient_mean
2922            + self
2923                .projection_lift
2924                .iter()
2925                .zip(point.iter().zip(self.normal_center.iter()))
2926                .map(|(&lift, (&value, &center))| lift * (value - center))
2927                .sum::<f64>();
2928        self.nodes.push((conditional_mean, log_weight));
2929    }
2930}
2931
2932fn converged_projection_nodes(
2933    mean: &Array1<f64>,
2934    covariance: &Array2<f64>,
2935    upper: &[f64],
2936    projection_lift: &Array1<f64>,
2937    ambient_mean: f64,
2938) -> Result<Vec<WeightedProjectionNode>, String> {
2939    let q = mean.len();
2940    if covariance.dim() != (q, q) || projection_lift.len() != q || upper.len() != q {
2941        return Err(format!(
2942            "truncated projection geometry mismatch: mean={q}, covariance={:?}, lift={}, \
2943             upper limits={}",
2944            covariance.dim(),
2945            projection_lift.len(),
2946            upper.len()
2947        ));
2948    }
2949    let factor = gam_linalg::triangular::cholesky_factor_in_place(
2950        covariance.view(),
2951        gam_linalg::triangular::CholeskyGuard::FiniteStrict,
2952    )
2953    .ok_or_else(|| {
2954        "orthant projection: the constraint-normal covariance is not numerically positive definite"
2955            .to_string()
2956    })?;
2957    let generator = kronecker_generator(q);
2958    let tilt = minimax_tilt(mean, upper, factor.view());
2959    let mut accumulator = ProjectionNodeAccumulator::new(mean, projection_lift, ambient_mean);
2960    let mut evaluated = 0usize;
2961    let mut previous: Option<(Array1<f64>, Array2<f64>)> = None;
2962    loop {
2963        let target = if evaluated == 0 {
2964            ORTHANT_MOMENT_INITIAL_POINTS
2965        } else {
2966            evaluated * 2
2967        };
2968        accumulate_orthant_nodes(
2969            &mut accumulator,
2970            mean,
2971            upper,
2972            None,
2973            factor.view(),
2974            &generator,
2975            tilt.as_ref(),
2976            0,
2977            evaluated,
2978            target,
2979        )?;
2980        evaluated = target;
2981        let current = accumulator.moments.moments()?;
2982        if let Some(ref last) = previous
2983            && moment_relative_change(last, &current, covariance)
2984                <= ORTHANT_MOMENT_RELATIVE_TOLERANCE
2985        {
2986            return accumulator.normalized_nodes();
2987        }
2988        if evaluated >= ORTHANT_MOMENT_MAXIMUM_POINTS {
2989            let change = previous
2990                .as_ref()
2991                .map(|last| moment_relative_change(last, &current, covariance))
2992                .unwrap_or(f64::INFINITY);
2993            return Err(format!(
2994                "orthant projection for a {q}-dimensional constraint face did not converge: \
2995                 relative moment change {change:.3e} still exceeds \
2996                 {ORTHANT_MOMENT_RELATIVE_TOLERANCE:.1e} at {evaluated} cubature nodes"
2997            ));
2998        }
2999        previous = Some(current);
3000    }
3001}
3002
3003fn projection_quantile(
3004    nodes: &[WeightedProjectionNode],
3005    residual_variance: f64,
3006    probability: f64,
3007    posterior_mean: f64,
3008    ambient_sd: f64,
3009) -> Result<f64, String> {
3010    if nodes.is_empty() {
3011        return Err("orthant projection quantile received no cubature nodes".to_string());
3012    }
3013    if residual_variance == 0.0 {
3014        let mut ordered = nodes.to_vec();
3015        ordered.sort_by(|left, right| left.conditional_mean.total_cmp(&right.conditional_mean));
3016        let mut cumulative = 0.0;
3017        for node in &ordered {
3018            cumulative += node.weight;
3019            if cumulative >= probability {
3020                return Ok(node.conditional_mean);
3021            }
3022        }
3023        return Ok(ordered
3024            .last()
3025            .expect("non-empty projection node set")
3026            .conditional_mean);
3027    }
3028
3029    let residual_sd = residual_variance.sqrt();
3030    let cdf = |value: f64| {
3031        nodes
3032            .iter()
3033            .map(|node| {
3034                node.weight * normal_cdf((value - node.conditional_mean) / residual_sd)
3035            })
3036            .sum::<f64>()
3037    };
3038    let mut step = ambient_sd.max(residual_sd).max(f64::MIN_POSITIVE);
3039    let mut lower = posterior_mean - step;
3040    let mut upper = posterior_mean + step;
3041    while cdf(lower) > probability {
3042        step *= 2.0;
3043        lower = posterior_mean - step;
3044        if !lower.is_finite() {
3045            return Err(format!(
3046                "orthant projection quantile could not bracket lower probability {probability}"
3047            ));
3048        }
3049    }
3050    step = ambient_sd.max(residual_sd).max(f64::MIN_POSITIVE);
3051    while cdf(upper) < probability {
3052        step *= 2.0;
3053        upper = posterior_mean + step;
3054        if !upper.is_finite() {
3055            return Err(format!(
3056                "orthant projection quantile could not bracket upper probability {probability}"
3057            ));
3058        }
3059    }
3060
3061    let resolution = f64::EPSILON.sqrt() * ambient_sd.max(residual_sd);
3062    loop {
3063        let midpoint = lower + 0.5 * (upper - lower);
3064        if midpoint == lower || midpoint == upper || upper - lower <= resolution {
3065            return Ok(midpoint);
3066        }
3067        if cdf(midpoint) < probability {
3068            lower = midpoint;
3069        } else {
3070            upper = midpoint;
3071        }
3072    }
3073}
3074
3075/// `log(1 − exp(d))` for `d ≤ 0`, evaluated on whichever of the two branches
3076/// keeps the cancellation out of the result.
3077///
3078/// `d` is always the log of the fraction of a half-line's mass that an upper
3079/// limit removes, so `d = −∞` is the unbounded coordinate and returns exactly
3080/// `0`. That exactness is what makes an infinite upper limit reproduce the
3081/// half-line arithmetic bit for bit rather than merely closely.
3082fn log1mexp(d: f64) -> f64 {
3083    if d == f64::NEG_INFINITY {
3084        return 0.0;
3085    }
3086    if d >= 0.0 {
3087        return f64::NEG_INFINITY;
3088    }
3089    if d > -std::f64::consts::LN_2 {
3090        (-d.exp_m1()).ln()
3091    } else {
3092        (-d.exp()).ln_1p()
3093    }
3094}
3095
3096/// Closed-form moments of `N(mean, variance)` restricted to `[0, upper]`.
3097///
3098/// `upper = f64::INFINITY` is the half-line, and takes the inverse-Mills branch
3099/// unchanged — a one-sided bound is not routed through the two-sided formula and
3100/// then hoped to agree with itself.
3101fn scalar_truncated_moments(
3102    mean: f64,
3103    variance: f64,
3104    upper: f64,
3105) -> Result<(Array1<f64>, Array2<f64>), String> {
3106    if !(variance.is_finite() && variance > 0.0) {
3107        return Err(format!(
3108            "scalar truncated moments need a positive finite variance, got {variance:?}"
3109        ));
3110    }
3111    let sd = variance.sqrt();
3112    // `alpha` is the truncation point in standardized units; the feasible half
3113    // is `z ≥ alpha`, and `mills` is the inverse Mills ratio `φ(α)/Φ̄(α)`
3114    // obtained on the numerically stable `Φ` branch by reflection.
3115    let alpha = -mean / sd;
3116    if !upper.is_finite() {
3117        let mills = signed_probit_logcdf_and_mills_ratio(-alpha).1;
3118        if !(mills.is_finite() && mills >= 0.0) {
3119            return Err(format!(
3120                "scalar truncated moments: inverse Mills ratio at {alpha} is {mills:?}"
3121            ));
3122        }
3123        let truncated_mean = mean + sd * mills;
3124        let truncated_variance = variance * (1.0 + alpha * mills - mills * mills);
3125        if !(truncated_variance.is_finite() && truncated_variance >= 0.0) {
3126            return Err(format!(
3127                "scalar truncated moments produced variance {truncated_variance:?} at \
3128                 standardized truncation point {alpha}"
3129            ));
3130        }
3131        return Ok((
3132            Array1::from_elem(1, truncated_mean),
3133            Array2::from_elem((1, 1), truncated_variance),
3134        ));
3135    }
3136    if !(upper > 0.0) {
3137        return Err(format!(
3138            "scalar truncated moments need the upper limit above the wall, got {upper:?}"
3139        ));
3140    }
3141    let beta = (upper - mean) / sd;
3142    // Reflect so the retained interval sits in the upper tail: every difference
3143    // below is then between two directly-evaluated tail quantities instead of
3144    // between two numbers within rounding of one.
3145    let reflect = alpha + beta < 0.0;
3146    let (low, high, centre) = if reflect {
3147        (-beta, -alpha, -mean)
3148    } else {
3149        (alpha, beta, mean)
3150    };
3151    let log_tail_low = normal_logsf(low);
3152    let log_tail_high = normal_logsf(high);
3153    let log_mass = log_tail_low + log1mexp(log_tail_high - log_tail_low);
3154    if !log_mass.is_finite() {
3155        return Err(format!(
3156            "scalar truncated moments: the interval [0, {upper:.6e}] around mean {mean:.6e} \
3157             with standard deviation {sd:.6e} carries no representable mass"
3158        ));
3159    }
3160    // `φ(high)/φ(low) = exp(½(low² − high²))`, factored as a difference of
3161    // squares so the exponent is not the cancellation of two large numbers. The
3162    // reflection above makes it non-positive.
3163    let log_density_ratio = 0.5 * (low - high) * (low + high);
3164    let density_ratio = log_density_ratio.exp();
3165    let scale = (-0.5 * low * low - 0.5 * (2.0 * std::f64::consts::PI).ln() - log_mass).exp();
3166    let first = scale * -log_density_ratio.exp_m1();
3167    let second = scale * (low - high * density_ratio);
3168    let truncated_mean = centre + sd * first;
3169    let truncated_variance = variance * (1.0 + second - first * first);
3170    if !(truncated_variance.is_finite() && truncated_variance >= 0.0) {
3171        return Err(format!(
3172            "scalar truncated moments produced variance {truncated_variance:?} on the \
3173             standardized interval [{low}, {high}]"
3174        ));
3175    }
3176    Ok((
3177        Array1::from_elem(1, if reflect { -truncated_mean } else { truncated_mean }),
3178        Array2::from_elem((1, 1), truncated_variance),
3179    ))
3180}
3181
3182/// Largest relative moment change between two cubature passes, measured in the
3183/// pre-truncation scale `sd_i = sqrt(W_ii)` so the criterion does not depend on
3184/// how the constraint rows happen to be scaled.
3185fn moment_relative_change(
3186    previous: &(Array1<f64>, Array2<f64>),
3187    current: &(Array1<f64>, Array2<f64>),
3188    w: &Array2<f64>,
3189) -> f64 {
3190    let q = current.0.len();
3191    let mut worst = 0.0f64;
3192    for i in 0..q {
3193        let sd_i = w[[i, i]].sqrt();
3194        worst = worst.max((current.0[i] - previous.0[i]).abs() / sd_i);
3195        for j in 0..q {
3196            let sd_j = w[[j, j]].sqrt();
3197            worst =
3198                worst.max((current.1[[i, j]] - previous.1[[i, j]]).abs() / (sd_i * sd_j));
3199        }
3200    }
3201    worst
3202}
3203
3204/// Kronecker (Richtmyer) lattice generator `α_i = frac(√p_i)` over the primes.
3205/// Deterministic and table-free: the sequence is reproduced from the primes
3206/// themselves, so the reported covariance does not depend on a stored vector of
3207/// magic direction numbers or on any random seed.
3208fn kronecker_generator(dimension: usize) -> Vec<f64> {
3209    let mut generator = Vec::with_capacity(dimension);
3210    let mut candidate = 2u64;
3211    while generator.len() < dimension {
3212        if is_prime(candidate) {
3213            let root = (candidate as f64).sqrt();
3214            generator.push(root - root.floor());
3215        }
3216        candidate += 1;
3217    }
3218    generator
3219}
3220
3221fn is_prime(value: u64) -> bool {
3222    if value < 2 {
3223        return false;
3224    }
3225    let mut divisor = 2u64;
3226    while divisor * divisor <= value {
3227        if value % divisor == 0 {
3228            return false;
3229        }
3230        divisor += 1;
3231    }
3232    true
3233}
3234
3235#[cfg(test)]
3236mod tests {
3237    use super::*;
3238    use ndarray::array;
3239
3240
3241    /// Independent reference: Simpson quadrature of `N(mean, variance)`
3242    /// restricted to `[0, ∞)`, with the density rescaled by its value at the
3243    /// truncation point so a deeply pinned centre does not underflow.
3244    fn quadrature_truncated_moments(mean: f64, variance: f64) -> (f64, f64) {
3245        let sd = variance.sqrt();
3246        let alpha = -mean / sd;
3247        let panels = 400_000usize;
3248        let upper = alpha + 60.0;
3249        let step = (upper - alpha) / panels as f64;
3250        let mut mass = 0.0f64;
3251        let mut first = 0.0f64;
3252        let mut second = 0.0f64;
3253        for index in 0..=panels {
3254            let z = alpha + step * index as f64;
3255            let simpson = if index == 0 || index == panels {
3256                1.0
3257            } else if index % 2 == 1 {
3258                4.0
3259            } else {
3260                2.0
3261            };
3262            let density = (-(z * z - alpha * alpha) / 2.0).exp();
3263            mass += simpson * density;
3264            first += simpson * density * z;
3265            second += simpson * density * z * z;
3266        }
3267        let m1 = first / mass;
3268        let m2 = second / mass;
3269        (mean + sd * m1, variance * (m2 - m1 * m1))
3270    }
3271
3272    /// The scalar closed form against the textbook truncated-normal moments at
3273    /// the three regimes the estimand argument turns on.
3274    #[test]
3275    fn scalar_truncated_moments_match_the_closed_form_at_every_regime() {
3276        // Mode exactly on the bound: half-normal, variance (1 - 2/pi) sigma^2.
3277        let (mean, variance) = scalar_truncated_moments(0.0, 1.0, f64::INFINITY).expect("half normal");
3278        let expected_mean = (2.0 / std::f64::consts::PI).sqrt();
3279        assert!(
3280            (mean[0] - expected_mean).abs() < 1e-12,
3281            "half-normal mean {} vs {expected_mean}",
3282            mean[0]
3283        );
3284        let expected_variance = 1.0 - 2.0 / std::f64::consts::PI;
3285        assert!(
3286            (variance[[0, 0]] - expected_variance).abs() < 1e-12,
3287            "half-normal variance {} vs {expected_variance}",
3288            variance[[0, 0]]
3289        );
3290        assert!(
3291            variance[[0, 0]] > 0.36 && variance[[0, 0]] < 0.37,
3292            "a coefficient whose mode sits exactly on its bound keeps a THIRD of its \
3293             unconstrained variance, not zero: got {}",
3294            variance[[0, 0]]
3295        );
3296
3297        // Strongly pinned: checked against an INDEPENDENT quadrature of the
3298        // truncated density rather than against an asymptotic, because the
3299        // leading `sigma^2/alpha^2` term carries an O(alpha^-4) deficit that a
3300        // tolerance would have to absorb.
3301        for center in [-2.0, -4.0, -8.0] {
3302            let (deep_mean, deep) = scalar_truncated_moments(center, 1.0, f64::INFINITY).expect("deep tail");
3303            let (reference_mean, reference_variance) = quadrature_truncated_moments(center, 1.0);
3304            assert!(
3305                (deep_mean[0] - reference_mean).abs() < 1e-9 * reference_mean.abs().max(1.0),
3306                "closed-form mean {} vs quadrature {reference_mean} at centre {center}",
3307                deep_mean[0]
3308            );
3309            assert!(
3310                (deep[[0, 0]] / reference_variance - 1.0).abs() < 1e-8,
3311                "closed-form variance {} vs quadrature {reference_variance} at centre {center}",
3312                deep[[0, 0]]
3313            );
3314            assert!(
3315                deep[[0, 0]] > 0.0,
3316                "a finite multiplier never gives zero variance, got {} at centre {center}",
3317                deep[[0, 0]]
3318            );
3319        }
3320        // ...and it does head to zero like sigma^2/alpha^2, which is the ONLY
3321        // limit in which the active-face answer becomes correct.
3322        let (_, at_eight) = scalar_truncated_moments(-8.0, 1.0, f64::INFINITY).expect("deep tail");
3323        assert!(
3324            at_eight[[0, 0]] * 64.0 > 0.9 && at_eight[[0, 0]] * 64.0 < 1.0,
3325            "variance times alpha^2 should approach one from below, got {}",
3326            at_eight[[0, 0]] * 64.0
3327        );
3328
3329        // Constraint far away: the moments relax back to the untruncated ones,
3330        // but only to the order of the tail mass the constraint still removes —
3331        // a bound five standard deviations below the centre still moves the mean
3332        // by `sd·φ(5)/Φ(5) ≈ 3e-6`, which is exactly the smooth dependence on
3333        // slack that makes a tightness predicate unnecessary.
3334        let (far_mean, far_variance) = scalar_truncated_moments(10.0, 4.0, f64::INFINITY).expect("inactive");
3335        let (reference_mean, reference_variance) = quadrature_truncated_moments(10.0, 4.0);
3336        assert!(
3337            (far_mean[0] - reference_mean).abs() < 1e-9,
3338            "inactive-bound mean {} vs quadrature {reference_mean}",
3339            far_mean[0]
3340        );
3341        assert!(
3342            (far_variance[[0, 0]] - reference_variance).abs() < 1e-9,
3343            "inactive-bound variance {} vs quadrature {reference_variance}",
3344            far_variance[[0, 0]]
3345        );
3346        assert!(
3347            (far_mean[0] - 10.0).abs() < 1e-5 && far_mean[0] > 10.0,
3348            "a bound five sd away moves the mean by the tail mass and no more, got {}",
3349            far_mean[0]
3350        );
3351        assert!(
3352            (far_variance[[0, 0]] - 4.0).abs() < 1e-4 && far_variance[[0, 0]] < 4.0,
3353            "a bound five sd away shrinks the variance by the tail mass and no more, got {}",
3354            far_variance[[0, 0]]
3355        );
3356    }
3357
3358    #[test]
3359    fn equal_tailed_projection_interval_is_asymmetric_for_a_half_normal() {
3360        let covariance = array![[1.0]];
3361        let center = array![0.0];
3362        let constraints =
3363            LinearInequalityConstraints::new(array![[1.0]], array![0.0]).expect("constraint");
3364        let correction =
3365            constrained_posterior_correction_from_covariance(&covariance, &center, &constraints)
3366                .expect("correction")
3367                .expect("active half-space");
3368        let geometry = ConstrainedPosteriorGeometry {
3369            constraints,
3370            mode: array![0.0],
3371            unconstrained_center: Some(center),
3372            correction: Some(correction),
3373            moment_status: ConstrainedPosteriorMomentStatus::Available,
3374        };
3375        let (lower, upper) = constrained_projection_equal_tailed_interval(
3376            &covariance,
3377            &geometry,
3378            &array![1.0],
3379            0.95,
3380        )
3381        .expect("equal-tailed interval");
3382
3383        // For Z | Z>=0, F(z)=2 Phi(z)-1. The equal-tailed endpoints are
3384        // Phi^-1((1+p)/2), p in {0.025, 0.975}.
3385        let expected_lower = standard_normal_quantile(0.5125).expect("lower quantile");
3386        let expected_upper = standard_normal_quantile(0.9875).expect("upper quantile");
3387        assert!(
3388            (lower - expected_lower).abs() < 2e-3,
3389            "half-normal lower endpoint {lower} vs {expected_lower}"
3390        );
3391        assert!(
3392            (upper - expected_upper).abs() < 2e-3,
3393            "half-normal upper endpoint {upper} vs {expected_upper}"
3394        );
3395        let posterior_mean = (2.0 / std::f64::consts::PI).sqrt();
3396        assert!(
3397            (posterior_mean - lower) < (upper - posterior_mean),
3398            "the exact skew interval must not collapse back to mean +/- z*sd"
3399        );
3400    }
3401
3402    #[test]
3403    fn equal_tailed_projection_sweep_has_exact_mass_and_repairs_the_short_symmetric_band() {
3404        let covariance = array![[1.0]];
3405        let constraints =
3406            LinearInequalityConstraints::new(array![[1.0]], array![0.0]).expect("constraint");
3407        let alpha = 0.025;
3408        let ambient_width =
3409            2.0 * standard_normal_quantile(1.0 - alpha).expect("ambient quantile");
3410        let mut saw_repaired_short_symmetric_band = false;
3411
3412        for center_value in [0.0, 0.25, 0.5, 0.75, 1.0, 1.5, 2.0, 3.0, 5.0] {
3413            let center = array![center_value];
3414            let correction = constrained_posterior_correction_from_covariance(
3415                &covariance,
3416                &center,
3417                &constraints,
3418            )
3419            .expect("correction")
3420            .expect("finite lower truncation");
3421            let posterior_variance =
3422                1.0 - correction.removed_variance_diagonal()[0];
3423            let geometry = ConstrainedPosteriorGeometry {
3424                constraints: constraints.clone(),
3425                mode: array![center_value.max(0.0)],
3426                unconstrained_center: Some(center),
3427                correction: Some(correction),
3428                moment_status: ConstrainedPosteriorMomentStatus::Available,
3429            };
3430            let (lower, upper) = constrained_projection_equal_tailed_interval(
3431                &covariance,
3432                &geometry,
3433                &array![1.0],
3434                0.95,
3435            )
3436            .expect("equal-tailed interval");
3437
3438            let mass_below_bound = normal_cdf(-center_value);
3439            let retained_mass = 1.0 - mass_below_bound;
3440            let truncated_cdf = |value: f64| {
3441                (normal_cdf(value - center_value) - mass_below_bound) / retained_mass
3442            };
3443            assert!(
3444                (truncated_cdf(lower) - alpha).abs() < 2e-8
3445                    && (truncated_cdf(upper) - (1.0 - alpha)).abs() < 2e-8,
3446                "centre {center_value}: endpoints [{lower}, {upper}] do not enclose exact \
3447                 posterior mass 0.95"
3448            );
3449            assert!(
3450                lower >= 0.0,
3451                "centre {center_value}: lower endpoint {lower} escaped the saved cone"
3452            );
3453            assert!(
3454                upper - lower <= ambient_width + 1e-10,
3455                "centre {center_value}: truncation widened [{lower}, {upper}] beyond the \
3456                 ambient Gaussian interval"
3457            );
3458
3459            if center_value == 3.0 {
3460                let symmetric_width = 2.0
3461                    * standard_normal_quantile(1.0 - alpha).expect("symmetric quantile")
3462                    * posterior_variance.sqrt();
3463                assert!(
3464                    upper - lower > symmetric_width,
3465                    "the exact 3-SE interval must repair the moment-matched symmetric interval's \
3466                     short, under-covering band: exact width {}, symmetric width {symmetric_width}",
3467                    upper - lower
3468                );
3469                saw_repaired_short_symmetric_band = true;
3470            }
3471        }
3472
3473        assert!(
3474            saw_repaired_short_symmetric_band,
3475            "the sweep must include its 3-SE regression cell"
3476        );
3477    }
3478
3479    /// A constraint row whose normal is nearly a combination of the accepted
3480    /// ones must be dropped — and the reason matters. It is not dropped because
3481    /// it is undetectable: its pivot sits two hundred times above the bare
3482    /// `ε·diagonal` limit at which an exactly dependent row stops being
3483    /// distinguishable. It is dropped because retaining it reports a lift
3484    /// `G = Σ Aᵀ W⁻¹` whose error exceeds the accuracy this module certifies its
3485    /// own moments to, and a wrong lift is worse than a missing row that imposes
3486    /// nothing the retained one does not already impose.
3487    ///
3488    /// Both arms are asserted so the gate discriminates: a filter that never
3489    /// drops fails the near-degenerate arm, one that always drops fails the
3490    /// resolvable arm.
3491    #[test]
3492    fn a_constraint_row_below_the_lift_accuracy_floor_is_dropped_though_detectable() {
3493        let identity = Array2::<f64>::eye(4);
3494        let center = Array1::<f64>::zeros(4);
3495
3496        let mut resolvable = Array2::<f64>::zeros((3, 4));
3497        resolvable[[0, 0]] = 1.0;
3498        resolvable[[1, 1]] = 1.0;
3499        resolvable[[2, 2]] = 1.0;
3500        let constraints = LinearInequalityConstraints::new(resolvable, Array1::<f64>::zeros(3))
3501            .expect("orthogonal constraint rows");
3502        let correction =
3503            constrained_posterior_correction_from_covariance(&identity, &center, &constraints)
3504                .expect("orthogonal face")
3505                .expect("an active face at zero slack");
3506        assert_eq!(
3507            correction.rows,
3508            vec![0, 1, 2],
3509            "three mutually independent constraint normals must all be retained"
3510        );
3511
3512        // Row 1 is row 0 rotated by `sine` in the `Σ` metric, so its pivot is
3513        // exactly `sine²` against a diagonal of `1 + sine²`.
3514        let sine = 3.0e-7;
3515        let pivot = sine * sine;
3516        let diagonal = 1.0 + pivot;
3517        let detectability_limit = 2.0 * f64::EPSILON * diagonal;
3518        assert!(
3519            pivot > detectability_limit,
3520            "the fixture must be DETECTABLE, or the drop below proves nothing: pivot \
3521             {pivot:e} against the bare rank limit {detectability_limit:e}"
3522        );
3523        assert!(
3524            pivot < detectability_limit / ORTHANT_MOMENT_RELATIVE_TOLERANCE,
3525            "the fixture must sit below the accuracy the first pass demands"
3526        );
3527
3528        let mut degenerate = Array2::<f64>::zeros((3, 4));
3529        degenerate[[0, 0]] = 1.0;
3530        degenerate[[1, 0]] = 1.0;
3531        degenerate[[1, 1]] = sine;
3532        degenerate[[2, 2]] = 1.0;
3533        let constraints = LinearInequalityConstraints::new(degenerate, Array1::<f64>::zeros(3))
3534            .expect("near-parallel constraint rows");
3535        let correction =
3536            constrained_posterior_correction_from_covariance(&identity, &center, &constraints)
3537                .expect("near-degenerate face")
3538                .expect("an active face at zero slack");
3539        assert_eq!(
3540            correction.rows,
3541            vec![0, 2],
3542            "the near-parallel row must be dropped: retaining it reports a lift whose own \
3543             defining identity A·G = I fails by more than the certified accuracy"
3544        );
3545    }
3546
3547    /// The retention floor is necessary and NOT sufficient, so the assembled
3548    /// face has to be checked and the floor raised until it delivers. Because
3549    /// the filter walks candidates in slack order rather than in pivot order —
3550    /// a statistical choice, since two near-parallel rows with different offsets
3551    /// are not the same constraint and the tighter one dominates — its per-row
3552    /// pivots do not reveal the assembled face's conditioning. Every pivot can
3553    /// clear the floor while the face as a whole does not.
3554    ///
3555    /// A Vandermonde face in clustered nodes is the sharp case: seven rows whose
3556    /// effective rank is five, all well inside the slack horizon so nothing is
3557    /// dropped for statistical reasons. Measured `max|A G − I|` on the face this
3558    /// fixture produces:
3559    ///
3560    /// * bare detectability floor, no check — `3.26e-1`, 326× the accuracy this
3561    ///   module reports its moments to;
3562    /// * one pass at the derived floor — `1.21e-1`, still 121× over;
3563    /// * the floor raised by the amount it missed by — `3.53e-5`, inside, in two
3564    ///   passes.
3565    ///
3566    /// So this gate fails on the shipped filter, fails on a single-pass floor
3567    /// change, and passes only when the realized lift governs the retained face.
3568    #[test]
3569    fn the_retained_face_satisfies_the_identity_that_defines_its_lift() {
3570        const ROWS: usize = 7;
3571        const DIMENSION: usize = 8;
3572        const DEGREE: usize = 5;
3573        const SPACING: f64 = 1.0e-2;
3574
3575        let mut a = Array2::<f64>::zeros((ROWS, DIMENSION));
3576        for row in 0..ROWS {
3577            let node = row as f64 * SPACING;
3578            for power in 0..DEGREE {
3579                a[[row, power]] = node.powi(power as i32);
3580            }
3581        }
3582        let constraints = LinearInequalityConstraints::new(a.clone(), Array1::<f64>::zeros(ROWS))
3583            .expect("clustered Vandermonde rows");
3584
3585        // Place the centre so every row sits at ~7 standardized units of slack:
3586        // inside the resolution horizon, so each row is a genuine candidate and
3587        // nothing is dropped for being statistically irrelevant, while the
3588        // truncation itself is nearly invisible — which keeps this gate a
3589        // statement about the lift rather than about the cubature.
3590        let covariance = Array2::<f64>::eye(DIMENSION);
3591        let mut center = Array1::<f64>::zeros(DIMENSION);
3592        center[0] = 7.0;
3593        for row in 0..ROWS {
3594            let normal = a.row(row);
3595            let slack = normal.dot(&center) / normal.dot(&normal).sqrt();
3596            assert!(
3597                slack < 8.12 && slack > 6.0,
3598                "row {row} must be a candidate inside the resolution horizon, got slack {slack}"
3599            );
3600        }
3601
3602        let correction =
3603            constrained_posterior_correction_from_covariance(&covariance, &center, &constraints)
3604                .expect("clustered Vandermonde face")
3605                .expect("an active face inside the horizon");
3606
3607        assert!(
3608            correction.rows.len() < ROWS,
3609            "the fixture must exercise the filter: all {ROWS} rows were retained"
3610        );
3611        assert!(
3612            correction.rows.len() >= 2,
3613            "the face must not collapse to a single row, or the identity below is vacuous: \
3614             retained {:?}",
3615            correction.rows
3616        );
3617
3618        let mut departure = 0.0_f64;
3619        for (i, &row_index) in correction.rows.iter().enumerate() {
3620            for j in 0..correction.rows.len() {
3621                let entry = a.row(row_index).dot(&correction.lift.column(j));
3622                let target = if i == j { 1.0 } else { 0.0 };
3623                departure = departure.max((entry - target).abs());
3624            }
3625        }
3626        assert!(
3627            departure <= ORTHANT_MOMENT_RELATIVE_TOLERANCE,
3628            "the reported lift must satisfy A·G = I, the identity it is defined by, to the \
3629             accuracy this module certifies its moments to: max|A G - I| = {departure:e} on \
3630             the retained rows {:?}",
3631            correction.rows
3632        );
3633    }
3634
3635    /// The retained-face walk and its oracle, sharing one face assembler.
3636    ///
3637    /// `excluded` is the set of constraint-row indices held out of the greedy
3638    /// walk. Returns the retained rows and whether their lift satisfies the
3639    /// identity that defines it, or `None` when the exclusion leaves no face.
3640    fn face_at_exclusion(
3641        candidates: &[(usize, f64, Array1<f64>)],
3642        constraints: &LinearInequalityConstraints,
3643        center: &Array1<f64>,
3644        excluded: &[usize],
3645    ) -> Option<(Vec<usize>, bool)> {
3646        let face = assemble_retained_face(
3647            candidates,
3648            ORTHANT_MOMENT_RELATIVE_TOLERANCE,
3649            constraints,
3650            center,
3651            excluded,
3652        )
3653        .expect("face assembly")?;
3654        let lift = cholesky_solve_right(&face.factor, &face.sigma_at).expect("lift solve");
3655        let departure =
3656            lift_identity_departure(&lift, constraints, &face.rows).expect("identity departure");
3657        Some((face.rows, departure <= ORTHANT_MOMENT_RELATIVE_TOLERANCE))
3658    }
3659
3660    /// #2714: the walk must return the LARGEST admissible face, and it must
3661    /// reach it by a rule that terminates.
3662    ///
3663    /// Two earlier rules searched a REAL NUMBER — the retention floor — where
3664    /// the object being chosen is a SET of rows, and both failed on that:
3665    ///
3666    /// * `demanded_accuracy /= departure/tolerance` stepped by a factor read off
3667    ///   a per-row error model the filter itself documents as not tight, so it
3668    ///   skipped faces and could leave the loop in one pass;
3669    /// * stepping to `max_r (k+1)·ε·diagonal_r/pivot_r` is exact in real
3670    ///   arithmetic and not in floating point — see
3671    ///   `the_floor_round_trip_retains_the_row_it_was_aimed_at_2714`, which
3672    ///   measures the round trip directly.
3673    ///
3674    /// The oracle here is brute force over EVERY exclusion set, which is the
3675    /// full family the walk searches (128 of them at `ROWS = 7`), rather than
3676    /// the floor-indexed subfamily the previous version of this test swept.
3677    /// Both sides use the SAME `constraint_face_candidates` /
3678    /// `assemble_retained_face`, so this compares search strategies over one
3679    /// face family rather than two implementations of the face.
3680    #[test]
3681    fn the_walk_returns_the_largest_admissible_face_2714() {
3682        const ROWS: usize = 7;
3683        const DIMENSION: usize = 8;
3684        const DEGREE: usize = 5;
3685        const SPACING: f64 = 1.0e-2;
3686
3687        let mut a = Array2::<f64>::zeros((ROWS, DIMENSION));
3688        for row in 0..ROWS {
3689            let node = row as f64 * SPACING;
3690            for power in 0..DEGREE {
3691                a[[row, power]] = node.powi(power as i32);
3692            }
3693        }
3694        let constraints = LinearInequalityConstraints::new(a.clone(), Array1::<f64>::zeros(ROWS))
3695            .expect("clustered Vandermonde rows");
3696        let covariance = Array2::<f64>::eye(DIMENSION);
3697        let mut center = Array1::<f64>::zeros(DIMENSION);
3698        center[0] = 7.0;
3699
3700        let correction =
3701            constrained_posterior_correction_from_covariance(&covariance, &center, &constraints)
3702                .expect("clustered Vandermonde face")
3703                .expect("an active face inside the horizon");
3704
3705        let sigma_at = covariance.dot(&constraints.a.t());
3706        let candidates = constraint_face_candidates(sigma_at.view(), &center, &constraints)
3707            .expect("candidate rows");
3708        let candidate_rows: Vec<usize> = candidates.iter().map(|(row, _, _)| *row).collect();
3709
3710        let mut admissible_faces: Vec<Vec<usize>> = Vec::new();
3711        let mut distinct_faces: Vec<Vec<usize>> = Vec::new();
3712        for mask in 0..(1u32 << candidate_rows.len()) {
3713            let excluded: Vec<usize> = candidate_rows
3714                .iter()
3715                .enumerate()
3716                .filter(|(position, _)| mask & (1 << position) != 0)
3717                .map(|(_, row)| *row)
3718                .collect();
3719            let Some((rows, admissible)) =
3720                face_at_exclusion(&candidates, &constraints, &center, &excluded)
3721            else {
3722                continue;
3723            };
3724            if !distinct_faces.contains(&rows) {
3725                distinct_faces.push(rows.clone());
3726            }
3727            if admissible && !admissible_faces.contains(&rows) {
3728                admissible_faces.push(rows);
3729            }
3730        }
3731        let largest = admissible_faces
3732            .iter()
3733            .map(Vec::len)
3734            .max()
3735            .expect("some exclusion set must yield a face satisfying its own identity");
3736
3737        // Non-vacuity: the fixture has to make the walk WORK. If the unexcluded
3738        // face were already admissible, or if only one face existed, this test
3739        // would pass on a walk that never ran.
3740        assert!(
3741            distinct_faces.len() >= 3,
3742            "#2714: the exclusion sweep saw only {} distinct face(s), so the fixture does not \
3743             exercise a walk: {distinct_faces:?}",
3744            distinct_faces.len()
3745        );
3746        let (unexcluded, unexcluded_admissible) =
3747            face_at_exclusion(&candidates, &constraints, &center, &[])
3748                .expect("the unexcluded face");
3749        assert!(
3750            !unexcluded_admissible,
3751            "#2714: the unexcluded face {unexcluded:?} already satisfies its own lift identity, \
3752             so the walk is not exercised"
3753        );
3754        assert!(
3755            unexcluded.len() > largest,
3756            "#2714: the unexcluded face {unexcluded:?} is no larger than the {largest}-row \
3757             answer, so nothing had to be dropped"
3758        );
3759
3760        // The walk's face must BE one of the admissible faces — this is what
3761        // says it returned a face it can actually lift — and it must be one of
3762        // the largest, which is what says it stopped dropping as soon as it
3763        // could. Asserting membership plus size rather than equality with one
3764        // enumerated face is deliberate: several distinct faces reach the
3765        // maximum, so an equality would be pinning the oracle's enumeration
3766        // order, not a property of the walk.
3767        assert!(
3768            admissible_faces.contains(&correction.rows),
3769            "#2714: the walk returned {:?}, which is not among the {} faces whose lift satisfies \
3770             its own identity: {admissible_faces:?}",
3771            correction.rows,
3772            admissible_faces.len()
3773        );
3774        assert_eq!(
3775            correction.rows.len(),
3776            largest,
3777            "#2714: the walk returned the {}-row face {:?} where an admissible face of {largest} \
3778             rows exists. Dropping the least independent accepted row is the step that reaches \
3779             the largest one; stepping a retention floor cannot, because the floor is a proxy \
3780             for the face and the proxy is not injective.",
3781            correction.rows.len(),
3782            correction.rows
3783        );
3784        // And among the largest, the one the SLACK ordering asks for: the walk
3785        // opens on the tightest candidate row and only ever drops rows that are
3786        // nearly dependent on rows already accepted, so the binding wall cannot
3787        // be the row that leaves. An enumeration over exclusion sets has no
3788        // such preference — on this fixture it also reaches a maximum-size face
3789        // that drops the tightest row — so this is the assertion that separates
3790        // the walk from "any largest face".
3791        assert_eq!(
3792            correction.rows.first(),
3793            candidate_rows.first(),
3794            "#2714: the walk returned {:?}, which does not retain the tightest candidate row \
3795             {:?}. The slack ordering is the reason a dropped row imposes no constraint the \
3796             retained ones do not; dropping the binding wall would relax the posterior by a \
3797             multiple of its own standard deviation.",
3798            correction.rows,
3799            candidate_rows.first()
3800        );
3801    }
3802
3803    /// The production entry point on the geometry #2714's witness actually has:
3804    /// far MORE constraint rows than the coefficient block has columns, so
3805    /// `W = A Σ Aᵀ` is structurally rank-deficient and the retention walk is the
3806    /// only thing between the fit and a face it can lift.
3807    ///
3808    /// A shape-constrained survival fit imposes its monotonicity guard at every
3809    /// observed exit time — one constraint row per data row, on a time block of
3810    /// a few coefficients — and the rows are near-collinear because adjacent
3811    /// times give near-identical derivative-basis rows. This fixture is that
3812    /// shape with the data removed: 40 Vandermonde rows at closely spaced nodes
3813    /// on 5 columns, identity covariance, every row within the resolution
3814    /// horizon of its wall. Nothing here is a recorded spectrum; the geometry
3815    /// is written down and the numbers follow from it.
3816    ///
3817    /// The old retention ladder PANICS on this input. Its step
3818    /// `d ← max_r (k+1)·ε·diagonal_r/pivot_r` retains the row it was aimed at
3819    /// on pass 26, rebuilds a bit-identical face, recomputes the same step, and
3820    /// trips `assert!(next < demanded_accuracy)` — which is why this test's
3821    /// primary assertion is that the call RETURNS. `constrained_posterior_correction`
3822    /// is reached from the terminal geometry of every constrained fit, so a
3823    /// panic there is a panic in a library, on data.
3824    #[test]
3825    fn a_rank_deficient_constraint_system_still_yields_a_liftable_face_2714() {
3826        const ROWS: usize = 40;
3827        const DIMENSION: usize = 5;
3828        const SPACING: f64 = 2.0e-2;
3829
3830        let mut a = Array2::<f64>::zeros((ROWS, DIMENSION));
3831        for row in 0..ROWS {
3832            let node = row as f64 * SPACING;
3833            for power in 0..DIMENSION {
3834                a[[row, power]] = node.powi(power as i32);
3835            }
3836        }
3837        let constraints = LinearInequalityConstraints::new(a, Array1::<f64>::zeros(ROWS))
3838            .expect("clustered Vandermonde rows");
3839        let covariance = Array2::<f64>::eye(DIMENSION);
3840        // Every row's value at the centre is its constant term, so every row
3841        // sits one unit above its wall in unscaled units and inside the
3842        // resolution horizon once divided by its own spread: all 40 are
3843        // candidates, against 5 columns.
3844        let mut center = Array1::<f64>::zeros(DIMENSION);
3845        center[0] = 1.0;
3846
3847        let sigma_at = covariance.dot(&constraints.a.t());
3848        let candidates = constraint_face_candidates(sigma_at.view(), &center, &constraints)
3849            .expect("candidate rows");
3850        assert!(
3851            candidates.len() > DIMENSION,
3852            "#2714: the fixture must be rank-deficient to exercise the walk, and it offers only \
3853             {} candidate row(s) against {DIMENSION} columns",
3854            candidates.len()
3855        );
3856        let (unexcluded, unexcluded_admissible) =
3857            face_at_exclusion(&candidates, &constraints, &center, &[])
3858                .expect("the unexcluded face");
3859        assert!(
3860            !unexcluded_admissible,
3861            "#2714: the unexcluded face {unexcluded:?} already satisfies its own lift identity, \
3862             so the walk never runs and this fixture asserts nothing"
3863        );
3864
3865        let correction =
3866            constrained_posterior_correction_from_covariance(&covariance, &center, &constraints)
3867                .expect("a rank-deficient constraint system must still produce a face")
3868                .expect("an active face inside the horizon");
3869
3870        // The face it settled on has to satisfy the identity that DEFINES the
3871        // lift, recomputed here from the returned `lift` rather than from the
3872        // walk's own bookkeeping.
3873        let departure = lift_identity_departure(&correction.lift, &constraints, &correction.rows)
3874            .expect("identity departure of the returned lift");
3875        assert!(
3876            departure <= ORTHANT_MOMENT_RELATIVE_TOLERANCE,
3877            "#2714: the returned {}-row face {:?} misses the identity that defines its lift by \
3878             {departure:.6e}, above {ORTHANT_MOMENT_RELATIVE_TOLERANCE:.1e}",
3879            correction.rows.len(),
3880            correction.rows
3881        );
3882        assert!(
3883            correction.rows.len() < unexcluded.len(),
3884            "#2714: the walk returned {:?}, which is not smaller than the inadmissible \
3885             unexcluded face {unexcluded:?} — so it accepted a face it had already rejected",
3886            correction.rows
3887        );
3888        assert_eq!(
3889            correction.rows.first(),
3890            candidates.first().map(|(row, _, _)| row),
3891            "#2714: the walk dropped the tightest candidate row"
3892        );
3893    }
3894
3895    /// #2714: the walk drops a DIRECTION, and a two-sided bound is one
3896    /// direction.
3897    ///
3898    /// A row anti-parallel to an accepted one is refused as a direction and
3899    /// keeps its wall as that row's upper limit (#2523), so a two-sided bound
3900    /// reaches the moments as one retained row carrying a finite `upper`. If
3901    /// the retention walk then drops that row for being nearly dependent and
3902    /// leaves its partner in the candidate pool, the next pass accepts the
3903    /// PARTNER in its place — with a full half-line and nothing to fold its own
3904    /// far wall into, because the row that carried it is excluded. The bound
3905    /// silently becomes one-sided, and on the wrong side: the walk is ordered
3906    /// by ascending slack, so the row that leaves is the tighter wall.
3907    ///
3908    /// The fixture is the rank-deficient geometry with every row given an
3909    /// opposite face, and the assertion is the invariant rather than one
3910    /// hand-picked pair: NO retained row may report an infinite upper limit,
3911    /// because every candidate direction in this system is two-sided.
3912    #[test]
3913    fn dropping_a_direction_takes_its_opposite_face_with_it_2714() {
3914        const NODES: usize = 40;
3915        const DIMENSION: usize = 5;
3916        const SPACING: f64 = 2.0e-2;
3917        // The far wall, in the same units as the near one. Every row's value at
3918        // the centre is `1`, so `3` leaves a two-unit-wide feasible slab in
3919        // every constraint-normal direction — wide enough that the region is
3920        // not near-empty and narrow enough that the far wall is inside the
3921        // resolution horizon and therefore a live candidate.
3922        const FAR_WALL: f64 = 3.0;
3923
3924        let mut a = Array2::<f64>::zeros((2 * NODES, DIMENSION));
3925        let mut b = Array1::<f64>::zeros(2 * NODES);
3926        for node in 0..NODES {
3927            let position = node as f64 * SPACING;
3928            for power in 0..DIMENSION {
3929                let entry = position.powi(power as i32);
3930                a[[node, power]] = entry;
3931                a[[NODES + node, power]] = -entry;
3932            }
3933            b[node] = 0.0;
3934            b[NODES + node] = -FAR_WALL;
3935        }
3936        let constraints =
3937            LinearInequalityConstraints::new(a, b).expect("two-sided Vandermonde slabs");
3938        let covariance = Array2::<f64>::eye(DIMENSION);
3939        let mut center = Array1::<f64>::zeros(DIMENSION);
3940        center[0] = 1.0;
3941
3942        let sigma_at = covariance.dot(&constraints.a.t());
3943        let candidates = constraint_face_candidates(sigma_at.view(), &center, &constraints)
3944            .expect("candidate rows");
3945        assert_eq!(
3946            candidates.len(),
3947            2 * NODES,
3948            "#2714: both walls of every slab must be candidates, or the fixture is not \
3949             two-sided where the walk runs"
3950        );
3951        let (unexcluded, unexcluded_admissible) =
3952            face_at_exclusion(&candidates, &constraints, &center, &[])
3953                .expect("the unexcluded face");
3954        assert!(
3955            !unexcluded_admissible,
3956            "#2714: the unexcluded face {unexcluded:?} already satisfies its own lift identity, \
3957             so no direction is ever dropped and this fixture asserts nothing"
3958        );
3959
3960        let correction =
3961            constrained_posterior_correction_from_covariance(&covariance, &center, &constraints)
3962                .expect("a two-sided rank-deficient system must still produce a face")
3963                .expect("an active face inside the horizon");
3964
3965        // Non-vacuity: the folding has to have happened at all.
3966        assert_eq!(
3967            correction.normal_upper_limits.len(),
3968            correction.rows.len(),
3969            "#2714: one upper limit per retained row"
3970        );
3971        for (position, &row) in correction.rows.iter().enumerate() {
3972            assert!(
3973                correction.normal_upper_limits[position].is_finite(),
3974                "#2714: retained row {row} reports an infinite upper limit on a system where \
3975                 EVERY direction is two-sided. Its opposite face was left in the candidate pool \
3976                 when the direction that carried the fold was dropped, so a two-sided bound is \
3977                 being reported as a half-line: rows {:?}, limits {:?}",
3978                correction.rows,
3979                correction.normal_upper_limits
3980            );
3981        }
3982        // And the near wall is the one that survived: every retained row is a
3983        // NEAR wall (index below `NODES`), never the far wall promoted in its
3984        // place.
3985        for &row in &correction.rows {
3986            assert!(
3987                row < NODES,
3988                "#2714: the walk retained far-wall row {row}, which sits at slack {FAR_WALL} \
3989                 against the near wall's 1 — the slacker of the pair replaced the binding one: \
3990                 {:?}",
3991                correction.rows
3992            );
3993        }
3994    }
3995
3996    /// The measurement that kills the retention-floor step (#2714), stated as a
3997    /// property of `f64` rather than as a story about one fit.
3998    ///
3999    /// The previous rule named the next face by the floor `d_r =
4000    /// (k+1)·ε·diagonal/pivot` at which accepted row `r` drops, because the
4001    /// retention test `pivot > (k+1)·ε·diagonal/d` then reads `pivot > pivot`.
4002    /// It does not: both sides are ROUNDED quotients, and the round trip lands
4003    /// strictly below `pivot` often enough to be reached by any fit. When it
4004    /// does, the rebuilt face is bit-identical, the recomputed step is the value
4005    /// the floor already has, and the walk stops descending — which the old code
4006    /// caught with a debug assertion, i.e. by panicking inside a library.
4007    ///
4008    /// The sweep is over the same three quantities the filter forms, across the
4009    /// magnitudes a penalized posterior actually produces. A single surviving
4010    /// triple is enough to refute the step rule; the count is reported so a
4011    /// change in the arithmetic cannot silently make this vacuous.
4012    #[test]
4013    fn the_floor_round_trip_retains_the_row_it_was_aimed_at_2714() {
4014        let mut retained = 0usize;
4015        let mut exact_stalls = 0usize;
4016        let mut examined = 0usize;
4017        for accepted in 0..12usize {
4018            for diagonal_exponent in -8i32..=4 {
4019                for pivot_decades in 1..=15i32 {
4020                    for tweak in 0..64u32 {
4021                        let diagonal = 10.0_f64.powi(diagonal_exponent)
4022                            * (1.0 + f64::from(tweak) / 64.0);
4023                        let pivot = diagonal * 10.0_f64.powi(-pivot_decades);
4024                        let scale = (accepted + 1) as f64 * f64::EPSILON * diagonal;
4025                        let step = scale / pivot;
4026                        let rebuilt_floor = scale / step;
4027                        examined += 1;
4028                        if pivot > rebuilt_floor {
4029                            retained += 1;
4030                            // The face is then unchanged, so the next step is
4031                            // recomputed from the same three numbers.
4032                            if scale / pivot == step {
4033                                exact_stalls += 1;
4034                            }
4035                        }
4036                    }
4037                }
4038            }
4039        }
4040        assert!(
4041            retained > 0,
4042            "#2714: the floor round trip never retained the row it was aimed at across \
4043             {examined} triples, which would make this refutation vacuous"
4044        );
4045        assert_eq!(
4046            retained, exact_stalls,
4047            "#2714: {retained} of {examined} triples retained the row the step was aimed at, and \
4048             {exact_stalls} of those recompute the same step. Every retention IS a stall — the \
4049             face is bit-identical, so the step is a function of unchanged inputs — and the old \
4050             rule's descent assertion fires on each one."
4051        );
4052    }
4053
4054    /// The cubature must reproduce the closed form when the orthant factorizes
4055    /// into independent coordinates, which is the only multivariate case with
4056    /// an exact answer to check against. The bound is the module's own
4057    /// certified accuracy, [`ORTHANT_MOMENT_RELATIVE_TOLERANCE`], measured on
4058    /// the pre-truncation scale — asserting tighter would assert something the
4059    /// algorithm does not promise.
4060    #[test]
4061    fn cubature_reproduces_independent_coordinates_within_its_certified_accuracy() {
4062        let mean = array![-0.5, 0.25, -1.5];
4063        let covariance = array![[2.0, 0.0, 0.0], [0.0, 0.5, 0.0], [0.0, 0.0, 1.0]];
4064        let factor = gam_linalg::triangular::cholesky_factor_in_place(
4065            covariance.view(),
4066            gam_linalg::triangular::CholeskyGuard::FiniteStrict,
4067        )
4068        .expect("independent orthant covariance factors");
4069        let (moment_mean, moment_covariance) =
4070            box_truncated_moments(&mean, &vec![f64::INFINITY; mean.len()], &covariance, factor.view())
4071                .expect("independent orthant");
4072        for i in 0..3 {
4073            let (exact_mean, exact_variance) =
4074                scalar_truncated_moments(mean[i], covariance[[i, i]], f64::INFINITY).expect("scalar");
4075            let scale = covariance[[i, i]].sqrt();
4076            assert!(
4077                (moment_mean[i] - exact_mean[0]).abs()
4078                    < ORTHANT_MOMENT_RELATIVE_TOLERANCE * scale,
4079                "coordinate {i} mean {} vs exact {}",
4080                moment_mean[i],
4081                exact_mean[0]
4082            );
4083            assert!(
4084                (moment_covariance[[i, i]] - exact_variance[[0, 0]]).abs()
4085                    < ORTHANT_MOMENT_RELATIVE_TOLERANCE * covariance[[i, i]],
4086                "coordinate {i} variance {} vs exact {}",
4087                moment_covariance[[i, i]],
4088                exact_variance[[0, 0]]
4089            );
4090            for j in 0..3 {
4091                if i != j {
4092                    assert!(
4093                        moment_covariance[[i, j]].abs()
4094                            < ORTHANT_MOMENT_RELATIVE_TOLERANCE
4095                                * scale
4096                                * covariance[[j, j]].sqrt(),
4097                        "independent coordinates must stay uncorrelated under an orthant \
4098                         truncation, got {} at ({i},{j})",
4099                        moment_covariance[[i, j]]
4100                    );
4101                }
4102            }
4103        }
4104    }
4105
4106    /// The whole point of the module: the correction lands strictly between the
4107    /// two answers the two fit paths ship today.
4108    #[test]
4109    fn correction_lands_strictly_between_full_space_and_active_face() {
4110        let covariance = array![[1.0, 0.4], [0.4, 1.0]];
4111        let constraints =
4112            LinearInequalityConstraints::new(array![[1.0, 0.0]], array![0.0]).expect("cone");
4113        // Unconstrained centre BELOW the bound: the constrained mode is pinned.
4114        let center = array![-0.6, 0.3];
4115        let correction = constrained_posterior_correction_from_covariance(&covariance, &center, &constraints)
4116            .expect("correction")
4117            .expect("an active row");
4118        let truncated = correction.apply_to_covariance(&covariance);
4119
4120        // Active-face answer: the same formula with the normal variance removed
4121        // in full.
4122        let mut face = covariance.clone();
4123        let full_removal = correction.lift.dot(&array![[1.0]]).dot(&correction.lift.t());
4124        face -= &full_removal;
4125
4126        assert!(
4127            truncated[[0, 0]] > face[[0, 0]] + 1e-6,
4128            "truncated variance {} must exceed the active-face answer {}",
4129            truncated[[0, 0]],
4130            face[[0, 0]]
4131        );
4132        assert!(
4133            truncated[[0, 0]] < covariance[[0, 0]] - 1e-6,
4134            "truncated variance {} must fall below the unconstrained answer {}",
4135            truncated[[0, 0]],
4136            covariance[[0, 0]]
4137        );
4138        assert!(
4139            face[[0, 0]].abs() < 1e-12,
4140            "the active-face answer for a single pinned coordinate is exactly zero, got {}",
4141            face[[0, 0]]
4142        );
4143        assert!(
4144            correction.normal_mean_shift[0] > 0.0,
4145            "truncation moves the posterior mean INTO the feasible region, shift was {}",
4146            correction.normal_mean_shift[0]
4147        );
4148    }
4149
4150    /// A constraint far from the posterior centre must leave the covariance
4151    /// untouched, so unconstrained-in-practice fits keep their exact bytes.
4152    #[test]
4153    fn inactive_constraints_produce_no_correction() {
4154        let covariance = array![[1.0, 0.0], [0.0, 1.0]];
4155        let constraints =
4156            LinearInequalityConstraints::new(array![[1.0, 0.0]], array![0.0]).expect("cone");
4157        let center = array![40.0, 0.0];
4158        let correction = constrained_posterior_correction_from_covariance(&covariance, &center, &constraints)
4159            .expect("correction");
4160        assert!(
4161            correction.is_none(),
4162            "a bound 40 posterior standard deviations away cannot move any moment at double \
4163             precision"
4164        );
4165    }
4166
4167    /// A duplicated constraint row must not make `W` singular.
4168    #[test]
4169    fn redundant_rows_are_dropped_by_the_rank_filter() {
4170        let covariance = array![[1.0, 0.2], [0.2, 1.0]];
4171        let constraints = LinearInequalityConstraints::new(
4172            array![[1.0, 0.0], [2.0, 0.0], [0.0, 1.0]],
4173            array![0.0, 0.0, 0.0],
4174        )
4175        .expect("cone");
4176        let center = array![-0.2, -0.3];
4177        let correction = constrained_posterior_correction_from_covariance(&covariance, &center, &constraints)
4178            .expect("correction")
4179            .expect("active rows");
4180        assert_eq!(
4181            correction.rows.len(),
4182            2,
4183            "the duplicated half-space must be filtered out, kept rows {:?}",
4184            correction.rows
4185        );
4186    }
4187
4188    /// The correction must never inflate a variance or drive one negative.
4189    #[test]
4190    fn corrected_covariance_stays_between_zero_and_the_unconstrained_answer() {
4191        let covariance = array![
4192            [1.0, 0.3, 0.1],
4193            [0.3, 1.2, -0.2],
4194            [0.1, -0.2, 0.8]
4195        ];
4196        let constraints = LinearInequalityConstraints::new(
4197            array![[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
4198            array![0.0, 0.0],
4199        )
4200        .expect("cone");
4201        for center in [
4202            array![-2.0, -1.0, 0.5],
4203            array![0.0, 0.0, 0.0],
4204            array![-0.1, 0.4, -3.0],
4205        ] {
4206            let correction = constrained_posterior_correction_from_covariance(&covariance, &center, &constraints)
4207                .expect("correction")
4208                .expect("active rows");
4209            let truncated = correction.apply_to_covariance(&covariance);
4210            for i in 0..3 {
4211                assert!(
4212                    truncated[[i, i]] > 0.0,
4213                    "coordinate {i} lost all variance at centre {center:?}: {}",
4214                    truncated[[i, i]]
4215                );
4216                assert!(
4217                    truncated[[i, i]] <= covariance[[i, i]] + 1e-9,
4218                    "coordinate {i} gained variance at centre {center:?}: {} vs {}",
4219                    truncated[[i, i]],
4220                    covariance[[i, i]]
4221                );
4222            }
4223            let diagonal = correction.removed_variance_diagonal();
4224            for i in 0..3 {
4225                assert!(
4226                    (diagonal[i] - (covariance[[i, i]] - truncated[[i, i]])).abs() < 1e-9,
4227                    "the diagonal-only accessor must agree with the dense correction at {i}"
4228                );
4229            }
4230        }
4231    }
4232    // ---------------------------------------------------------------- #2523
4233
4234    /// The two rows a `linear(min=l, max=u)` term emits: `+e_0ᵀβ ≥ l` and
4235    /// `−e_0ᵀβ ≥ −u`, exactly as `gam-terms/src/smooth/term_design.rs` builds
4236    /// them.
4237    fn two_sided_bound_rows(lower: f64, upper: f64, columns: usize) -> LinearInequalityConstraints {
4238        let mut a = Array2::<f64>::zeros((2, columns));
4239        a[[0, 0]] = 1.0;
4240        a[[1, 0]] = -1.0;
4241        LinearInequalityConstraints::new(a, array![lower, -upper])
4242            .expect("two-sided bound rows")
4243    }
4244
4245    /// Independent reference: Simpson quadrature of `N(mean, variance)`
4246    /// restricted to `[0, upper]`, built directly from the density rather than
4247    /// from any tail function the code under test also uses.
4248    fn quadrature_box_moments(mean: f64, variance: f64, upper: f64) -> (f64, f64) {
4249        let sd = variance.sqrt();
4250        let low = -mean / sd;
4251        let high = (upper - mean) / sd;
4252        let reference = if low <= 0.0 && 0.0 <= high {
4253            0.0
4254        } else if high < 0.0 {
4255            high
4256        } else {
4257            low
4258        };
4259        let panels = 400_000usize;
4260        let step = (high - low) / panels as f64;
4261        let (mut mass, mut first, mut second) = (0.0f64, 0.0f64, 0.0f64);
4262        for index in 0..=panels {
4263            let z = low + step * index as f64;
4264            let simpson = if index == 0 || index == panels {
4265                1.0
4266            } else if index % 2 == 1 {
4267                4.0
4268            } else {
4269                2.0
4270            };
4271            let density = (-(z * z - reference * reference) / 2.0).exp();
4272            mass += simpson * density;
4273            first += simpson * density * z;
4274            second += simpson * density * z * z;
4275        }
4276        let m1 = first / mass;
4277        let m2 = second / mass;
4278        (mean + sd * m1, variance * (m2 - m1 * m1))
4279    }
4280
4281    /// The defect: a row that is anti-parallel to an accepted one carries no new
4282    /// constraint-normal DIRECTION and is still a constraint. It must survive as
4283    /// the coordinate's upper limit rather than be dropped as redundant.
4284    ///
4285    /// The fixture is the one measured on #2523 — `Σ = I`, `0 ≤ β₀ ≤ 2`, ambient
4286    /// centre `0.6` — where both walls sit far inside the resolution horizon
4287    /// (`0.6` and `1.4` standardized against `8.126`), so neither is discarded
4288    /// as statistically irrelevant.
4289    #[test]
4290    fn two_sided_coefficient_bound_keeps_its_far_wall_2523() {
4291        let columns = 3;
4292        let covariance = Array2::<f64>::eye(columns);
4293        let centre = array![0.6, 0.0, 0.0];
4294        let constraints = two_sided_bound_rows(0.0, 2.0, columns);
4295        let correction = constrained_posterior_correction_from_covariance(
4296            &covariance,
4297            &centre,
4298            &constraints,
4299        )
4300        .expect("two-sided correction")
4301        .expect("an active two-sided bound corrects the posterior");
4302
4303        assert_eq!(
4304            correction.rows.len(),
4305            1,
4306            "the anti-parallel row adds no direction, so exactly one is retained"
4307        );
4308        let limits = correction.upper_limits();
4309        assert_eq!(limits.len(), 1);
4310        assert!(
4311            (limits[0] - 2.0).abs() < 1e-12,
4312            "the far wall of [0, 2] must arrive as the coordinate's upper limit, got {}",
4313            limits[0]
4314        );
4315
4316        // The retained law is now `u ~ N(0.6, 1)` on `[0, 2]`, not on `[0, ∞)`.
4317        // Both moments must be the bounded ones.
4318        let (bounded_mean, bounded_variance) =
4319            quadrature_box_moments(0.6, 1.0, 2.0);
4320        let (half_line_mean, half_line_variance) = quadrature_truncated_moments(0.6, 1.0);
4321        let reported_mean = 0.6 + correction.normal_mean_shift[0];
4322        let reported_variance = 1.0 - correction.removed_normal_variance[[0, 0]];
4323        assert!(
4324            (reported_mean - bounded_mean).abs() < 1e-6,
4325            "reported mean {reported_mean} must be the [0,2] mean {bounded_mean}, \
4326             not the [0,inf) mean {half_line_mean}"
4327        );
4328        assert!(
4329            (reported_variance - bounded_variance).abs() < 1e-6,
4330            "reported variance {reported_variance} must be the [0,2] variance \
4331             {bounded_variance}, not the [0,inf) variance {half_line_variance}"
4332        );
4333        // Discrimination: the two laws are far apart, so agreeing with one is
4334        // evidence against the other rather than a bound both would clear.
4335        assert!(
4336            (bounded_mean - half_line_mean).abs() > 0.1
4337                && (bounded_variance - half_line_variance).abs() > 0.1,
4338            "the fixture must separate the two answers: means {bounded_mean} vs \
4339             {half_line_mean}, variances {bounded_variance} vs {half_line_variance}"
4340        );
4341    }
4342
4343    /// Control for the test above: push the far wall past the resolution horizon
4344    /// and the answer must return to the half-line one BIT FOR BIT. A coordinate
4345    /// with no reachable upper limit is not merely close to the old arithmetic,
4346    /// it takes it.
4347    #[test]
4348    fn a_far_wall_beyond_the_horizon_restores_the_half_line_answer_exactly() {
4349        let columns = 3;
4350        let covariance = Array2::<f64>::eye(columns);
4351        let centre = array![0.6, 0.0, 0.0];
4352        let two_sided = constrained_posterior_correction_from_covariance(
4353            &covariance,
4354            &centre,
4355            &two_sided_bound_rows(0.0, 40.0, columns),
4356        )
4357        .expect("wide two-sided correction")
4358        .expect("the lower wall is still active");
4359
4360        let mut lower_only = Array2::<f64>::zeros((1, columns));
4361        lower_only[[0, 0]] = 1.0;
4362        let one_sided = constrained_posterior_correction_from_covariance(
4363            &covariance,
4364            &centre,
4365            &LinearInequalityConstraints::new(lower_only, array![0.0]).expect("lower wall"),
4366        )
4367        .expect("one-sided correction")
4368        .expect("an active lower bound corrects the posterior");
4369
4370        assert_eq!(two_sided.rows.len(), 1);
4371        assert_eq!(
4372            two_sided.upper_limits(),
4373            vec![f64::INFINITY],
4374            "a wall 39.4 standard deviations away is not a candidate at all"
4375        );
4376        assert_eq!(
4377            two_sided.normal_mean_shift[0], one_sided.normal_mean_shift[0],
4378            "no reachable upper limit must reproduce the half-line mean shift exactly"
4379        );
4380        assert_eq!(
4381            two_sided.removed_normal_variance[[0, 0]],
4382            one_sided.removed_normal_variance[[0, 0]],
4383            "no reachable upper limit must reproduce the half-line variance exactly"
4384        );
4385    }
4386
4387    /// A retained face whose coordinates are half-lines must SURVIVE
4388    /// persistence (#2601).
4389    ///
4390    /// `+∞` is the value an unbounded upper limit takes — not a sentinel, not a
4391    /// defect — and JSON has no literal for it. Before the extended-real codec,
4392    /// `serde_json` wrote each one as `null` and `Vec<f64>` refused its own
4393    /// output on the way back in with `invalid type: null, expected f64`, so
4394    /// every shape-constrained fit that retained a face produced a model that
4395    /// could not be loaded. The failure surfaced at LOAD, arbitrarily far from
4396    /// the fit, and named neither the field nor the fit.
4397    ///
4398    /// This is the type-level guard: whatever the fit produces, a correction
4399    /// carrying infinite, finite, and mixed limits must come back bit-for-bit.
4400    #[test]
4401    fn a_half_line_upper_limit_survives_the_json_round_trip_2601() {
4402        for limits in [
4403            vec![f64::INFINITY; 3],
4404            vec![2.5, f64::INFINITY, 1e300],
4405            Vec::new(),
4406        ] {
4407            let q = limits.len().max(1);
4408            let correction = ConstrainedPosteriorCorrection {
4409                lift: Array2::<f64>::zeros((4, q)),
4410                removed_normal_variance: Array2::<f64>::eye(q),
4411                normal_mean_shift: Array1::<f64>::zeros(q),
4412                rows: (0..q).collect(),
4413                normal_upper_limits: limits.clone(),
4414            };
4415            let json = serde_json::to_string(&correction).expect("serialize correction");
4416            let back: ConstrainedPosteriorCorrection =
4417                serde_json::from_str(&json).unwrap_or_else(|e| {
4418                    panic!("a correction with limits {limits:?} must reload: {e}\n{json}")
4419                });
4420            assert_eq!(
4421                back.normal_upper_limits, limits,
4422                "upper limits must round-trip bit for bit"
4423            );
4424            assert_eq!(back.upper_limits(), correction.upper_limits());
4425        }
4426    }
4427
4428    /// The end-to-end shape of the same defect: a correction produced by the
4429    /// solver (not hand-built) must reload. `Σ = I` with a lower bound only
4430    /// gives exactly the `+∞`-limit face that #2601's `[concave]` fit produced.
4431    #[test]
4432    fn a_solver_produced_half_line_correction_reloads_2601() {
4433        let columns = 3;
4434        let covariance = Array2::<f64>::eye(columns);
4435        let centre = array![0.6, 0.0, 0.0];
4436        let mut lower_only = Array2::<f64>::zeros((1, columns));
4437        lower_only[[0, 0]] = 1.0;
4438        let correction = constrained_posterior_correction_from_covariance(
4439            &covariance,
4440            &centre,
4441            &LinearInequalityConstraints::new(lower_only, array![0.0]).expect("lower wall"),
4442        )
4443        .expect("one-sided correction")
4444        .expect("an active lower bound corrects the posterior");
4445        assert_eq!(
4446            correction.normal_upper_limits,
4447            vec![f64::INFINITY],
4448            "precondition: a half-line coordinate carries an infinite upper limit"
4449        );
4450
4451        let json = serde_json::to_string(&correction).expect("serialize");
4452        let back: ConstrainedPosteriorCorrection =
4453            serde_json::from_str(&json).expect("a solver-produced correction must reload");
4454        assert_eq!(back.normal_upper_limits, vec![f64::INFINITY]);
4455
4456        // And the structural write-side guard must NOT mistake the legitimate
4457        // half-line for the defect it exists to catch.
4458        assert!(
4459            gam_problem::ensure_serialized_floats_are_finite(&correction).is_ok(),
4460            "an unbounded upper limit is a value, not a non-finite defect"
4461        );
4462    }
4463
4464    /// The regime a two-sided bound is declared FOR: the unconstrained fit lands
4465    /// beyond the far wall, so the retained slab sits deep in a tail. This is
4466    /// what the reflection inside the cubature and the closed form exist for; a
4467    /// sign error there puts the posterior mean outside its own box.
4468    #[test]
4469    fn a_box_the_unconstrained_centre_overshoots_stays_inside_itself() {
4470        let columns = 2;
4471        let covariance = Array2::<f64>::eye(columns);
4472        // Ambient centre at -3 with the box [-1, 1]: the coordinate
4473        // `u = beta_0 + 1` has untruncated mean -2 and lives on [0, 2].
4474        let centre = array![-3.0, 0.0];
4475        let correction = constrained_posterior_correction_from_covariance(
4476            &covariance,
4477            &centre,
4478            &two_sided_bound_rows(-1.0, 1.0, columns),
4479        )
4480        .expect("overshooting correction")
4481        .expect("both walls bind");
4482
4483        let limits = correction.upper_limits();
4484        assert!(
4485            (limits[0] - 2.0).abs() < 1e-12,
4486            "the slab is two units wide, got {}",
4487            limits[0]
4488        );
4489        let reported_mean = -2.0 + correction.normal_mean_shift[0];
4490        let reported_variance = 1.0 - correction.removed_normal_variance[[0, 0]];
4491        assert!(
4492            reported_mean > 0.0 && reported_mean < limits[0],
4493            "the posterior mean of a law supported on [0, {}] cannot sit outside it, \
4494             got {reported_mean}",
4495            limits[0]
4496        );
4497        // Popoviciu: any law on an interval of width `w` has variance at most
4498        // `w²/4`. A one-sided answer here would report ~0.06 on a coordinate the
4499        // box confines to at most 1.0, so this separates them.
4500        assert!(
4501            reported_variance > 0.0 && reported_variance <= limits[0] * limits[0] / 4.0,
4502            "variance {reported_variance} exceeds the width bound for [0, {}]",
4503            limits[0]
4504        );
4505        let (expected_mean, expected_variance) = quadrature_box_moments(-2.0, 1.0, 2.0);
4506        assert!(
4507            (reported_mean - expected_mean).abs() < 1e-6
4508                && (reported_variance - expected_variance).abs() < 1e-6,
4509            "deep-tail slab moments {reported_mean}/{reported_variance} against the \
4510             independent quadrature {expected_mean}/{expected_variance}"
4511        );
4512    }
4513
4514    /// The two branches of the scalar closed form are separate code, so they are
4515    /// checked against each other where they must agree: an upper limit far
4516    /// enough out that it removes no representable mass.
4517    #[test]
4518    fn the_two_sided_scalar_form_meets_the_mills_branch_at_a_distant_wall() {
4519        for &(mean, variance) in &[(0.6f64, 1.0f64), (-2.5, 1.0), (0.0, 4.0), (3.0, 0.25)] {
4520            let sd: f64 = variance.sqrt();
4521            let distant = mean + 40.0 * sd;
4522            let (bounded_mean, bounded_variance) =
4523                scalar_truncated_moments(mean, variance, distant).expect("bounded");
4524            let (open_mean, open_variance) =
4525                scalar_truncated_moments(mean, variance, f64::INFINITY).expect("half line");
4526            assert!(
4527                (bounded_mean[0] - open_mean[0]).abs() <= 1e-12 * open_mean[0].abs().max(1.0),
4528                "mean {} vs {} at mean={mean} variance={variance}",
4529                bounded_mean[0],
4530                open_mean[0]
4531            );
4532            assert!(
4533                (bounded_variance[[0, 0]] - open_variance[[0, 0]]).abs()
4534                    <= 1e-12 * open_variance[[0, 0]].abs().max(1.0),
4535                "variance {} vs {} at mean={mean} variance={variance}",
4536                bounded_variance[[0, 0]],
4537                open_variance[[0, 0]]
4538            );
4539        }
4540    }
4541
4542    /// The two-sided closed form across the regimes the reflection switches on,
4543    /// against Simpson quadrature of the density itself.
4544    #[test]
4545    fn the_two_sided_scalar_form_matches_an_independent_quadrature() {
4546        for &(mean, variance, upper) in &[
4547            (0.6f64, 1.0f64, 2.0f64),
4548            (-1.5, 1.0, 0.5),
4549            (3.0, 0.25, 0.4),
4550            (-4.0, 1.0, 0.2),
4551            (0.05, 1.0, 0.1),
4552            (-2.0, 1.0, 2.0),
4553            (0.5, 9.0, 12.0),
4554        ] {
4555            let (moment_mean, moment_variance) =
4556                scalar_truncated_moments(mean, variance, upper).expect("two-sided moments");
4557            let (reference_mean, reference_variance) =
4558                quadrature_box_moments(mean, variance, upper);
4559            let scale = variance.sqrt();
4560            assert!(
4561                (moment_mean[0] - reference_mean).abs() < 1e-9 * scale,
4562                "mean {} vs {reference_mean} at mean={mean} variance={variance} upper={upper}",
4563                moment_mean[0]
4564            );
4565            assert!(
4566                (moment_variance[[0, 0]] - reference_variance).abs() < 1e-9 * variance,
4567                "variance {} vs {reference_variance} at mean={mean} variance={variance} \
4568                 upper={upper}",
4569                moment_variance[[0, 0]]
4570            );
4571            assert!(
4572                moment_mean[0] > 0.0 && moment_mean[0] < upper,
4573                "the mean of a law on [0, {upper}] must lie inside it, got {}",
4574                moment_mean[0]
4575            );
4576        }
4577    }
4578
4579    /// The multivariate cubature over a genuine box, against the same
4580    /// coordinatewise reference on a product law where the two agree exactly.
4581    /// A diagonal covariance makes the box-truncated joint the product of its
4582    /// box-truncated marginals, so the reference needs no second cubature.
4583    #[test]
4584    fn the_box_cubature_reproduces_a_product_law_it_cannot_shortcut() {
4585        let mean = array![0.4, -1.2, 0.9];
4586        let covariance = Array2::from_diag(&array![1.0, 0.5, 2.0]);
4587        let upper = vec![1.5, 0.8, f64::INFINITY];
4588        let factor = gam_linalg::triangular::cholesky_factor_in_place(
4589            covariance.view(),
4590            gam_linalg::triangular::CholeskyGuard::FiniteStrict,
4591        )
4592        .expect("diagonal factor");
4593        let (cubature_mean, cubature_covariance) =
4594            box_truncated_moments(&mean, &upper, &covariance, factor.view()).expect("box moments");
4595        for i in 0..mean.len() {
4596            let (reference_mean, reference_variance) =
4597                scalar_truncated_moments(mean[i], covariance[[i, i]], upper[i])
4598                    .expect("marginal closed form");
4599            let sd = covariance[[i, i]].sqrt();
4600            assert!(
4601                (cubature_mean[i] - reference_mean[0]).abs()
4602                    < ORTHANT_MOMENT_RELATIVE_TOLERANCE * sd,
4603                "coordinate {i} mean {} vs {}",
4604                cubature_mean[i],
4605                reference_mean[0]
4606            );
4607            assert!(
4608                (cubature_covariance[[i, i]] - reference_variance[[0, 0]]).abs()
4609                    < ORTHANT_MOMENT_RELATIVE_TOLERANCE * covariance[[i, i]],
4610                "coordinate {i} variance {} vs {}",
4611                cubature_covariance[[i, i]],
4612                reference_variance[[0, 0]]
4613            );
4614        }
4615        // Independence survives truncation to a product region, so every
4616        // off-diagonal must vanish. This is what a mis-indexed upper limit would
4617        // break first.
4618        for i in 0..mean.len() {
4619            for j in 0..mean.len() {
4620                if i == j {
4621                    continue;
4622                }
4623                let sd = (covariance[[i, i]] * covariance[[j, j]]).sqrt();
4624                assert!(
4625                    cubature_covariance[[i, j]].abs() < ORTHANT_MOMENT_RELATIVE_TOLERANCE * sd,
4626                    "a product law truncated to a box stays a product law: entry ({i},{j}) \
4627                     is {}",
4628                    cubature_covariance[[i, j]]
4629                );
4630            }
4631        }
4632    }
4633
4634    /// The reflection inside the cubature, gated where it decides the answer
4635    /// rather than where it is merely present.
4636    ///
4637    /// A slab sitting `d` standard deviations BELOW the mean has both endpoints
4638    /// deep in the lower tail, where `Φ̄` is within rounding of one and the
4639    /// slab's whole mass is the difference between them. Measured against a
4640    /// Simpson reference on a diagonal `q = 2` law — where the box-truncated
4641    /// joint is exactly the product of its box-truncated marginals, so the
4642    /// reference needs no second cubature — the unreflected arithmetic holds to
4643    /// `d = 9` and then fails completely: at `d = 12` it returns zero for a mean
4644    /// of 1.9019, and by `d = 40` every node has underflowed and there is no
4645    /// mass left to normalize. Reflected, the error is 2.2e-5 at `d = 12` and
4646    /// keeps FALLING with depth, reaching 6.1e-6 at `d = 40`.
4647    ///
4648    /// `d = 12` is therefore the shallowest depth at which this test can tell
4649    /// the two apart, which is why it is the depth used.
4650    #[test]
4651    fn a_slab_twelve_deviations_below_the_mean_keeps_its_mass() {
4652        let depth = 12.0_f64;
4653        let mean = array![depth, depth];
4654        let covariance = Array2::<f64>::eye(2);
4655        let upper = vec![2.0, 2.0];
4656        let factor = gam_linalg::triangular::cholesky_factor_in_place(
4657            covariance.view(),
4658            gam_linalg::triangular::CholeskyGuard::FiniteStrict,
4659        )
4660        .expect("identity factor");
4661        let (cubature_mean, cubature_covariance) =
4662            box_truncated_moments(&mean, &upper, &covariance, factor.view())
4663                .expect("a slab deep in a tail still carries mass");
4664        let (reference_mean, reference_variance) = quadrature_box_moments(depth, 1.0, 2.0);
4665        // The density rises across the whole slab, so the mean sits near the far
4666        // wall; a lost slab would report 0 and a mis-signed reflection would
4667        // report the mirror image near the near wall.
4668        assert!(
4669            reference_mean > 1.85 && reference_mean < 2.0,
4670            "the fixture must place the mean near the far wall, got {reference_mean}"
4671        );
4672        for i in 0..2 {
4673            assert!(
4674                (cubature_mean[i] - reference_mean).abs() < 1e-3,
4675                "coordinate {i} mean {} against the Simpson reference {reference_mean}",
4676                cubature_mean[i]
4677            );
4678            assert!(
4679                (cubature_covariance[[i, i]] - reference_variance).abs() < 1e-3,
4680                "coordinate {i} variance {} against the Simpson reference {reference_variance}",
4681                cubature_covariance[[i, i]]
4682            );
4683        }
4684    }
4685
4686    /// Independent reference: Simpson CDF of `N(mean, variance)` restricted to
4687    /// `[0, upper]`, evaluated at `x`. Built from the density, so it shares no
4688    /// tail function with the quantile under test.
4689    fn quadrature_box_cdf(mean: f64, variance: f64, upper: f64, x: f64) -> f64 {
4690        let sd = variance.sqrt();
4691        let low = -mean / sd;
4692        let high = (upper - mean) / sd;
4693        let point = (x - mean) / sd;
4694        let reference = if low <= 0.0 && 0.0 <= high {
4695            0.0
4696        } else if high < 0.0 {
4697            high
4698        } else {
4699            low
4700        };
4701        let mass = |from: f64, to: f64| -> f64 {
4702            let panels = 200_000usize;
4703            let step = (to - from) / panels as f64;
4704            let mut total = 0.0f64;
4705            for index in 0..=panels {
4706                let z = from + step * index as f64;
4707                let simpson = if index == 0 || index == panels {
4708                    1.0
4709                } else if index % 2 == 1 {
4710                    4.0
4711                } else {
4712                    2.0
4713                };
4714                total += simpson * (-(z * z - reference * reference) / 2.0).exp();
4715            }
4716            total * step
4717        };
4718        mass(low, point) / mass(low, high)
4719    }
4720
4721    /// The quantile's reflection is a SEPARATE sign from the mass's, and a
4722    /// mass-only gate passes straight over a broken one.
4723    ///
4724    /// Credit to the #2529 lane, who hit exactly this: their first deep-tail
4725    /// interval returned the upper endpoint for every requested fraction — a
4726    /// perfectly in-bounds, monotone-looking answer — while the mass beside it
4727    /// stayed correct, and every case shallower than about 9σ passed. This
4728    /// module has a clamp into `[low, high]` on the inverted quantile, which is
4729    /// justified as rounding but is exactly the construct that would render
4730    /// such a collapse as a clean in-range number.
4731    ///
4732    /// So this asserts the round trip rather than the range: distinct, strictly
4733    /// increasing answers, strictly interior, and `F(Q(f)) = f` against a
4734    /// Simpson CDF built from the density. At `d = 12` the reported errors are
4735    /// at 1e-14; a collapse would show as equal answers and `F` pinned at one.
4736    #[test]
4737    fn the_deep_tail_quantile_round_trips_rather_than_collapsing_to_an_endpoint() {
4738        let mean = 12.0_f64;
4739        let variance = 1.0_f64;
4740        let upper = 2.0_f64;
4741        let fractions = [0.01, 0.1, 0.25, 0.5, 0.75, 0.9, 0.99];
4742        let mut previous = 0.0_f64;
4743        for &fraction in &fractions {
4744            let point = scalar_truncated_quantile(mean, variance, upper, fraction)
4745                .expect("a slab deep in a tail still has quantiles");
4746            assert!(
4747                point > 0.0 && point < upper,
4748                "the {fraction} quantile of a law on [0, {upper}] must be interior, got {point}"
4749            );
4750            assert!(
4751                point > previous,
4752                "quantiles must be strictly increasing in the probability; {fraction} gave \
4753                 {point} against {previous} for the fraction before it, which is what a \
4754                 collapse to one endpoint looks like"
4755            );
4756            previous = point;
4757            let recovered = quadrature_box_cdf(mean, variance, upper, point);
4758            assert!(
4759                (recovered - fraction).abs() < 1e-6,
4760                "round trip at {fraction}: the quantile returned {point}, whose independent \
4761                 Simpson CDF is {recovered}"
4762            );
4763        }
4764        // The span is bracketed rather than merely bounded below, because a
4765        // degenerate rule can be increasing and interior and still useless, and
4766        // a rule with the wrong scale can be wide.
4767        //
4768        // The bracket is derived, not chosen. Across a slab this far into a
4769        // tail the Gaussian log-density is very nearly linear, with slope
4770        // `(mean - x)/variance` — steepest at the near wall, shallowest at the
4771        // far one. An exponential law of rate `λ` puts its 1%-99% span at
4772        // `ln(99)/λ`, so the true span must sit between the values those two
4773        // slopes give. At `mean = 12`, `variance = 1`, `upper = 2` that is
4774        // `[ln(99)/12, ln(99)/10] = [0.3829, 0.4595]`, and the rule returns
4775        // 0.4453. The first version of this assertion used `0.25 * upper`, a
4776        // guess, and it was wrong in the direction that would have made the
4777        // test reject a correct answer — the span is set by the density's
4778        // steepness, not by the width of the box.
4779        let first = scalar_truncated_quantile(mean, variance, upper, 0.01).expect("low");
4780        let last = scalar_truncated_quantile(mean, variance, upper, 0.99).expect("high");
4781        let span = last - first;
4782        let steepest = mean / variance;
4783        let shallowest = (mean - upper) / variance;
4784        let narrowest = 99.0_f64.ln() / steepest;
4785        let widest = 99.0_f64.ln() / shallowest;
4786        assert!(
4787            span >= narrowest && span <= widest,
4788            "the 1%-99% span {span} is outside the [{narrowest}, {widest}] the log-density's \
4789             own slopes allow across this slab"
4790        );
4791    }
4792
4793    /// A two-sided bound with no width between its walls is an equality
4794    /// constraint, and this module reports moments of a density. It refuses
4795    /// rather than reporting the moments of a point.
4796    #[test]
4797    fn coincident_two_sided_walls_are_refused_not_collapsed() {
4798        let columns = 2;
4799        let covariance = Array2::<f64>::eye(columns);
4800        let centre = array![0.5, 0.0];
4801        let error = constrained_posterior_correction_from_covariance(
4802            &covariance,
4803            &centre,
4804            &two_sided_bound_rows(0.25, 0.25, columns),
4805        )
4806        .expect_err("an empty slab has no posterior to report");
4807        assert!(
4808            error.contains("no width between them"),
4809            "the refusal must name the geometry, got: {error}"
4810        );
4811    }
4812
4813    /// The interval path reads the same box the moments do. An equal-tailed
4814    /// interval for a bounded coordinate cannot leave the coordinate's own
4815    /// bounds — which is exactly the confidently-wrong report #2523 describes.
4816    #[test]
4817    fn a_two_sided_projection_interval_stays_within_its_own_bounds() {
4818        let columns = 2;
4819        let covariance = Array2::<f64>::eye(columns);
4820        let centre = array![0.6, 0.0];
4821        let constraints = two_sided_bound_rows(0.0, 1.0, columns);
4822        let correction = constrained_posterior_correction_from_covariance(
4823            &covariance,
4824            &centre,
4825            &constraints,
4826        )
4827        .expect("correction")
4828        .expect("active");
4829        let geometry = ConstrainedPosteriorGeometry {
4830            constraints,
4831            mode: array![0.6, 0.0],
4832            unconstrained_center: Some(centre),
4833            correction: Some(correction),
4834            moment_status: ConstrainedPosteriorMomentStatus::Available,
4835        };
4836        let (low, high) = constrained_projection_equal_tailed_interval(
4837            &covariance,
4838            &geometry,
4839            &array![1.0, 0.0],
4840            0.95,
4841        )
4842        .expect("two-sided projection interval");
4843        assert!(
4844            low >= -1e-9 && high <= 1.0 + 1e-9,
4845            "a coefficient declared to lie in [0, 1] cannot be reported in [{low}, {high}]"
4846        );
4847        assert!(low < high, "the interval must be non-degenerate");
4848    }
4849}
4850
4851/// Coverage gate for #2417.
4852///
4853/// The estimand is settled by COVERAGE against a known truth, not by the two
4854/// fit paths agreeing: two paths agreeing on a wrong covariance is not
4855/// progress. Each cell simulates from a known constrained model, refits with
4856/// the production constrained-quadratic solver, and measures what fraction of
4857/// nominal-95% intervals actually contain the truth under four procedures:
4858///
4859/// * **full space** `Σ = φH⁻¹` centred at the constrained mode — what the PIRLS
4860///   path reported before this change, with no reference to the active geometry;
4861/// * **active face** `Z(ZᵀHZ)⁻¹Zᵀ` centred at the mode — what the blockwise path
4862///   reports, reproduced here with the SAME tightness predicate it uses
4863///   (`scaled slack ≤ ACTIVE_SET_WORKING_FACE_TOL`, `covariance.rs:1583-1592`);
4864/// * **truncated** `Σ − G(W − Cov[u])Gᵀ` centred at the mode — what this module
4865///   computes and what the fit now reports;
4866/// * **truncated, mean-centred** — the same covariance around the truncated
4867///   posterior MEAN rather than the mode. Not what the fit ships (moving the
4868///   reported coefficients is out of scope for #2417); measured so the size of
4869///   the mode-vs-mean effect is on the record rather than asserted.
4870///
4871/// Among procedures that reach nominal coverage, expected interval length is
4872/// the tie-break.
4873#[cfg(test)]
4874mod orthant_tilt_2601_tests {
4875    use super::*;
4876
4877    /// The face that #2601's `monotone_decreasing` fit actually produces,
4878    /// captured from the failing fit rather than guessed at.
4879    ///
4880    /// `y ~ s(x, shape=monotone_decreasing)` on 300 rows of clean increasing
4881    /// linear data — the `[monotone_decreasing]` parametrisation of
4882    /// `test_gaussian_reml_fit_all_shape_constraints_do_not_panic`. All eleven
4883    /// monotonicity coordinates are active.
4884    ///
4885    ///   wall depth   -0.66 .. +2.65 sd   (mild; two walls are on the FEASIBLE side)
4886    ///   max |corr|    0.691
4887    ///   corr eigenvalues 0.144 .. 1.920
4888    fn refusing_face() -> (Array1<f64>, Array2<f64>) {
4889        let mean = Array1::from_vec(vec![
4890            -1.73263658148929162e-01, -1.61028415044015161e-01, -1.53745491056003519e-01,
4891            -1.14020228922959738e-01, -1.13372022294778579e-01, -5.68386260921473555e-02,
4892            -2.01787006225858517e-02, 7.79317061445884696e-04, 1.73520774327455551e-03,
4893            2.00473386863282976e-02, 4.22790949294645502e-02,
4894        ]);
4895        let w = Array2::from_shape_vec(
4896            (11, 11),
4897            vec![
4898                4.28613165678045412e-03, -6.40620724624387243e-04, -6.68752057617351208e-04,
4899                -5.83957865274831135e-04, -5.55233848052857893e-04, -7.90532734874588739e-04,
4900                -8.09846363272953844e-04, -2.35978352506513071e-04, -4.59953354398140966e-04,
4901                -2.40276816847910670e-04, -4.66860409610777736e-04, -6.40620724624387243e-04,
4902                4.30519348418363125e-03, -5.77958088890881253e-04, -7.08255560103122385e-04,
4903                -6.52329255706248488e-04, -4.23821703180157501e-04, -7.53554384768477959e-04,
4904                -1.29045376459765944e-04, -2.55856263721782311e-04, -3.25781213177120355e-04,
4905                -6.88688636712164021e-04, -6.68752057617351208e-04, -5.77958088890881253e-04,
4906                4.33564940679504254e-03, -6.74857893211149419e-04, -6.22390211789637811e-04,
4907                -7.39567533429216599e-04, -4.59996677084055332e-04, -3.33480282492368946e-04,
4908                -7.09611827680745955e-04, -1.43809374573677527e-04, -2.85910172307334801e-04,
4909                -5.83957865274831135e-04, -7.08255560103122385e-04, -6.74857893211149419e-04,
4910                4.23561983569142528e-03, -3.68170739599590739e-04, -2.42878795544258373e-04,
4911                -6.65817316820568449e-04, -7.99047220448408411e-05, -1.55911037671571136e-04,
4912                -1.76197943321878327e-04, -2.51754204620259080e-04, -5.55233848052857893e-04,
4913                -6.52329255706248488e-04, -6.22390211789637811e-04, -3.68170739599590739e-04,
4914                4.29588162752489629e-03, -7.57152955247313302e-04, -2.80188801979815898e-04,
4915                -2.28980327675770300e-04, -3.66373527157145380e-04, -9.83175770979363879e-05,
4916                -1.94759571987295659e-04, -7.90532734874588739e-04, -4.23821703180157501e-04,
4917                -7.39567533429216599e-04, -2.42878795544258373e-04, -7.57152955247313302e-04,
4918                4.82590971142383366e-03, -2.30789443520484135e-04, 5.11092628516201207e-04,
4919                4.74012818803116673e-04, -1.01494286876908989e-04, -2.04446376075374456e-04,
4920                -8.09846363272953844e-04, -7.53554384768477959e-04, -4.59996677084055332e-04,
4921                -6.65817316820568449e-04, -2.80188801979815898e-04, -2.30789443520484135e-04,
4922                4.97201788205004890e-03, -9.74102815703460092e-05, -1.93235956110176968e-04,
4923                5.67517929096417986e-04, 5.90297796785945318e-04, -2.35978352506513071e-04,
4924                -1.29045376459765944e-04, -3.33480282492368946e-04, -7.99047220448408411e-05,
4925                -2.28980327675770300e-04, 5.11092628516201207e-04, -9.74102815703460092e-05,
4926                1.15689169020833748e-03, 1.48276767553545967e-03, -4.51194193048010442e-05,
4927                -9.11302898497036765e-05, -4.59953354398140966e-04, -2.55856263721782311e-04,
4928                -7.09611827680745955e-04, -1.55911037671571136e-04, -3.66373527157145380e-04,
4929                4.74012818803116673e-04, -1.93235956110176968e-04, 1.48276767553545967e-03,
4930                4.05500634945223787e-03, -8.98171912603273025e-05, -1.81408583551147815e-04,
4931                -2.40276816847910670e-04, -3.25781213177120355e-04, -1.43809374573677527e-04,
4932                -1.76197943321878327e-04, -9.83175770979363879e-05, -1.01494286876908989e-04,
4933                5.67517929096417986e-04, -4.51194193048010442e-05, -8.98171912603273025e-05,
4934                1.17935427809862667e-03, 1.52706063180176217e-03, -4.66860409610777736e-04,
4935                -6.88688636712164021e-04, -2.85910172307334801e-04, -2.51754204620259080e-04,
4936                -1.94759571987295659e-04, -2.04446376075374456e-04, 5.90297796785945318e-04,
4937                -9.11302898497036765e-05, -1.81408583551147815e-04, 1.52706063180176217e-03,
4938                4.14230573743777988e-03,
4939            ],
4940        )
4941        .expect("11x11 captured constraint-normal covariance");
4942        (mean, w)
4943    }
4944
4945    fn lower_cholesky(w: &Array2<f64>) -> Array2<f64> {
4946        let q = w.nrows();
4947        let mut factor = Array2::<f64>::zeros((q, q));
4948        for i in 0..q {
4949            for j in 0..=i {
4950                let mut sum = w[[i, j]];
4951                for k in 0..j {
4952                    sum -= factor[[i, k]] * factor[[j, k]];
4953                }
4954                if i == j {
4955                    factor[[i, i]] = f64::sqrt(sum);
4956                } else {
4957                    factor[[i, j]] = sum / factor[[j, j]];
4958                }
4959            }
4960        }
4961        factor
4962    }
4963
4964    /// Effective sample size of the self-normalized node weights, as a fraction
4965    /// of the nodes evaluated. This is the quantity that decides whether the
4966    /// estimator is a cubature or a Monte Carlo draw wearing one's clothes.
4967    fn weight_efficiency(
4968        mean: &Array1<f64>,
4969        upper: &[f64],
4970        factor: ArrayView2<'_, f64>,
4971        tilt: Option<&Array1<f64>>,
4972        nodes: usize,
4973    ) -> (f64, f64) {
4974        struct WeightSpy {
4975            inner: OrthantAccumulator,
4976            log_weights: Vec<f64>,
4977        }
4978        impl OrthantNodeSink for WeightSpy {
4979            fn push(&mut self, log_weight: f64, point: &Array1<f64>) {
4980                self.log_weights.push(log_weight);
4981                self.inner.push(log_weight, point);
4982            }
4983        }
4984        let q = mean.len();
4985        let generator = kronecker_generator(q);
4986        let mut spy = WeightSpy {
4987            inner: OrthantAccumulator::new(q),
4988            log_weights: Vec::new(),
4989        };
4990        accumulate_orthant_nodes(
4991            &mut spy, mean, upper, None, factor, &generator, tilt, 0, 0, nodes,
4992        )
4993        .expect("orthant nodes");
4994        let finite: Vec<f64> = spy
4995            .log_weights
4996            .iter()
4997            .copied()
4998            .filter(|v| v.is_finite())
4999            .collect();
5000        let hi = finite.iter().copied().fold(f64::NEG_INFINITY, f64::max);
5001        let lo = finite.iter().copied().fold(f64::INFINITY, f64::min);
5002        let sum: f64 = finite.iter().map(|v| (v - hi).exp()).sum();
5003        let sum_sq: f64 = finite.iter().map(|v| (2.0 * (v - hi)).exp()).sum();
5004        (
5005            (sum * sum / sum_sq) / finite.len() as f64,
5006            (hi - lo) / std::f64::consts::LN_10,
5007        )
5008    }
5009
5010    /// The mechanism, measured on the real face: without the tilt the node
5011    /// weights span 26 decades and 99% of the evaluated nodes contribute
5012    /// nothing; with it they span ~1 decade and essentially every node counts.
5013    ///
5014    /// This is the assertion that would catch a regression of #2601 mechanism 3
5015    /// from the direction the convergence test cannot see. A future change that
5016    /// broke the tilt but left the tolerance reachable by brute force would pass
5017    /// a convergence check and fail this one.
5018    #[test]
5019    fn the_tilt_turns_a_monte_carlo_draw_back_into_a_cubature_2601() {
5020        let (mean, w) = refusing_face();
5021        let q = mean.len();
5022        let upper = vec![f64::INFINITY; q];
5023        let factor = lower_cholesky(&w);
5024
5025        let (untilted_ess, untilted_decades) =
5026            weight_efficiency(&mean, &upper, factor.view(), None, 1 << 16);
5027        let tilt = minimax_tilt(&mean, &upper, factor.view()).expect("this face is tilted");
5028        let (tilted_ess, tilted_decades) =
5029            weight_efficiency(&mean, &upper, factor.view(), Some(&tilt), 1 << 16);
5030
5031        println!(
5032            "MEASURE2601 untilted ess={:.4}% over {:.1} decades; \
5033             tilted ess={:.4}% over {:.1} decades",
5034            100.0 * untilted_ess,
5035            untilted_decades,
5036            100.0 * tilted_ess,
5037            tilted_decades,
5038        );
5039        assert!(
5040            untilted_ess < 0.05,
5041            "precondition: the untilted proposal wastes the node budget on this \
5042             face (ess {:.4}% of nodes)",
5043            100.0 * untilted_ess
5044        );
5045        assert!(
5046            tilted_ess > 0.5,
5047            "the tilt must make most nodes count; got ess {:.4}% of nodes over \
5048             {tilted_decades:.1} decades of weight",
5049            100.0 * tilted_ess
5050        );
5051        assert!(
5052            tilted_decades < 5.0,
5053            "the tilted weights must be nearly flat; got {tilted_decades:.1} decades"
5054        );
5055    }
5056
5057    /// The behaviour #2601 mechanism 3 reports: this face must produce moments,
5058    /// and they must be the RIGHT moments.
5059    ///
5060    /// Correctness is scored against the UNTILTED rule carried far past the
5061    /// production cap. That is an independent proposal — a different estimator
5062    /// of the same integral — so agreement is evidence about the answer rather
5063    /// than about one estimator's self-consistency. The band is set by the
5064    /// reference's own error (~1.3e-2 at 2^20, measured), not by what the
5065    /// production rule happens to produce.
5066    #[test]
5067    fn the_face_that_refuses_2601_produces_the_right_moments() {
5068        let (mean, w) = refusing_face();
5069        let q = mean.len();
5070        let upper = vec![f64::INFINITY; q];
5071        let factor = lower_cholesky(&w);
5072
5073        // Pin the geometry, so a future capture that drifts cannot silently
5074        // inherit the conclusion drawn from this one.
5075        let sd: Vec<f64> = (0..q).map(|i| f64::sqrt(w[[i, i]])).collect();
5076        let depth: Vec<f64> = (0..q).map(|i| -mean[i] / sd[i]).collect();
5077        let depth_min = depth.iter().copied().fold(f64::INFINITY, f64::min);
5078        let depth_max = depth.iter().copied().fold(f64::NEG_INFINITY, f64::max);
5079        let mut corr_max = 0.0f64;
5080        for i in 0..q {
5081            for j in 0..i {
5082                corr_max = corr_max.max(f64::abs(w[[i, j]] / (sd[i] * sd[j])));
5083            }
5084        }
5085        assert!(
5086            depth_max < 3.0 && depth_min < 0.0,
5087            "the refusing face is MILD in depth ({depth_min:.2}..{depth_max:.2} sd), \
5088             which is what rules depth out as the cause"
5089        );
5090        assert!(
5091            corr_max > 0.6,
5092            "the refusing face is strongly correlated (max |corr| = {corr_max:.3}), \
5093             which is the regime that fails"
5094        );
5095
5096        let (produced_mean, produced_cov) = box_truncated_moments(&mean, &upper, &w, factor.view())
5097            .expect("the face #2601 reports must produce moments");
5098
5099        let generator = kronecker_generator(q);
5100        let mut reference = OrthantAccumulator::new(q);
5101        accumulate_orthant_nodes(
5102            &mut reference,
5103            &mean,
5104            &upper,
5105            None,
5106            factor.view(),
5107            &generator,
5108            None,
5109            0,
5110            0,
5111            1 << 20,
5112        )
5113        .expect("untilted reference nodes");
5114        let truth = reference.moments().expect("reference moments");
5115        let gap = moment_relative_change(&(produced_mean, produced_cov), &truth, &w);
5116        println!("MEASURE2601 gap vs untilted 2^20 reference: {gap:.3e}");
5117        assert!(
5118            gap < 3.0e-2,
5119            "the tilted rule must agree with an INDEPENDENT untilted reference; \
5120             gap {gap:.3e} (the reference's own error at 2^20 is ~1.3e-2)"
5121        );
5122    }
5123
5124    /// Before/after across the synthetic sweep that first identified
5125    /// correlation as the driver: every face the untilted rule could not
5126    /// resolve at the 2^20 cap, the tilted one resolves — and the ones it
5127    /// already resolved it resolves no slower.
5128    ///
5129    /// The sweep is what rules out tail depth: an 11-dimensional face FOUR
5130    /// standard deviations deep converges in 16k nodes when the constraint
5131    /// normals are uncorrelated, while correlation at ANY depth, including
5132    /// 0.5 sd, does not converge at 2^20 without the tilt.
5133    #[test]
5134    fn the_tilt_resolves_every_correlated_face_the_sweep_could_not() {
5135        for &q in &[4usize, 8, 11] {
5136            for &c in &[0.5_f64, 1.0, 2.0, 4.0] {
5137                for &corr in &[0.0_f64, 0.6, 0.9] {
5138                    let mut w = Array2::<f64>::zeros((q, q));
5139                    for i in 0..q {
5140                        for j in 0..q {
5141                            w[[i, j]] = corr.powi((i as i32 - j as i32).abs());
5142                        }
5143                    }
5144                    let mean = Array1::<f64>::from_elem(q, -c);
5145                    let upper = vec![f64::INFINITY; q];
5146                    let factor = lower_cholesky(&w);
5147                    let (ess, decades) =
5148                        weight_efficiency(&mean, &upper, factor.view(), None, 1 << 14);
5149                    let tilt = minimax_tilt(&mean, &upper, factor.view());
5150                    let (tilted_ess, tilted_decades) =
5151                        weight_efficiency(&mean, &upper, factor.view(), tilt.as_ref(), 1 << 14);
5152                    let outcome = box_truncated_moments(&mean, &upper, &w, factor.view());
5153                    println!(
5154                        "MEASURE2601 q={q} depth={c} corr={corr} \
5155                         ess {:.2}%->{:.2}% decades {decades:.1}->{tilted_decades:.1} {}",
5156                        100.0 * ess,
5157                        100.0 * tilted_ess,
5158                        if outcome.is_ok() { "converged" } else { "REFUSED" },
5159                    );
5160                    assert!(
5161                        outcome.is_ok(),
5162                        "q={q} depth={c} corr={corr} must converge: {:?}",
5163                        outcome.err()
5164                    );
5165                    assert!(
5166                        tilted_ess >= ess * 0.9,
5167                        "the tilt must never make a face WORSE: q={q} depth={c} \
5168                         corr={corr} ess {:.4}% -> {:.4}%",
5169                        100.0 * ess,
5170                        100.0 * tilted_ess
5171                    );
5172                }
5173            }
5174        }
5175    }
5176}
5177
5178#[cfg(test)]
5179mod coverage_gate_tests {
5180    use super::*;
5181    use gam_linalg::triangular::{CholeskyGuard, cholesky_factor_in_place, cholesky_solve_vector};
5182
5183    /// Deterministic SplitMix64 — the gate must produce the same numbers on
5184    /// every host, so no external RNG and no thread-local state.
5185    struct SplitMix64 {
5186        state: u64,
5187    }
5188
5189    impl SplitMix64 {
5190        fn new(seed: u64) -> Self {
5191            Self { state: seed }
5192        }
5193        fn next_u64(&mut self) -> u64 {
5194            self.state = self.state.wrapping_add(0x9e37_79b9_7f4a_7c15);
5195            let mut z = self.state;
5196            z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
5197            z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
5198            z ^ (z >> 31)
5199        }
5200        fn unit(&mut self) -> f64 {
5201            ((self.next_u64() >> 11) as f64 + 0.5) / (1u64 << 53) as f64
5202        }
5203        fn normal(&mut self) -> f64 {
5204            let (u1, u2) = (self.unit().max(1.0e-12), self.unit());
5205            (-2.0 * u1.ln()).sqrt() * (std::f64::consts::TAU * u2).cos()
5206        }
5207    }
5208
5209    /// Realized coverage and mean half-width of one interval procedure.
5210    struct CoverageTally {
5211        covered: usize,
5212        replicates: usize,
5213        total_half_width: f64,
5214    }
5215
5216    impl CoverageTally {
5217        fn new() -> Self {
5218            Self {
5219                covered: 0,
5220                replicates: 0,
5221                total_half_width: 0.0,
5222            }
5223        }
5224        fn record(&mut self, center: f64, half_width: f64, truth: f64) {
5225            self.replicates += 1;
5226            self.total_half_width += half_width;
5227            if (truth - center).abs() <= half_width {
5228                self.covered += 1;
5229            }
5230        }
5231        fn coverage(&self) -> f64 {
5232            self.covered as f64 / self.replicates as f64
5233        }
5234        fn mean_half_width(&self) -> f64 {
5235            self.total_half_width / self.replicates as f64
5236        }
5237    }
5238
5239    /// The four procedures the gate compares, in one bundle.
5240    struct CellResult {
5241        full_space: CoverageTally,
5242        active_face: CoverageTally,
5243        truncated: CoverageTally,
5244        truncated_mean_centred: CoverageTally,
5245        pinned_fraction: f64,
5246    }
5247
5248    /// Two-sided nominal level the gate reports against.
5249    const NOMINAL_HALF_WIDTH_MULTIPLIER: f64 = 1.959_963_984_540_054;
5250    const NOMINAL_COVERAGE: f64 = 0.95;
5251
5252    /// `Σ = σ²(XᵀX)⁻¹` for a fixed design.
5253    fn gaussian_posterior_covariance(gram: &Array2<f64>, noise_variance: f64) -> Array2<f64> {
5254        let p = gram.nrows();
5255        let factor = cholesky_factor_in_place(gram.view(), CholeskyGuard::FiniteStrict)
5256            .expect("simulation design is full rank");
5257        let mut covariance = Array2::<f64>::zeros((p, p));
5258        for j in 0..p {
5259            let mut unit = Array1::<f64>::zeros(p);
5260            unit[j] = 1.0;
5261            let column = cholesky_solve_vector(&factor, &unit);
5262            for i in 0..p {
5263                covariance[[i, j]] = noise_variance * column[i];
5264            }
5265        }
5266        covariance
5267    }
5268
5269    /// Rows of `A β ≥ b` that are tight at `beta`, under the SAME scaled-slack
5270    /// predicate the blockwise covariance path applies at `β̂`.
5271    fn tight_rows_at(constraints: &LinearInequalityConstraints, beta: &Array1<f64>) -> Vec<usize> {
5272        let mut tight = Vec::new();
5273        for row_index in 0..constraints.a.nrows() {
5274            let row = constraints.a.row(row_index).to_owned();
5275            let norm = row.dot(&row).sqrt();
5276            if norm > 0.0
5277                && (row.dot(beta) - constraints.b[row_index]) / norm
5278                    <= crate::active_set::ACTIVE_SET_WORKING_FACE_TOL
5279            {
5280                tight.push(row_index);
5281            }
5282        }
5283        tight
5284    }
5285
5286    /// `Σ_face = Σ − ΣA_tᵀ(A_tΣA_tᵀ)⁻¹A_tΣ` on the tight rows: the active-face
5287    /// reduction written as the same low-rank removal, so the comparator and
5288    /// the estimand under test differ ONLY in whether the constraint-normal
5289    /// variance is removed in full or only by the truncated part.
5290    fn active_face_variance(
5291        covariance: &Array2<f64>,
5292        constraints: &LinearInequalityConstraints,
5293        tight: &[usize],
5294        index: usize,
5295    ) -> f64 {
5296        if tight.is_empty() {
5297            return covariance[[index, index]];
5298        }
5299        let q = tight.len();
5300        let mut sigma_at = Array2::<f64>::zeros((covariance.nrows(), q));
5301        for (position, &row_index) in tight.iter().enumerate() {
5302            let column = covariance.dot(&constraints.a.row(row_index).to_owned());
5303            sigma_at.column_mut(position).assign(&column);
5304        }
5305        let mut normal = Array2::<f64>::zeros((q, q));
5306        for (i, &row_i) in tight.iter().enumerate() {
5307            for j in 0..q {
5308                normal[[i, j]] = constraints
5309                    .a
5310                    .row(row_i)
5311                    .to_owned()
5312                    .dot(&sigma_at.column(j).to_owned());
5313            }
5314        }
5315        let Some(factor) = cholesky_factor_in_place(normal.view(), CholeskyGuard::FiniteStrict)
5316        else {
5317            // A rank-deficient tight face pins every direction it spans; the
5318            // face answer for this coordinate is zero variance.
5319            return 0.0;
5320        };
5321        let row = sigma_at.row(index).to_owned();
5322        let solved = cholesky_solve_vector(&factor, &row);
5323        covariance[[index, index]] - row.dot(&solved)
5324    }
5325
5326    /// One simulation cell: a fixed design, a known truth strictly inside
5327    /// `A β ≥ 0`, and `replicates` refits under Gaussian noise with a KNOWN
5328    /// noise scale, so the comparison isolates the covariance question from
5329    /// dispersion estimation.
5330    fn run_cell(
5331        design: &Array2<f64>,
5332        truth: &Array1<f64>,
5333        constraints: &LinearInequalityConstraints,
5334        reported_index: usize,
5335        noise_sd: f64,
5336        replicates: usize,
5337        seed: u64,
5338    ) -> CellResult {
5339        let n = design.nrows();
5340        let p = design.ncols();
5341        let gram = design.t().dot(design);
5342        let covariance = gaussian_posterior_covariance(&gram, noise_sd * noise_sd);
5343        let mut rng = SplitMix64::new(seed);
5344        let mut result = CellResult {
5345            full_space: CoverageTally::new(),
5346            active_face: CoverageTally::new(),
5347            truncated: CoverageTally::new(),
5348            truncated_mean_centred: CoverageTally::new(),
5349            pinned_fraction: 0.0,
5350        };
5351        let mean_response = design.dot(truth);
5352        let mut pinned = 0usize;
5353
5354        for _ in 0..replicates {
5355            let mut response = Array1::<f64>::zeros(n);
5356            for i in 0..n {
5357                response[i] = mean_response[i] + noise_sd * rng.normal();
5358            }
5359            let rhs = design.t().dot(&response);
5360            let start = crate::active_set::feasible_point_for_linear_constraints(constraints, p)
5361                .expect("the simulation cone has an interior");
5362            let (beta_hat, _) = crate::active_set::solve_quadratic_with_linear_constraints(
5363                &gram,
5364                &rhs,
5365                &start,
5366                constraints,
5367                None,
5368            )
5369            .expect("constrained quadratic solve");
5370
5371            let full_half_width =
5372                NOMINAL_HALF_WIDTH_MULTIPLIER * covariance[[reported_index, reported_index]].sqrt();
5373            result.full_space.record(
5374                beta_hat[reported_index],
5375                full_half_width,
5376                truth[reported_index],
5377            );
5378
5379            let tight = tight_rows_at(constraints, &beta_hat);
5380            if !tight.is_empty() {
5381                pinned += 1;
5382            }
5383            let face_variance =
5384                active_face_variance(&covariance, constraints, &tight, reported_index);
5385            result.active_face.record(
5386                beta_hat[reported_index],
5387                NOMINAL_HALF_WIDTH_MULTIPLIER * face_variance.max(0.0).sqrt(),
5388                truth[reported_index],
5389            );
5390
5391            // `β_unc = β̂ − Σ ∇ℓ_p(β̂)` with `∇ℓ_p(β̂) = XᵀXβ̂ − Xᵀy`. For this
5392            // Gaussian cell that is exactly the unconstrained least-squares
5393            // solution — the centre a truncated Gaussian keeps.
5394            let penalized_gradient = gram.dot(&beta_hat) - &rhs;
5395            let center = &beta_hat
5396                - &(covariance.dot(&penalized_gradient) / (noise_sd * noise_sd));
5397            let correction =
5398                constrained_posterior_correction_from_covariance(&covariance, &center, constraints)
5399                    .expect("truncated correction");
5400            let (truncated_half_width, truncated_center) = match correction {
5401                None => (full_half_width, beta_hat[reported_index]),
5402                Some(ref correction) => {
5403                    let variance = covariance[[reported_index, reported_index]]
5404                        - correction.removed_variance_diagonal()[reported_index];
5405                    (
5406                        NOMINAL_HALF_WIDTH_MULTIPLIER * variance.max(0.0).sqrt(),
5407                        correction.posterior_mean(&center)[reported_index],
5408                    )
5409                }
5410            };
5411            result.truncated.record(
5412                beta_hat[reported_index],
5413                truncated_half_width,
5414                truth[reported_index],
5415            );
5416            result.truncated_mean_centred.record(
5417                truncated_center,
5418                truncated_half_width,
5419                truth[reported_index],
5420            );
5421        }
5422        result.pinned_fraction = pinned as f64 / replicates as f64;
5423        result
5424    }
5425
5426    fn report_cell(label: &str, cell: &CellResult) {
5427        eprintln!(
5428            "[#2417 coverage] {label}: nominal {NOMINAL_COVERAGE:.2}, {} replicates, mode pinned \
5429             in {:.1}% of them",
5430            cell.full_space.replicates,
5431            100.0 * cell.pinned_fraction
5432        );
5433        for (name, tally) in [
5434            ("full space          ", &cell.full_space),
5435            ("active face         ", &cell.active_face),
5436            ("truncated           ", &cell.truncated),
5437            ("truncated+mean shift", &cell.truncated_mean_centred),
5438        ] {
5439            eprintln!(
5440                "[#2417 coverage]   {name} coverage {:.4}  mean half-width {:.5}",
5441                tally.coverage(),
5442                tally.mean_half_width()
5443            );
5444        }
5445    }
5446
5447    /// A single box bound with the truth half a standard error inside the
5448    /// feasible region: the regime where the constrained mode pins in about a
5449    /// third of replicates, so the active-face answer reports a ZERO-WIDTH
5450    /// interval a third of the time and cannot possibly cover.
5451    #[test]
5452    fn box_bound_at_half_a_standard_error_separates_the_three_covariances() {
5453        let n = 60;
5454        let mut rng = SplitMix64::new(20_417);
5455        let mut design = Array2::<f64>::zeros((n, 2));
5456        for i in 0..n {
5457            design[[i, 0]] = 1.0;
5458            design[[i, 1]] = rng.normal();
5459        }
5460        let gram = design.t().dot(&design);
5461        let noise_sd = 1.0;
5462        let covariance = gaussian_posterior_covariance(&gram, noise_sd * noise_sd);
5463        let standard_error = covariance[[1, 1]].sqrt();
5464        let truth = Array1::from_vec(vec![0.3, 0.5 * standard_error]);
5465        let constraints =
5466            LinearInequalityConstraints::new(ndarray::array![[0.0, 1.0]], ndarray::array![0.0])
5467                .expect("nonnegativity bound");
5468
5469        let cell = run_cell(&design, &truth, &constraints, 1, noise_sd, 4000, 91_137);
5470        report_cell("box bound, truth 0.5 se", &cell);
5471
5472        // The mode pins often enough that a zero-width interval is not a corner
5473        // case; if it stopped pinning the cell would stop testing anything.
5474        assert!(
5475            cell.pinned_fraction > 0.2,
5476            "the cell must actually exercise the boundary, pinned fraction {:.3}",
5477            cell.pinned_fraction
5478        );
5479        assert!(
5480            cell.active_face.coverage() < 0.80,
5481            "the active-face covariance must under-cover catastrophically here — it reports a \
5482             zero-width interval whenever the mode pins — but coverage was {:.4}",
5483            cell.active_face.coverage()
5484        );
5485        assert!(
5486            cell.truncated.coverage() >= NOMINAL_COVERAGE - 0.01,
5487            "the truncated covariance must reach nominal coverage, got {:.4}",
5488            cell.truncated.coverage()
5489        );
5490        assert!(
5491            cell.truncated_mean_centred.coverage() >= NOMINAL_COVERAGE - 0.01,
5492            "and it must still reach it once the centre moves to the truncated posterior \
5493             mean, got {:.4}",
5494            cell.truncated_mean_centred.coverage()
5495        );
5496        assert!(
5497            cell.truncated.mean_half_width() < 0.85 * cell.full_space.mean_half_width(),
5498            "the truncated covariance must buy its coverage with materially SHORTER intervals \
5499             than the full-space answer: {:.5} vs {:.5}",
5500            cell.truncated.mean_half_width(),
5501            cell.full_space.mean_half_width()
5502        );
5503        assert!(
5504            cell.full_space.coverage() >= NOMINAL_COVERAGE,
5505            "the full-space covariance over-covers by construction, got {:.4}",
5506            cell.full_space.coverage()
5507        );
5508    }
5509
5510    /// The truth pushed further from the bound, where the mode is pinned less
5511    /// often but is much further from the truth when it is. This cell is the
5512    /// counterexample to narrowing the covariance ALONE.
5513    ///
5514    /// Measured here: the truncated covariance around the constrained MODE
5515    /// covers 0.873 against a nominal 0.95 — worse than both the full-space
5516    /// answer (0.976) and the active face (0.910) — while the SAME covariance
5517    /// around the truncated posterior MEAN covers 0.966 at an interval 16%
5518    /// shorter than full space. Truncating the spread without moving the
5519    /// location keeps the interval centred on a point the posterior says is its
5520    /// least-likely feasible value, then makes it narrower. The two halves of
5521    /// the estimand are not separable, and this test exists so that fact cannot
5522    /// be lost: a covariance-only change is a REGRESSION here.
5523    #[test]
5524    fn narrowing_the_covariance_without_moving_the_mean_is_a_regression() {
5525        let n = 60;
5526        let mut rng = SplitMix64::new(31_417);
5527        let mut design = Array2::<f64>::zeros((n, 2));
5528        for i in 0..n {
5529            design[[i, 0]] = 1.0;
5530            design[[i, 1]] = rng.normal();
5531        }
5532        let gram = design.t().dot(&design);
5533        let noise_sd = 1.0;
5534        let covariance = gaussian_posterior_covariance(&gram, noise_sd * noise_sd);
5535        let standard_error = covariance[[1, 1]].sqrt();
5536        let truth = Array1::from_vec(vec![-0.2, 1.5 * standard_error]);
5537        let constraints =
5538            LinearInequalityConstraints::new(ndarray::array![[0.0, 1.0]], ndarray::array![0.0])
5539                .expect("nonnegativity bound");
5540
5541        let cell = run_cell(&design, &truth, &constraints, 1, noise_sd, 4000, 47_903);
5542        report_cell("box bound, truth 1.5 se", &cell);
5543
5544        assert!(
5545            cell.truncated.coverage() < NOMINAL_COVERAGE - 0.02,
5546            "this cell exists BECAUSE the mode-centred truncated interval under-covers here; \
5547             if it stopped doing so the counterexample would no longer be testing anything, \
5548             got {:.4}",
5549            cell.truncated.coverage()
5550        );
5551        assert!(
5552            cell.truncated.coverage() < cell.active_face.coverage(),
5553            "the point of the cell: narrowing the covariance while leaving the interval \
5554             centred on the mode is worse than the active-face answer it replaces, {:.4} vs \
5555             {:.4}",
5556            cell.truncated.coverage(),
5557            cell.active_face.coverage()
5558        );
5559        assert!(
5560            cell.truncated_mean_centred.coverage() >= NOMINAL_COVERAGE - 0.01,
5561            "moving the centre to the truncated posterior mean recovers nominal coverage with \
5562             the same covariance, got {:.4}",
5563            cell.truncated_mean_centred.coverage()
5564        );
5565        assert!(
5566            cell.truncated_mean_centred.mean_half_width() < cell.full_space.mean_half_width(),
5567            "and it does so with shorter intervals than the full-space answer: {:.5} vs {:.5}",
5568            cell.truncated_mean_centred.mean_half_width(),
5569            cell.full_space.mean_half_width()
5570        );
5571    }
5572
5573    /// Two coupled bounds, so the correction runs through the multivariate
5574    /// orthant cubature rather than the scalar closed form.
5575    #[test]
5576    fn two_coupled_bounds_exercise_the_orthant_cubature() {
5577        let n = 80;
5578        let mut rng = SplitMix64::new(74_211);
5579        let mut design = Array2::<f64>::zeros((n, 3));
5580        for i in 0..n {
5581            design[[i, 0]] = 1.0;
5582            let shared = rng.normal();
5583            design[[i, 1]] = shared;
5584            // Correlated with column 1, so the two bounds are coupled and the
5585            // constraint-normal covariance `W` is not diagonal.
5586            design[[i, 2]] = 0.7 * shared + 0.7 * rng.normal();
5587        }
5588        let gram = design.t().dot(&design);
5589        let noise_sd = 1.0;
5590        let covariance = gaussian_posterior_covariance(&gram, noise_sd * noise_sd);
5591        let truth = Array1::from_vec(vec![
5592            0.25,
5593            0.5 * covariance[[1, 1]].sqrt(),
5594            0.5 * covariance[[2, 2]].sqrt(),
5595        ]);
5596        let constraints = LinearInequalityConstraints::new(
5597            ndarray::array![[0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
5598            ndarray::array![0.0, 0.0],
5599        )
5600        .expect("two nonnegativity bounds");
5601
5602        let cell = run_cell(&design, &truth, &constraints, 1, noise_sd, 600, 55_301);
5603        report_cell("two coupled bounds, truth 0.5 se", &cell);
5604
5605        assert!(
5606            cell.active_face.coverage() < 0.85,
5607            "the active-face covariance must under-cover here too, got {:.4}",
5608            cell.active_face.coverage()
5609        );
5610        assert!(
5611            cell.truncated.coverage() >= NOMINAL_COVERAGE - 0.03,
5612            "the truncated covariance must reach nominal coverage through the orthant \
5613             cubature, got {:.4}",
5614            cell.truncated.coverage()
5615        );
5616        assert!(
5617            cell.truncated.mean_half_width() < cell.full_space.mean_half_width(),
5618            "shorter intervals at nominal coverage: {:.5} vs {:.5}",
5619            cell.truncated.mean_half_width(),
5620            cell.full_space.mean_half_width()
5621        );
5622    }
5623
5624}
5625
5626
5627#[cfg(test)]
5628mod affine_ceiling_tests {
5629    use super::*;
5630    use ndarray::array;
5631
5632    /// Run the cubature over `{u >= 0}` intersected with an optional affine
5633    /// wall, and return the log mass.
5634    fn log_mass(
5635        mean: &Array1<f64>,
5636        sd: &Array1<f64>,
5637        ceiling: Option<&StandardizedCeiling>,
5638        upper: &[f64],
5639        nodes: usize,
5640    ) -> Option<f64> {
5641        let q = mean.len();
5642        let mut factor = Array2::<f64>::zeros((q, q));
5643        for i in 0..q {
5644            factor[[i, i]] = sd[i];
5645        }
5646        let generator = kronecker_generator(q);
5647        let mut accumulator = OrthantAccumulator::new(q);
5648        accumulate_orthant_nodes(
5649            &mut accumulator,
5650            mean,
5651            upper,
5652            ceiling,
5653            factor.view(),
5654            &generator,
5655            None,
5656            0,
5657            0,
5658            nodes,
5659        )
5660        .expect("cubature");
5661        // The accumulator carries its scale separately so a face spanning
5662        // hundreds of decades never underflows; the mass is that scale times the
5663        // accumulated sum, averaged over the nodes evaluated.
5664        if !(accumulator.weight_sum.is_finite() && accumulator.weight_sum > 0.0) {
5665            return None;
5666        }
5667        Some(accumulator.log_scale + accumulator.weight_sum.ln() - (nodes as f64).ln())
5668    }
5669
5670    fn diagonal_factor(sd: &Array1<f64>) -> Array2<f64> {
5671        let q = sd.len();
5672        let mut factor = Array2::<f64>::zeros((q, q));
5673        for i in 0..q {
5674            factor[[i, i]] = sd[i];
5675        }
5676        factor
5677    }
5678
5679    #[test]
5680    fn a_coordinate_normal_reproduces_the_box_it_is_the_degenerate_case_of() {
5681        // The box ceiling and an affine wall whose normal is a single
5682        // coordinate describe the SAME region. They are computed differently on
5683        // purpose — the box keeps its subtraction-free width — so this asserts
5684        // they agree, which is the property that lets one arithmetic path serve
5685        // both.
5686        let mean = array![0.35, -0.20];
5687        let sd = array![1.0, 0.8];
5688        let factor = diagonal_factor(&sd);
5689        let width = 1.4;
5690
5691        let boxed = log_mass(&mean, &sd, None, &[width, f64::INFINITY], 1 << 14)
5692            .expect("box mass");
5693        let wall = StandardizedCeiling::new(&array![1.0, 0.0], width, &mean, factor.view())
5694            .expect("coordinate wall");
5695        let affine = log_mass(
5696            &mean,
5697            &sd,
5698            Some(&wall),
5699            &[f64::INFINITY, f64::INFINITY],
5700            1 << 14,
5701        )
5702        .expect("affine mass");
5703        assert_eq!(wall.pivot, 0, "a normal touching only coordinate 0 pivots there");
5704        assert!(
5705            (boxed - affine).abs() < 1e-12,
5706            "box {boxed:.15} and affine {affine:.15} describe the same region"
5707        );
5708    }
5709
5710    #[test]
5711    fn an_affine_ceiling_removes_the_mass_it_should() {
5712        // Region: {u >= 0} ∩ {u0 + u1 <= c}, a triangle. With independent
5713        // coordinates the answer is a one-dimensional integral, which Simpson
5714        // resolves to far better than the cubature's own tolerance — an
5715        // independent reference rather than a second run of the same rule.
5716        let mean = array![0.4, -0.3];
5717        let sd = array![0.9, 1.1];
5718        let factor = diagonal_factor(&sd);
5719        let bound = 1.6;
5720        let wall = StandardizedCeiling::new(&array![1.0, 1.0], bound, &mean, factor.view())
5721            .expect("sum wall");
5722        assert_eq!(wall.pivot, 1, "a wall touching both coordinates pivots on the last");
5723
5724        let got = log_mass(
5725            &mean,
5726            &sd,
5727            Some(&wall),
5728            &[f64::INFINITY, f64::INFINITY],
5729            1 << 16,
5730        )
5731        .expect("triangle mass");
5732
5733        // Simpson over u0 in [0, bound] of  φ((u0−m0)/s0)/s0 · P(0 ≤ u1 ≤ bound−u0)
5734        let panels = 4000usize;
5735        let step = bound / panels as f64;
5736        let density = |x: f64| {
5737            let z = (x - mean[0]) / sd[0];
5738            (-0.5 * z * z).exp() / (sd[0] * (2.0 * std::f64::consts::PI).sqrt())
5739        };
5740        let inner = |x: f64| {
5741            let hi = (bound - x - mean[1]) / sd[1];
5742            let lo = -mean[1] / sd[1];
5743            if hi <= lo {
5744                0.0
5745            } else {
5746                normal_cdf(hi) - normal_cdf(lo)
5747            }
5748        };
5749        let integrand = |x: f64| density(x) * inner(x);
5750        let mut total = integrand(0.0) + integrand(bound);
5751        for k in 1..panels {
5752            let x = k as f64 * step;
5753            total += integrand(x) * if k % 2 == 0 { 2.0 } else { 4.0 };
5754        }
5755        let reference = (total * step / 3.0).ln();
5756        assert!(
5757            (got - reference).abs() < 5e-4,
5758            "cubature {got:.12} against the Simpson reference {reference:.12}"
5759        );
5760
5761        // ... and the wall must actually bite: the same region without it is
5762        // strictly larger. A ceiling that changed nothing would pass the check
5763        // above just as well.
5764        let unbounded = log_mass(
5765            &mean,
5766            &sd,
5767            None,
5768            &[f64::INFINITY, f64::INFINITY],
5769            1 << 16,
5770        )
5771        .expect("unbounded mass");
5772        assert!(
5773            unbounded > got + 0.05,
5774            "the wall removed {:.4} nats, which is not enough to call it active",
5775            unbounded - got
5776        );
5777    }
5778
5779    #[test]
5780    fn a_wall_that_crosses_the_orthant_leaves_no_mass() {
5781        // `u0 + u1 <= -1` is disjoint from the closed orthant, so every node's
5782        // interval is empty and the cubature reports no feasible mass rather
5783        // than a small wrong one.
5784        let mean = array![0.2, 0.1];
5785        let sd = array![1.0, 1.0];
5786        let factor = diagonal_factor(&sd);
5787        let wall = StandardizedCeiling::new(&array![1.0, 1.0], -1.0, &mean, factor.view())
5788            .expect("infeasible wall");
5789        assert!(
5790            log_mass(&mean, &sd, Some(&wall), &[f64::INFINITY, f64::INFINITY], 1 << 10).is_none(),
5791            "an empty region reports no mass"
5792        );
5793    }
5794
5795    #[test]
5796    fn a_vanishing_normal_is_refused_rather_than_pivoted_arbitrarily() {
5797        let mean = array![0.0, 0.0];
5798        let factor = diagonal_factor(&array![1.0, 1.0]);
5799        let message = StandardizedCeiling::new(&array![0.0, 0.0], 1.0, &mean, factor.view())
5800            .expect_err("a zero normal constrains nothing");
5801        assert!(
5802            message.contains("vanished"),
5803            "the refusal must say the normal vanished, got: {message}"
5804        );
5805        let mismatched = StandardizedCeiling::new(&array![1.0], 1.0, &mean, factor.view())
5806            .expect_err("a normal of the wrong length is refused");
5807        assert!(mismatched.contains("length"), "got: {mismatched}");
5808    }
5809
5810    #[test]
5811    fn the_pivot_follows_the_factor_not_just_the_normal() {
5812        // `Lᵀa` is what decides the pivot, so a normal touching only coordinate
5813        // 0 can still reach earlier coordinates through a dense factor — but
5814        // never a LATER one, because L is lower triangular. This pins the
5815        // direction of the transform, which a transpose slip would invert.
5816        let mean = array![0.0, 0.0, 0.0];
5817        let mut factor = Array2::<f64>::zeros((3, 3));
5818        factor[[0, 0]] = 1.0;
5819        factor[[1, 0]] = 0.7;
5820        factor[[1, 1]] = 1.0;
5821        factor[[2, 0]] = 0.3;
5822        factor[[2, 1]] = 0.4;
5823        factor[[2, 2]] = 1.0;
5824        let wall = StandardizedCeiling::new(&array![0.0, 0.0, 1.0], 2.0, &mean, factor.view())
5825            .expect("last-coordinate normal");
5826        assert_eq!(wall.pivot, 2);
5827        let early = StandardizedCeiling::new(&array![1.0, 0.0, 0.0], 2.0, &mean, factor.view())
5828            .expect("first-coordinate normal");
5829        assert_eq!(
5830            early.pivot, 0,
5831            "a normal on coordinate 0 cannot reach a later coordinate through a lower-triangular factor"
5832        );
5833    }
5834}
5835
5836
5837#[cfg(test)]
5838mod projection_law_2446_tests {
5839    use super::*;
5840    use ndarray::array;
5841
5842    /// The integrand a locscale response-moment consumer actually evaluates:
5843    /// a survival probability read off an inverse link at `eta0 + w`. Smooth,
5844    /// bounded, and monotone, so nothing about the comparison below depends on
5845    /// picking an integrand that flatters the mixture.
5846    fn integrand(w: f64) -> f64 {
5847        1.0 / (1.0 + (-0.5 + w).exp())
5848    }
5849
5850    /// Composite Simpson on `[0, upper]`, odd `points`.
5851    fn simpson<F: Fn(f64) -> f64>(lower: f64, upper: f64, points: usize, f: F) -> f64 {
5852        assert!(points % 2 == 1, "Simpson needs an odd point count");
5853        let h = (upper - lower) / ((points - 1) as f64);
5854        let mut total = 0.0;
5855        for index in 0..points {
5856            let weight = if index == 0 || index == points - 1 {
5857                1.0
5858            } else if index % 2 == 1 {
5859                4.0
5860            } else {
5861                2.0
5862            };
5863            total += weight * f(lower + h * (index as f64));
5864        }
5865        total * h / 3.0
5866    }
5867
5868    /// `E[f(cᵀβ)]` under `N(center, covariance)` restricted to `β ≥ 0`, for a
5869    /// two-dimensional fixture, by tensor Simpson on the exact density. This is
5870    /// the reference: it never calls the cubature under test, and its own error
5871    /// is `O(h⁴)` at `h ≈ 3e-3`.
5872    fn exact_orthant_expectation(
5873        center: &Array1<f64>,
5874        covariance: &Array2<f64>,
5875        contrast: &Array1<f64>,
5876    ) -> f64 {
5877        exact_orthant_expectation_of(center, covariance, contrast, integrand)
5878    }
5879
5880    /// Same reference for an arbitrary scalar functional of `cᵀβ`.
5881    fn exact_orthant_expectation_of<F: Fn(f64) -> f64>(
5882        center: &Array1<f64>,
5883        covariance: &Array2<f64>,
5884        contrast: &Array1<f64>,
5885        functional: F,
5886    ) -> f64 {
5887        let det = covariance[[0, 0]] * covariance[[1, 1]] - covariance[[0, 1]] * covariance[[1, 0]];
5888        let inverse = array![
5889            [covariance[[1, 1]] / det, -covariance[[0, 1]] / det],
5890            [-covariance[[1, 0]] / det, covariance[[0, 0]] / det]
5891        ];
5892        let density = |b0: f64, b1: f64| -> f64 {
5893            let d0 = b0 - center[0];
5894            let d1 = b1 - center[1];
5895            let quadratic = inverse[[0, 0]] * d0 * d0
5896                + 2.0 * inverse[[0, 1]] * d0 * d1
5897                + inverse[[1, 1]] * d1 * d1;
5898            (-0.5 * quadratic).exp()
5899        };
5900        // 12 sd past the wall in each coordinate leaves `exp(-72)` of the mass
5901        // outside the box, which is below the reference's own quadrature error
5902        // by more than twenty orders.
5903        let upper0 = center[0].max(0.0) + 12.0 * covariance[[0, 0]].sqrt();
5904        let upper1 = center[1].max(0.0) + 12.0 * covariance[[1, 1]].sqrt();
5905        let points = 2001;
5906        let mass = simpson(0.0, upper0, points, |b0| {
5907            simpson(0.0, upper1, points, |b1| density(b0, b1))
5908        });
5909        let weighted = simpson(0.0, upper0, points, |b0| {
5910            simpson(0.0, upper1, points, |b1| {
5911                density(b0, b1) * functional(contrast[0] * b0 + contrast[1] * b1)
5912            })
5913        });
5914        weighted / mass
5915    }
5916
5917    /// `E[f(w)]` under `N(mean, variance)` on the WHOLE line — the law the
5918    /// locscale warp integral ships today: the normal carrying the constrained
5919    /// posterior's first two moments.
5920    fn normal_expectation(mean: f64, variance: f64) -> f64 {
5921        let sd = variance.sqrt();
5922        let points = 4001;
5923        simpson(mean - 12.0 * sd, mean + 12.0 * sd, points, |w| {
5924            let z = (w - mean) / sd;
5925            (-0.5 * z * z).exp() * integrand(w)
5926        }) / (sd * (2.0 * std::f64::consts::PI).sqrt())
5927    }
5928
5929    /// Mass a moment-matched normal puts on `w < 0` — warps a non-negative
5930    /// basis over a non-negative coefficient block cannot produce.
5931    fn normal_infeasible_mass(mean: f64, variance: f64) -> f64 {
5932        let sd = variance.sqrt();
5933        let lower = mean - 12.0 * sd;
5934        if lower >= 0.0 {
5935            return 0.0;
5936        }
5937        simpson(lower, 0.0, 4001, |w| {
5938            let z = (w - mean) / sd;
5939            (-0.5 * z * z).exp()
5940        }) / (sd * (2.0 * std::f64::consts::PI).sqrt())
5941    }
5942
5943    /// #2446: the pushforward of a cone-truncated joint through a contrast is
5944    /// NOT a normal, so reporting it as the normal with the right first two
5945    /// moments has an error FLOOR. The mixture this module's own cubature
5946    /// produces has an error RATE instead — and, unlike the normal, it puts no
5947    /// mass on infeasible warps.
5948    ///
5949    /// The fixture is the `q = 2` near-wall case: both coefficients are
5950    /// constrained non-negative (`A = I`), correlated, with the ambient centre
5951    /// straddling the wall. The contrast is non-negative, as an I-spline basis
5952    /// row is, so `w = bᵀβ ≥ 0` is exactly the feasible image.
5953    ///
5954    /// Measured here, against a reference that is a tensor Simpson rule on the
5955    /// exact truncated density and calls none of the code under test.
5956    #[test]
5957    fn projection_law_beats_the_moment_matched_normal_on_a_two_row_cone_2446() {
5958        let ambient = array![[0.40, 0.24], [0.24, 0.36]];
5959        let center = array![0.05, -0.10];
5960        let contrast = array![0.70, 0.30];
5961        let constraints =
5962            LinearInequalityConstraints::new(array![[1.0, 0.0], [0.0, 1.0]], array![0.0, 0.0])
5963                .expect("build the two-row non-negativity cone");
5964        let correction =
5965            constrained_posterior_correction_from_covariance(&ambient, &center, &constraints)
5966                .expect("the correction is computable on this face")
5967                .expect("a centre straddling both walls must retain the face");
5968        // The retention walk orders rows by slack, so compare the SET.
5969        let mut retained = correction.rows.clone();
5970        retained.sort_unstable();
5971        assert_eq!(
5972            retained,
5973            vec![0, 1],
5974            "the fixture must retain BOTH rows; a one-row face is the closed-form case and \
5975             would measure nothing about the pushforward"
5976        );
5977        let geometry = ConstrainedPosteriorGeometry {
5978            constraints,
5979            mode: array![0.0, 0.0],
5980            unconstrained_center: Some(center.clone()),
5981            correction: Some(correction.clone()),
5982            moment_status: ConstrainedPosteriorMomentStatus::Available,
5983        };
5984
5985        // (e) the node mixture, from the SHIPPED cubature.
5986        let law = constrained_projection_law(&ambient, &geometry, &contrast)
5987            .expect("projection law on a retained face");
5988        assert!(
5989            law.residual_variance <= 1e-12 * ambient[[0, 0]],
5990            "with `A = I` the contrast is carried entirely by the constraint normals, so the \
5991             tangent component must vanish and the mixture must BE the whole law; got residual \
5992             variance {:.3e}",
5993            law.residual_variance
5994        );
5995        let weight_sum = law.nodes.iter().map(|(_, weight)| weight).sum::<f64>();
5996        assert!(
5997            (weight_sum - 1.0).abs() < 1e-9,
5998            "node weights must be normalized, got {weight_sum:.12e}"
5999        );
6000        let infeasible_nodes = law
6001            .nodes
6002            .iter()
6003            .filter(|(location, _)| *location < 0.0)
6004            .count();
6005        assert_eq!(
6006            infeasible_nodes, 0,
6007            "every cubature node is a point of the retained orthant, so no node may carry a \
6008             negative warp"
6009        );
6010        let node_sum = law
6011            .nodes
6012            .iter()
6013            .map(|(location, weight)| weight * integrand(*location))
6014            .sum::<f64>();
6015
6016        // (c) the moment-matched normal: exactly what the locscale warp
6017        // integral evaluates today.
6018        let posterior_mean = contrast.dot(&correction.posterior_mean(&center));
6019        let corrected = correction.apply_to_covariance(&ambient);
6020        let posterior_variance = contrast.dot(&corrected.dot(&contrast));
6021        let normal_value = normal_expectation(posterior_mean, posterior_variance);
6022        let infeasible_mass = normal_infeasible_mass(posterior_mean, posterior_variance);
6023
6024        let reference = exact_orthant_expectation(&center, &ambient, &contrast);
6025        let node_error = (node_sum - reference).abs();
6026        let normal_error = (normal_value - reference).abs();
6027        eprintln!(
6028            "[2446] nodes={} reference={reference:.12e} node_sum={node_sum:.12e} \
6029             (err {node_error:.3e}) normal={normal_value:.12e} (err {normal_error:.3e}) \
6030             normal_infeasible_mass={infeasible_mass:.4e} \
6031             posterior_mean={posterior_mean:.9e} law_mean={:.9e} \
6032             posterior_variance={posterior_variance:.9e} law_variance={:.9e}",
6033            law.nodes.len(),
6034            law.mean(),
6035            law.variance()
6036        );
6037
6038        // The mixture must be the SAME object the module's moments describe,
6039        // not a second estimand that happens to be nearby. Both moments are
6040        // formed from the same cubature, so they agree to its own certified
6041        // tolerance and no better -- asserting tighter would be asserting
6042        // against `ORTHANT_MOMENT_RELATIVE_TOLERANCE` rather than against the
6043        // law.
6044        let moment_tolerance = 4.0 * ORTHANT_MOMENT_RELATIVE_TOLERANCE;
6045        assert!(
6046            (law.mean() - posterior_mean).abs()
6047                <= moment_tolerance * posterior_variance.sqrt(),
6048            "the node mixture's mean must be the reported posterior mean: law {:.9e} vs \
6049             reported {posterior_mean:.9e}",
6050            law.mean()
6051        );
6052        assert!(
6053            (law.variance() - posterior_variance).abs()
6054                <= moment_tolerance * posterior_variance,
6055            "the node mixture's variance must be the reported posterior variance: law {:.9e} \
6056             vs reported {posterior_variance:.9e}",
6057            law.variance()
6058        );
6059
6060        // The fixture must actually exercise the defect this issue is about.
6061        assert!(
6062            infeasible_mass > 1.0e-2,
6063            "the moment-matched normal must put a non-trivial share of its mass on warps the \
6064             cone excludes, or this fixture measures nothing; got {infeasible_mass:.3e}"
6065        );
6066        // The mixture agrees with the exact pushforward, which is the claim the
6067        // moment-matched normal cannot make no matter how well its moments are
6068        // computed: its error is a floor set by asserting normality, not a rate.
6069        assert!(
6070            node_error < 1.0e-4,
6071            "the node mixture must agree with the exact pushforward: reference \
6072             {reference:.12e}, node sum {node_sum:.12e} (error {node_error:.3e}); the \
6073             moment-matched normal is at {normal_value:.12e} (error {normal_error:.3e}) with \
6074             {infeasible_mass:.3e} of its mass on w < 0"
6075        );
6076    }
6077
6078    /// #2679: the same claim for the JOINT rule, on a fixture whose contrast is
6079    /// NOT carried entirely by the constraint normals.
6080    ///
6081    /// The law is `x = cᵀu + s·t` with `u` the two-row truncated cone above and
6082    /// `t` an independent standard normal — the exact structure a response
6083    /// moment sees, where the cone moves the whole coefficient vector and the
6084    /// tangent adds the part of the predictor's variance the constraint normals
6085    /// do not carry.
6086    ///
6087    /// The reference folds the tangent into the functional analytically-in-form
6088    /// (`g(x) = E_t[f(x + s·t)]`, a 1-D Gaussian Simpson) and then integrates
6089    /// `g` against the exact truncated density by the SAME tensor Simpson rule
6090    /// the #2446 test uses. Neither half calls the cubature under test.
6091    ///
6092    /// Three separate properties are asserted, because the rule has to have all
6093    /// three before it can price a moment on a predict path:
6094    ///
6095    /// * **support** — every point is feasible, exactly, not to a tolerance;
6096    /// * **tangent measure** — the tangent block really is standard normal
6097    ///   under the SOV importance weights, which is free to fail (the weights
6098    ///   are a function of the constraint-normal coordinates of the same
6099    ///   lattice point, so an aliased generator would correlate them);
6100    /// * **accuracy** — against the density reference, and by a wide margin
6101    ///   over the moment-matched normal that is what ships today.
6102    #[test]
6103    fn joint_cubature_carries_the_tangent_block_at_a_bounded_point_count_2679() {
6104        let ambient = array![[0.40, 0.24], [0.24, 0.36]];
6105        let center = array![0.05, -0.10];
6106        let contrast = array![0.70, 0.30];
6107        // Tangent share of the predictor. Large enough that a rule which simply
6108        // dropped the tangent would miss by far more than the bound below.
6109        let tangent_sd = 0.6_f64;
6110        let constraints =
6111            LinearInequalityConstraints::new(array![[1.0, 0.0], [0.0, 1.0]], array![0.0, 0.0])
6112                .expect("build the two-row non-negativity cone");
6113        let correction =
6114            constrained_posterior_correction_from_covariance(&ambient, &center, &constraints)
6115                .expect("the correction is computable on this face")
6116                .expect("a centre straddling both walls must retain the face");
6117        let mut retained = correction.rows.clone();
6118        retained.sort_unstable();
6119        assert_eq!(
6120            retained,
6121            vec![0, 1],
6122            "the fixture must retain BOTH rows or the pushforward is the closed-form case"
6123        );
6124
6125        // `A = I`, so the constraint-normal coordinate IS the coefficient
6126        // vector and `W = Σ`, `E_untrunc[u] = centre`.
6127        let upper_limits = correction.upper_limits();
6128        let normal_center = Array1::from_vec(
6129            correction
6130                .rows
6131                .iter()
6132                .map(|&row| center[row])
6133                .collect::<Vec<_>>(),
6134        );
6135        let normal_covariance = {
6136            let mut out = Array2::<f64>::zeros((2, 2));
6137            for (i, &row_i) in correction.rows.iter().enumerate() {
6138                for (j, &row_j) in correction.rows.iter().enumerate() {
6139                    out[[i, j]] = ambient[[row_i, row_j]];
6140                }
6141            }
6142            out
6143        };
6144        let lift_contrast = Array1::from_vec(
6145            correction
6146                .rows
6147                .iter()
6148                .map(|&row| contrast[row])
6149                .collect::<Vec<_>>(),
6150        );
6151
6152        const POINTS: usize = 1 << 13;
6153        let joint = constrained_posterior_joint_cubature(
6154            &normal_center,
6155            &normal_covariance,
6156            &upper_limits,
6157            1,
6158            POINTS,
6159        )
6160        .expect("joint cubature on a retained two-row face");
6161        assert_eq!(
6162            joint.len(),
6163            POINTS,
6164            "the joint rule's cost is the point count it was asked for and nothing else"
6165        );
6166
6167        // (a) support. Exact, not to a tolerance: the SOV map cannot produce an
6168        // infeasible point, so any tolerance here would be hiding a bug.
6169        let infeasible_points = joint
6170            .iter()
6171            .filter(|point| point.normal_coordinates.iter().any(|&value| value < 0.0))
6172            .count();
6173        assert_eq!(
6174            infeasible_points, 0,
6175            "every joint point must lie in the retained cone; {infeasible_points} of {POINTS} did \
6176             not"
6177        );
6178
6179        let weight_sum = joint.iter().map(|point| point.weight).sum::<f64>();
6180        assert!(
6181            (weight_sum - 1.0).abs() < 1e-9,
6182            "joint weights must be normalized, got {weight_sum:.12e}"
6183        );
6184
6185        // (b) the tangent block is standard normal under the SAME weights.
6186        let tangent_mean = joint
6187            .iter()
6188            .map(|point| point.weight * point.tangent[0])
6189            .sum::<f64>();
6190        let tangent_second = joint
6191            .iter()
6192            .map(|point| point.weight * point.tangent[0] * point.tangent[0])
6193            .sum::<f64>();
6194        eprintln!(
6195            "[2679] points={POINTS} tangent_mean={tangent_mean:.6e} \
6196             tangent_second={tangent_second:.6e}"
6197        );
6198        assert!(
6199            tangent_mean.abs() < 2.0e-2,
6200            "the tangent block must integrate to a zero mean under the SOV weights, got \
6201             {tangent_mean:.6e}"
6202        );
6203        assert!(
6204            (tangent_second - 1.0).abs() < 5.0e-2,
6205            "the tangent block must integrate to unit variance under the SOV weights, got \
6206             {tangent_second:.6e}"
6207        );
6208
6209        let joint_value = joint
6210            .iter()
6211            .map(|point| {
6212                let normal_part = lift_contrast.dot(&point.normal_coordinates);
6213                point.weight * integrand(normal_part + tangent_sd * point.tangent[0])
6214            })
6215            .sum::<f64>();
6216
6217        // (c) accuracy against a reference built from the density.
6218        let convolved = |x: f64| -> f64 {
6219            simpson(
6220                x - 12.0 * tangent_sd,
6221                x + 12.0 * tangent_sd,
6222                4001,
6223                |value| {
6224                    let z = (value - x) / tangent_sd;
6225                    (-0.5 * z * z).exp() * integrand(value)
6226                },
6227            ) / (tangent_sd * (2.0 * std::f64::consts::PI).sqrt())
6228        };
6229        let reference = exact_orthant_expectation_of(&center, &ambient, &contrast, convolved);
6230
6231        // What ships today: the moment-matched normal, with the tangent's
6232        // variance folded into it — which is exactly how the locscale rule
6233        // treats the same decomposition.
6234        let posterior_mean = contrast.dot(&correction.posterior_mean(&center));
6235        let corrected = correction.apply_to_covariance(&ambient);
6236        let posterior_variance =
6237            contrast.dot(&corrected.dot(&contrast)) + tangent_sd * tangent_sd;
6238        let normal_value = normal_expectation(posterior_mean, posterior_variance);
6239
6240        let joint_error = (joint_value - reference).abs();
6241        let normal_error = (normal_value - reference).abs();
6242        eprintln!(
6243            "[2679] reference={reference:.12e} joint={joint_value:.12e} (err {joint_error:.3e}) \
6244             normal={normal_value:.12e} (err {normal_error:.3e})"
6245        );
6246        assert!(
6247            normal_error > 1.0e-4,
6248            "the fixture must leave the moment-matched normal measurably wrong, or the \
6249             comparison below is vacuous; got {normal_error:.3e}"
6250        );
6251        assert!(
6252            joint_error < 0.2 * normal_error,
6253            "the joint rule must be decisively closer to the exact pushforward than the \
6254             moment-matched normal: joint error {joint_error:.3e} vs normal error \
6255             {normal_error:.3e} against reference {reference:.12e}"
6256        );
6257    }
6258}