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