Skip to main content

gam_models/gamlss/gaussian/
wiggle.rs

1// Real concern-organized submodule of the gamlss family stack.
2// Cross-module items are re-exported flat through the parent (`gamlss.rs`),
3// so `use super::*;` makes the sibling-concern symbols this module references
4// resolve through the parent namespace.
5use super::*;
6
7pub(crate) struct GaussianLocationScaleWiggleGeometry {
8    pub(crate) basis: Array2<f64>,
9    pub(crate) basis_d1: Array2<f64>,
10    pub(crate) basis_d2: Array2<f64>,
11    pub(crate) basis_d3: Array2<f64>,
12    pub(crate) dq_dq0: Array1<f64>,
13    pub(crate) d2q_dq02: Array1<f64>,
14    pub(crate) d3q_dq03: Array1<f64>,
15    pub(crate) d4q_dq04: Array1<f64>,
16}
17
18/// Per-row pieces of the 3-block Gaussian location-scale-wiggle joint
19/// Hessian. Both the dense path and the matrix-free workspace share these
20/// row coefficients; only the assembly differs.
21pub(crate) struct GaussianLocationScaleWiggleHessianRowPieces {
22    pub(crate) coeff_mm: Array1<f64>,
23    pub(crate) coeff_ml: Array1<f64>,
24    pub(crate) coeff_ll: Array1<f64>,
25    pub(crate) coeff_mw_b: Array1<f64>,
26    pub(crate) coeff_mw_d: Array1<f64>,
27    pub(crate) coeff_lw_b: Array1<f64>,
28    pub(crate) coeff_ww: Array1<f64>,
29    pub(crate) basis: Array2<f64>,
30    pub(crate) basis_d1: Array2<f64>,
31}
32
33impl GaussianLocationScaleWiggleHessianRowPieces {
34    pub(crate) fn assemble_dense(
35        &self,
36        xmu: &Array2<f64>,
37        x_ls: &Array2<f64>,
38    ) -> Result<Array2<f64>, String> {
39        let h_mm = xt_diag_x_dense(xmu, &self.coeff_mm)?;
40        let h_ml = xt_diag_y_dense(xmu, &self.coeff_ml, x_ls)?;
41        let h_ll = xt_diag_x_dense(x_ls, &self.coeff_ll)?;
42        let h_mw = xt_diag_y_dense(xmu, &self.coeff_mw_b, &self.basis)?
43            + &xt_diag_y_dense(xmu, &self.coeff_mw_d, &self.basis_d1)?;
44        let h_lw = xt_diag_y_dense(x_ls, &self.coeff_lw_b, &self.basis)?;
45        let h_ww = xt_diag_x_dense(&self.basis, &self.coeff_ww)?;
46        Ok(gaussian_pack_wiggle_joint_symmetrichessian(
47            &h_mm, &h_ml, &h_mw, &h_ll, &h_lw, &h_ww,
48        ))
49    }
50}
51
52pub struct GaussianLocationScaleWiggleFamily {
53    pub y: Array1<f64>,
54    pub weights: Array1<f64>,
55    pub mu_design: Option<DesignMatrix>,
56    pub log_sigma_design: Option<DesignMatrix>,
57    pub wiggle_knots: Array1<f64>,
58    pub wiggle_degree: usize,
59    /// Resource policy threaded into PsiDesignMap construction (and any other
60    /// per-call materialization decision) made during exact-Newton joint psi
61    /// derivative evaluation. Defaults to `ResourcePolicy::default_library()`
62    /// when the family is built without an explicit policy.
63    pub policy: gam_runtime::resource::ResourcePolicy,
64    pub(crate) cached_row_scalars:
65        std::sync::RwLock<Option<(f64, f64, f64, f64, f64, f64, Arc<GaussianJointRowScalars>)>>,
66}
67
68impl Clone for GaussianLocationScaleWiggleFamily {
69    fn clone(&self) -> Self {
70        Self {
71            y: self.y.clone(),
72            weights: self.weights.clone(),
73            mu_design: self.mu_design.clone(),
74            log_sigma_design: self.log_sigma_design.clone(),
75            wiggle_knots: self.wiggle_knots.clone(),
76            wiggle_degree: self.wiggle_degree,
77            policy: self.policy.clone(),
78            cached_row_scalars: std::sync::RwLock::new(
79                self.cached_row_scalars
80                    .read()
81                    .expect("lock poisoned")
82                    .clone(),
83            ),
84        }
85    }
86}
87
88impl GaussianLocationScaleWiggleFamily {
89    pub const BLOCK_MU: usize = 0;
90    pub const BLOCK_LOG_SIGMA: usize = 1;
91    pub const BLOCK_WIGGLE: usize = 2;
92
93    pub fn parameternames() -> &'static [&'static str] {
94        &["mu", "log_sigma", "wiggle"]
95    }
96
97    pub fn parameter_links() -> &'static [ParameterLink] {
98        &[
99            ParameterLink::Identity,
100            ParameterLink::Log,
101            ParameterLink::Wiggle,
102        ]
103    }
104
105    pub fn metadata() -> FamilyMetadata {
106        FamilyMetadata {
107            name: "gaussian_location_scalewiggle",
108            parameternames: Self::parameternames(),
109            parameter_links: Self::parameter_links(),
110        }
111    }
112
113    pub(crate) fn exact_joint_supported(&self) -> bool {
114        self.mu_design.is_some() && self.log_sigma_design.is_some()
115    }
116
117    pub(crate) fn wiggle_basiswith_options(
118        &self,
119        q0: ArrayView1<'_, f64>,
120        options: BasisOptions,
121    ) -> Result<Array2<f64>, String> {
122        monotone_wiggle_basis_with_derivative_order(
123            q0,
124            &self.wiggle_knots,
125            self.wiggle_degree,
126            options.derivative_order,
127        )
128    }
129
130    pub(crate) fn wiggle_design(&self, q0: ArrayView1<'_, f64>) -> Result<Array2<f64>, String> {
131        self.wiggle_basiswith_options(q0, BasisOptions::value())
132    }
133
134    pub(crate) fn wiggle_dq_dq0(
135        &self,
136        q0: ArrayView1<'_, f64>,
137        beta_link_wiggle: ArrayView1<'_, f64>,
138    ) -> Result<Array1<f64>, String> {
139        let d1 = self.wiggle_basiswith_options(q0, BasisOptions::first_derivative())?;
140        if d1.ncols() != beta_link_wiggle.len() {
141            return Err(GamlssError::DimensionMismatch { reason: format!(
142                "wiggle derivative/beta mismatch: basis has {} columns but beta_link_wiggle has {} coefficients",
143                d1.ncols(),
144                beta_link_wiggle.len()
145            ) }.into());
146        }
147        Ok(d1.dot(&beta_link_wiggle) + 1.0)
148    }
149
150    pub(crate) fn wiggle_d2q_dq02(
151        &self,
152        q0: ArrayView1<'_, f64>,
153        beta_link_wiggle: ArrayView1<'_, f64>,
154    ) -> Result<Array1<f64>, String> {
155        let d2 = self.wiggle_basiswith_options(q0, BasisOptions::second_derivative())?;
156        if d2.ncols() != beta_link_wiggle.len() {
157            return Err(GamlssError::DimensionMismatch { reason: format!(
158                "wiggle second-derivative/beta mismatch: basis has {} columns but beta_link_wiggle has {} coefficients",
159                d2.ncols(),
160                beta_link_wiggle.len()
161            ) }.into());
162        }
163        Ok(d2.dot(&beta_link_wiggle))
164    }
165
166    pub(crate) fn wiggle_d3basis_constrained(
167        &self,
168        q0: ArrayView1<'_, f64>,
169    ) -> Result<Array2<f64>, String> {
170        monotone_wiggle_basis_with_derivative_order(q0, &self.wiggle_knots, self.wiggle_degree, 3)
171    }
172
173    pub(crate) fn wiggle_d3q_dq03(
174        &self,
175        q0: ArrayView1<'_, f64>,
176        beta_link_wiggle: ArrayView1<'_, f64>,
177    ) -> Result<Array1<f64>, String> {
178        let d3 = self.wiggle_d3basis_constrained(q0)?;
179        if d3.ncols() != beta_link_wiggle.len() {
180            return Err(GamlssError::DimensionMismatch { reason: format!(
181                "wiggle third-derivative/beta mismatch: basis has {} columns but beta_link_wiggle has {} coefficients",
182                d3.ncols(),
183                beta_link_wiggle.len()
184            ) }.into());
185        }
186        Ok(d3.dot(&beta_link_wiggle))
187    }
188
189    pub(crate) fn wiggle_d4q_dq04(
190        &self,
191        q0: ArrayView1<'_, f64>,
192        beta_link_wiggle: ArrayView1<'_, f64>,
193    ) -> Result<Array1<f64>, String> {
194        let d4 = monotone_wiggle_basis_with_derivative_order(
195            q0,
196            &self.wiggle_knots,
197            self.wiggle_degree,
198            4,
199        )?;
200        if d4.ncols() != beta_link_wiggle.len() {
201            return Err(GamlssError::DimensionMismatch { reason: format!(
202                "wiggle fourth-derivative/beta mismatch: basis has {} columns but beta_link_wiggle has {} coefficients",
203                d4.ncols(),
204                beta_link_wiggle.len()
205            ) }.into());
206        }
207        Ok(d4.dot(&beta_link_wiggle))
208    }
209
210    pub(crate) fn wiggle_geometry(
211        &self,
212        q0: ArrayView1<'_, f64>,
213        beta_link_wiggle: ArrayView1<'_, f64>,
214    ) -> Result<GaussianLocationScaleWiggleGeometry, String> {
215        let basis = self.wiggle_design(q0)?;
216        let basis_d1 = self.wiggle_basiswith_options(q0, BasisOptions::first_derivative())?;
217        let basis_d2 = self.wiggle_basiswith_options(q0, BasisOptions::second_derivative())?;
218        let basis_d3 = self.wiggle_d3basis_constrained(q0)?;
219        let dq_dq0 = self.wiggle_dq_dq0(q0, beta_link_wiggle)?;
220        let d2q_dq02 = self.wiggle_d2q_dq02(q0, beta_link_wiggle)?;
221        let d3q_dq03 = self.wiggle_d3q_dq03(q0, beta_link_wiggle)?;
222        let d4q_dq04 = self.wiggle_d4q_dq04(q0, beta_link_wiggle)?;
223        Ok(GaussianLocationScaleWiggleGeometry {
224            basis,
225            basis_d1,
226            basis_d2,
227            basis_d3,
228            dq_dq0,
229            d2q_dq02,
230            d3q_dq03,
231            d4q_dq04,
232        })
233    }
234
235    pub(crate) fn get_or_compute_row_scalars(
236        &self,
237        q: &Array1<f64>,
238        eta_ls: &Array1<f64>,
239    ) -> Result<Arc<GaussianJointRowScalars>, String> {
240        Ok(Arc::new(gaussian_jointrow_scalars(
241            &self.y,
242            q,
243            eta_ls,
244            &self.weights,
245        )?))
246    }
247
248    pub(crate) fn dense_block_designs(
249        &self,
250    ) -> Result<(Cow<'_, Array2<f64>>, Cow<'_, Array2<f64>>), String> {
251        dense_locscale_block_designs_cached(
252            self.mu_design.as_ref(),
253            self.log_sigma_design.as_ref(),
254            "GaussianLocationScaleWiggleFamily",
255            "GaussianLocationScaleWiggle",
256            "mu",
257            &self.policy.material_policy(),
258        )
259    }
260    pub(crate) fn dense_block_designs_fromspecs<'a>(
261        &self,
262        specs: &'a [ParameterBlockSpec],
263    ) -> Result<(Cow<'a, Array2<f64>>, Cow<'a, Array2<f64>>), String> {
264        dense_locscale_block_designs_fromspecs(
265            specs,
266            3,
267            "GaussianLocationScaleWiggleFamily",
268            "GaussianLocationScaleWiggle",
269            Self::BLOCK_MU,
270            Self::BLOCK_LOG_SIGMA,
271            "mu",
272            &self.policy.material_policy(),
273        )
274    }
275
276    pub(crate) fn exact_joint_dense_block_designs<'a>(
277        &'a self,
278        specs: Option<&'a [ParameterBlockSpec]>,
279    ) -> Result<Option<(Cow<'a, Array2<f64>>, Cow<'a, Array2<f64>>)>, String> {
280        if self.exact_joint_supported() {
281            return self.dense_block_designs().map(Some);
282        }
283        if let Some(specs) = specs {
284            return self.dense_block_designs_fromspecs(specs).map(Some);
285        }
286        Ok(None)
287    }
288
289    /// Build the [`BlockEffectiveJacobian`] for block `block_idx`.
290    ///
291    /// The wiggle block (block 2) modulates the inverse link nonlinearly and
292    /// does not contribute a linear additive term to any output η; its
293    /// Jacobian is an `(2 * n, p_wiggle)` zero matrix.
294    ///
295    /// - block 0 (mu):        output 0 = design rows, output 1 = zeros
296    /// - block 1 (log_sigma): output 0 = zeros, output 1 = design rows
297    /// - block 2 (wiggle):    all zeros (nonlinear link modulation)
298    pub fn block_effective_jacobian(
299        specs: &[ParameterBlockSpec],
300        block_idx: usize,
301    ) -> Result<Box<dyn BlockEffectiveJacobian>, String> {
302        crate::block_layout::block_jacobian::AdditiveWiggleBlockLayout {
303            family: "GaussianLocationScaleWiggleFamily",
304            n_outputs: 2,
305            additive_blocks: &[Self::BLOCK_MU, Self::BLOCK_LOG_SIGMA],
306            wiggle_block: Some(Self::BLOCK_WIGGLE),
307        }
308        .block_effective_jacobian(specs, block_idx)
309    }
310}
311
312/// Row-coefficient bundle for the GLS Wiggle joint second directional
313/// derivative, shared by the matrix-free operator and the dense
314/// `_from_designs` assemblies. Holds exactly the quantities both consumers
315/// read downstream of the (identical) coefficient computation.
316pub(crate) struct GlsWiggleSecondDirCoeffs {
317    pub(crate) coeff_mm_uv: Array1<f64>,
318    pub(crate) coeff_ml_uv: Array1<f64>,
319    pub(crate) coeff_ll_uv: Array1<f64>,
320    pub(crate) a_u: Array1<f64>,
321    pub(crate) a_v: Array1<f64>,
322    pub(crate) a_uv: Array1<f64>,
323    pub(crate) c_u: Array1<f64>,
324    pub(crate) c_v: Array1<f64>,
325    pub(crate) c_uv: Array1<f64>,
326    pub(crate) l_u: Array1<f64>,
327    pub(crate) l_v: Array1<f64>,
328    pub(crate) l_uv: Array1<f64>,
329    pub(crate) dw_u: Array1<f64>,
330    pub(crate) dw_v: Array1<f64>,
331    pub(crate) dw_uv: Array1<f64>,
332}
333
334/// The two probe directions resolved to row space for the GLS Wiggle joint
335/// second directional derivative: `xi`/`zeta` are the X_mu/X_ls contractions,
336/// and `q`/`s1`/`g2` are the mixed first/second-derivative wiggle pieces.
337pub(crate) struct GlsWiggleDirPieces<'a> {
338    pub(crate) zeta_u: &'a Array1<f64>,
339    pub(crate) zeta_v: &'a Array1<f64>,
340    pub(crate) q_u: &'a Array1<f64>,
341    pub(crate) q_v: &'a Array1<f64>,
342    pub(crate) q_uv: &'a Array1<f64>,
343    pub(crate) s1_u: &'a Array1<f64>,
344    pub(crate) s1_v: &'a Array1<f64>,
345    pub(crate) s1_uv: &'a Array1<f64>,
346    pub(crate) g2_u: &'a Array1<f64>,
347    pub(crate) g2_v: &'a Array1<f64>,
348    pub(crate) g2_uv: &'a Array1<f64>,
349}
350
351/// Compute the shared GLS Wiggle second-directional row coefficients from the
352/// per-row scalars, wiggle geometry, and the resolved probe directions.
353pub(crate) fn gls_wiggle_second_directional_coeffs(
354    rows: &GaussianJointRowScalars,
355    geom: &GaussianLocationScaleWiggleGeometry,
356    dir: &GlsWiggleDirPieces<'_>,
357) -> GlsWiggleSecondDirCoeffs {
358    let GlsWiggleDirPieces {
359        zeta_u,
360        zeta_v,
361        q_u,
362        q_v,
363        q_uv,
364        s1_u,
365        s1_v,
366        s1_uv,
367        g2_u,
368        g2_v,
369        g2_uv,
370    } = *dir;
371    let szeta_u = &rows.kappa * zeta_u;
372    let szeta_v = &rows.kappa * zeta_v;
373    let zeta_u_zeta_v = zeta_u * zeta_v;
374    let dw_u = -2.0 * &rows.w * &szeta_u;
375    let dw_v = -2.0 * &rows.w * &szeta_v;
376    let dw_uv =
377        4.0 * &rows.w * &(&szeta_u * &szeta_v) - 2.0 * &rows.w * &rows.kappa_prime * &zeta_u_zeta_v;
378    let dm_u = -(&rows.w * q_u) - &(2.0 * &rows.m * &szeta_u);
379    let dm_v = -(&rows.w * q_v) - &(2.0 * &rows.m * &szeta_v);
380    let dm_uv = &(2.0 * &rows.w * &(q_u * &szeta_v + q_v * &szeta_u)) - &(&rows.w * q_uv)
381        + &(4.0 * &rows.m * &(&szeta_u * &szeta_v))
382        - 2.0 * &rows.m * &rows.kappa_prime * &zeta_u_zeta_v;
383    let coeff_mm_uv = &(&dw_uv * &geom.dq_dq0.mapv(|v| v * v))
384        + &(2.0 * &dw_u * &geom.dq_dq0 * s1_v)
385        + &(2.0 * &dw_v * &geom.dq_dq0 * s1_u)
386        + &(2.0 * &rows.w * s1_u * s1_v)
387        + &(2.0 * &rows.w * &geom.dq_dq0 * s1_uv)
388        - &(&dm_uv * &geom.d2q_dq02)
389        - &(&dm_u * g2_v)
390        - &(&dm_v * g2_u)
391        - &(&rows.m * g2_uv);
392    // OBSERVED joint Hessian second directional derivatives (#1561). Second δn
393    // along (u, v); η_ls is linear in β so there is no zeta_uv leg.
394    let dn_u = &(-(2.0 * &rows.m * q_u)) - &(2.0 * &rows.n * &szeta_u);
395    let dn_v = &(-(2.0 * &rows.m * q_v)) - &(2.0 * &rows.n * &szeta_v);
396    let dn_uv = &(&(&(2.0 * &rows.w * &(q_u * q_v)) - &(2.0 * &rows.m * q_uv))
397        + &(4.0 * &rows.m * &(&(q_u * &szeta_v) + &(q_v * &szeta_u))))
398        + &(&(4.0 * &rows.n * &(&szeta_u * &szeta_v))
399            - &(2.0 * &rows.n * &rows.kappa_prime * &zeta_u_zeta_v));
400    // κ''' = κ''(1−2κ) − 2(κ')² (logb); a_coef = 2κ²−κ'.
401    let ktp = &(&rows.kappa_dprime * &(1.0 - 2.0 * &rows.kappa))
402        - &(2.0 * &rows.kappa_prime * &rows.kappa_prime);
403    let a_coef = 2.0 * &rows.kappa * &rows.kappa - &rows.kappa_prime;
404    let amn = &rows.obs_weight - &rows.n;
405    // D²(2κm·dq_dq0)[u,v]: differentiate κ (zeta), m (dm), dq_dq0=D (s1).
406    let d = &geom.dq_dq0;
407    let ml_term_kpp = &rows.kappa_dprime * &zeta_u_zeta_v * &rows.m * d;
408    let ml_term_kp_d = &rows.kappa_prime * d * &(&(zeta_v * &dm_u) + &(zeta_u * &dm_v));
409    let ml_term_kp_m = &rows.kappa_prime * &rows.m * &(&(zeta_v * s1_u) + &(zeta_u * s1_v));
410    let ml_term_k_d = &rows.kappa * d * &dm_uv;
411    let ml_term_k_s = &rows.kappa * &(&(&dm_v * s1_u) + &(&dm_u * s1_v));
412    let ml_term_k_m = &rows.kappa * &rows.m * s1_uv;
413    let coeff_ml_uv = 2.0
414        * &(&(&(&(&(&ml_term_kpp + &ml_term_kp_d) + &ml_term_kp_m) + &ml_term_k_d) + &ml_term_k_s)
415            + &ml_term_k_m);
416    // D²(κ'(a−n)+2κ²n)[u,v] via dn: κ'''(a−n)ζ_uζ_v + 4(κ'²+κκ'')nζ_uζ_v
417    // + (4κκ'−κ'')(ζ_v·dn_u + ζ_u·dn_v) + (2κ²−κ')·dn_uv.
418    let four_kkp_minus_kdp = 4.0 * &rows.kappa * &rows.kappa_prime - &rows.kappa_dprime;
419    let coeff_ll_uv = &(&(&(&ktp * &amn * &zeta_u_zeta_v)
420        + &(4.0
421            * &(&(&rows.kappa_prime * &rows.kappa_prime) + &(&rows.kappa * &rows.kappa_dprime))
422            * &rows.n
423            * &zeta_u_zeta_v))
424        + &(&four_kkp_minus_kdp * &(&(zeta_v * &dn_u) + &(zeta_u * &dn_v))))
425        + &(&a_coef * &dn_uv);
426
427    let a_u = &dw_u * &geom.dq_dq0 + &rows.w * s1_u;
428    let a_v = &dw_v * &geom.dq_dq0 + &rows.w * s1_v;
429    let a_uv = &dw_uv * &geom.dq_dq0 + &dw_u * s1_v + &dw_v * s1_u + &rows.w * s1_uv;
430    let c_u = -&dm_u;
431    let c_v = -&dm_v;
432    let c_uv = -&dm_uv;
433    // OBSERVED ls↔wiggle cross 2κm: l_u/l_v = D(2κm), l_uv = D²(2κm).
434    let l_u = &(2.0 * &rows.kappa_prime * zeta_u * &rows.m) + &(2.0 * &rows.kappa * &dm_u);
435    let l_v = &(2.0 * &rows.kappa_prime * zeta_v * &rows.m) + &(2.0 * &rows.kappa * &dm_v);
436    let l_uv = &(&(2.0 * &rows.kappa_dprime * &zeta_u_zeta_v * &rows.m)
437        + &(2.0 * &rows.kappa_prime * &(&(zeta_v * &dm_u) + &(zeta_u * &dm_v))))
438        + &(2.0 * &rows.kappa * &dm_uv);
439
440    GlsWiggleSecondDirCoeffs {
441        coeff_mm_uv,
442        coeff_ml_uv,
443        coeff_ll_uv,
444        a_u,
445        a_v,
446        a_uv,
447        c_u,
448        c_v,
449        c_uv,
450        l_u,
451        l_v,
452        l_uv,
453        dw_u,
454        dw_v,
455        dw_uv,
456    }
457}
458
459impl GaussianLocationScaleWiggleFamily {
460    pub(crate) fn exact_newton_joint_hessian_for_specs(
461        &self,
462        block_states: &[ParameterBlockState],
463        specs: Option<&[ParameterBlockSpec]>,
464    ) -> Result<Option<Array2<f64>>, String> {
465        let Some((xmu, x_ls)) = self.exact_joint_dense_block_designs(specs)? else {
466            return Ok(None);
467        };
468        self.exact_newton_joint_hessian_from_designs(block_states, &xmu, &x_ls)
469    }
470
471    pub(crate) fn exact_newton_joint_hessian_directional_derivative_for_specs(
472        &self,
473        block_states: &[ParameterBlockState],
474        specs: Option<&[ParameterBlockSpec]>,
475        d_beta_flat: &Array1<f64>,
476    ) -> Result<Option<Array2<f64>>, String> {
477        let Some((xmu, x_ls)) = self.exact_joint_dense_block_designs(specs)? else {
478            return Ok(None);
479        };
480        self.exact_newton_joint_hessian_directional_derivative_from_designs(
481            block_states,
482            &xmu,
483            &x_ls,
484            d_beta_flat,
485        )
486    }
487
488    pub(crate) fn exact_newton_joint_hessian_second_directional_derivative_for_specs(
489        &self,
490        block_states: &[ParameterBlockState],
491        specs: Option<&[ParameterBlockSpec]>,
492        d_beta_u_flat: &Array1<f64>,
493        d_beta_v_flat: &Array1<f64>,
494    ) -> Result<Option<Array2<f64>>, String> {
495        let Some((xmu, x_ls)) = self.exact_joint_dense_block_designs(specs)? else {
496            return Ok(None);
497        };
498        self.exact_newton_joint_hessiansecond_directional_derivative_from_designs(
499            block_states,
500            &xmu,
501            &x_ls,
502            d_beta_u_flat,
503            d_beta_v_flat,
504        )
505    }
506
507    pub(crate) fn exact_newton_joint_psi_direction(
508        &self,
509        block_states: &[ParameterBlockState],
510        derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
511        psi_index: usize,
512        xmu: &Array2<f64>,
513        x_ls: &Array2<f64>,
514        policy: &gam_runtime::resource::ResourcePolicy,
515    ) -> Result<Option<LocationScaleJointPsiDirection>, String> {
516        let Some(parts) = locscale_joint_psi_direction_parts(
517            block_states,
518            derivative_blocks,
519            psi_index,
520            self.y.len(),
521            xmu.ncols(),
522            x_ls.ncols(),
523            Self::BLOCK_MU,
524            Self::BLOCK_LOG_SIGMA,
525            3,
526            "GaussianLocationScaleWiggleFamily",
527            "mu",
528            policy,
529        )?
530        else {
531            return Ok(None);
532        };
533        Ok(Some(LocationScaleJointPsiDirection {
534            block_idx: parts.block_idx,
535            local_idx: parts.local_idx,
536            z_primary_psi: parts.primary_z,
537            z_ls_psi: parts.log_sigma_z,
538            x_primary_psi: parts.primary_psi,
539            x_ls_psi: parts.log_sigma_psi,
540        }))
541    }
542
543    pub(crate) fn exact_newton_joint_psisecond_design_drifts(
544        &self,
545        block_states: &[ParameterBlockState],
546        derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
547        psi_a: &LocationScaleJointPsiDirection,
548        psi_b: &LocationScaleJointPsiDirection,
549        xmu: &Array2<f64>,
550        x_ls: &Array2<f64>,
551    ) -> Result<LocationScaleJointPsiSecondDrifts, String> {
552        locscale_joint_psisecond_design_drifts(
553            block_states,
554            derivative_blocks,
555            psi_a,
556            psi_b,
557            LocScalePsiDriftConfig {
558                n: self.y.len(),
559                p_primary: xmu.ncols(),
560                p_log_sigma: x_ls.ncols(),
561                primary_block_idx: Self::BLOCK_MU,
562                log_sigma_block_idx: Self::BLOCK_LOG_SIGMA,
563                family_name: "GaussianLocationScaleWiggleFamily",
564                primary_label: "mu",
565                policy: &self.policy,
566            },
567        )
568    }
569
570    /// Compute the rowwise Hessian pieces shared by the dense path and the
571    /// matrix-free workspace operator. The same coefficients reconstruct the
572    /// dense p×p matrix or apply `Hv` directly without ever forming it.
573    pub(crate) fn wiggle_hessian_row_pieces(
574        &self,
575        block_states: &[ParameterBlockState],
576    ) -> Result<GaussianLocationScaleWiggleHessianRowPieces, String> {
577        validate_block_count::<GamlssError>(
578            "GaussianLocationScaleWiggleFamily",
579            3,
580            block_states.len(),
581        )?;
582        let q0 = &block_states[Self::BLOCK_MU].eta;
583        let eta_ls = &block_states[Self::BLOCK_LOG_SIGMA].eta;
584        let etaw = &block_states[Self::BLOCK_WIGGLE].eta;
585        let betaw = &block_states[Self::BLOCK_WIGGLE].beta;
586        let n = self.y.len();
587        if q0.len() != n || eta_ls.len() != n || etaw.len() != n || self.weights.len() != n {
588            return Err(GamlssError::DimensionMismatch {
589                reason: "GaussianLocationScaleWiggleFamily input size mismatch".to_string(),
590            }
591            .into());
592        }
593        let q = q0 + etaw;
594        let geom = self.wiggle_geometry(q0.view(), betaw.view())?;
595        if geom.basis.ncols() != betaw.len() {
596            return Err(GamlssError::DimensionMismatch { reason: format!(
597                "GaussianLocationScaleWiggleFamily wiggle basis/beta mismatch: basis has {} columns but beta has {} entries",
598                geom.basis.ncols(),
599                betaw.len()
600            ) }.into());
601        }
602        let rows = self.get_or_compute_row_scalars(&q, eta_ls)?;
603        let coeff_mm = &rows.w * &geom.dq_dq0.mapv(|v| v * v) - &rows.m * &geom.d2q_dq02;
604        // OBSERVED joint LAML Hessian (Wood–Pya–Säfken 2016; #1561 cutover from
605        // the block-Fisher #566/#684 object). The LAML criterion `−½log|H+S|`
606        // and its ρ-gradient require the OBSERVED curvature at β̂; the earlier
607        // Fisher object (cross ≡ 0, σσ = 2κ²a) overstated σ-block information on
608        // the near-flat scale surface and over-smoothed log σ. μ AND the wiggle
609        // both enter the mean q = q0 + B(q0)·βw (see `let q = q0 + etaw`), so the
610        // whole mean side carries the observed cross-curvature 2κm against log σ.
611        // The single-source (mm, ml=2κm, ll=κ'(a−n)+2κ²n) is
612        // `gaussian_locscale_observed_joint_row_coeffs`; here it is warped by the
613        // wiggle mean-geometry: the μ-side Jacobian is ∂q/∂β_μ = dq_dq0·X_μ (cf.
614        // coeff_mm's dq_dq0² and coeff_mw_b's dq_dq0), so H_{μ,ls} = 2κm·dq_dq0,
615        // while the wiggle-side Jacobian is ∂q/∂β_w = B (no warp), so
616        // H_{w,ls} = 2κm (coeff_lw_b). At a flat/true-null σ surface n→a, m→0,
617        // so observed → Fisher and the null behavior is unchanged.
618        let (_, ml_base, ll) = gaussian_locscale_observed_joint_row_coeffs(&rows);
619        let coeff_ml = &ml_base * &geom.dq_dq0;
620        let coeff_ll = ll;
621        let coeff_mw_b = &rows.w * &geom.dq_dq0;
622        let coeff_mw_d = -&rows.m;
623        // OBSERVED ls↔wiggle cross H_{w,ls} = 2κm (wiggle is mean-side; ∂q/∂β_w
624        // = B, no dq_dq0 warp, unlike coeff_ml). Same single-source 2κm.
625        let coeff_lw_b = ml_base;
626        let coeff_ww = rows.w.clone();
627        Ok(GaussianLocationScaleWiggleHessianRowPieces {
628            coeff_mm,
629            coeff_ml,
630            coeff_ll,
631            coeff_mw_b,
632            coeff_mw_d,
633            coeff_lw_b,
634            coeff_ww,
635            basis: geom.basis,
636            basis_d1: geom.basis_d1,
637        })
638    }
639
640    pub(crate) fn exact_newton_joint_hessian_from_designs(
641        &self,
642        block_states: &[ParameterBlockState],
643        xmu: &Array2<f64>,
644        x_ls: &Array2<f64>,
645    ) -> Result<Option<Array2<f64>>, String> {
646        let pieces = self.wiggle_hessian_row_pieces(block_states)?;
647        Ok(Some(pieces.assemble_dense(xmu, x_ls)?))
648    }
649
650    pub(crate) fn exact_newton_joint_hessian_directional_derivative_from_designs(
651        &self,
652        block_states: &[ParameterBlockState],
653        xmu: &Array2<f64>,
654        x_ls: &Array2<f64>,
655        d_beta_flat: &Array1<f64>,
656    ) -> Result<Option<Array2<f64>>, String> {
657        validate_block_count::<GamlssError>(
658            "GaussianLocationScaleWiggleFamily",
659            3,
660            block_states.len(),
661        )?;
662        let pmu = xmu.ncols();
663        let p_ls = x_ls.ncols();
664        let q0 = &block_states[Self::BLOCK_MU].eta;
665        let eta_ls = &block_states[Self::BLOCK_LOG_SIGMA].eta;
666        let etaw = &block_states[Self::BLOCK_WIGGLE].eta;
667        let betaw = &block_states[Self::BLOCK_WIGGLE].beta;
668        let n = self.y.len();
669        let layout = GamlssBetaLayout::withwiggle(pmu, p_ls, betaw.len());
670        let (umu, u_ls, uw) = layout.split_three(
671            d_beta_flat,
672            "GaussianLocationScaleWiggleFamily exact joint directional Hessian",
673        )?;
674        if q0.len() != n || eta_ls.len() != n || etaw.len() != n || self.weights.len() != n {
675            return Err(GamlssError::DimensionMismatch {
676                reason: "GaussianLocationScaleWiggleFamily input size mismatch".to_string(),
677            }
678            .into());
679        }
680        let q = q0 + etaw;
681        let geom = self.wiggle_geometry(q0.view(), betaw.view())?;
682        let rows = self.get_or_compute_row_scalars(&q, eta_ls)?;
683        let xi = fast_av(xmu, &umu);
684        let zeta = fast_av(x_ls, &u_ls);
685        // logb κ-scaled η_ls direction; κ' = dκ/dη_ls = κ(1−κ).
686        let szeta = &rows.kappa * &zeta;
687        let phi = fast_av(&geom.basis, &uw);
688        let mut q_u = &geom.dq_dq0 * &xi;
689        q_u += &phi;
690        let mut s1_u = &geom.d2q_dq02 * &xi;
691        s1_u += &fast_av(&geom.basis_d1, &uw);
692        let mut g2_u = &geom.d3q_dq03 * &xi;
693        g2_u += &fast_av(&geom.basis_d2, &uw);
694        let basis_u = scale_matrix_rows(&geom.basis_d1, &xi)?;
695        let basis1_u = scale_matrix_rows(&geom.basis_d2, &xi)?;
696        let dw_u = -2.0 * &rows.w * &szeta;
697        let dm_u = -(&rows.w * &q_u) - &(2.0 * &rows.m * &szeta);
698
699        let coeff_mm_u = &(&dw_u * &geom.dq_dq0.mapv(|v| v * v))
700            + &(2.0 * &rows.w * &geom.dq_dq0 * &s1_u)
701            - &(&dm_u * &geom.d2q_dq02)
702            - &(&rows.m * &g2_u);
703        // OBSERVED joint Hessian directional derivatives (#1561). The μ↔ls cross
704        // is H_{μ,ls} = 2κm·dq_dq0, so its β-directional derivative differentiates
705        // κ (via zeta), m (via dm_u), and dq_dq0 (via s1_u). H_{ls,ls} =
706        // κ'(a−n)+2κ²n → d/dβ = κ''ζ(a−n) + 4κκ'ζn + (2κ²−κ')·dn (dn = δn along u).
707        // The dense sibling `gaussian_joint_psi_firstweights` d_u/c_u channels
708        // carry the same closed forms.
709        let a_coef = 2.0 * &rows.kappa * &rows.kappa - &rows.kappa_prime;
710        let dn_u = -(2.0 * &rows.m * &q_u) - &(2.0 * &rows.n * &szeta);
711        let coeff_ml_u = &(2.0 * &rows.kappa_prime * &zeta * &rows.m * &geom.dq_dq0)
712            + &(2.0 * &rows.kappa * &dm_u * &geom.dq_dq0)
713            + &(2.0 * &rows.kappa * &rows.m * &s1_u);
714        let coeff_ll_u = &(&rows.kappa_dprime * &zeta * &(&rows.obs_weight - &rows.n))
715            + &(4.0 * &rows.kappa * &rows.kappa_prime * &zeta * &rows.n)
716            + &(&a_coef * &dn_u);
717        let a_u = &dw_u * &geom.dq_dq0 + &rows.w * &s1_u;
718        let c_u = -&dm_u;
719        // OBSERVED ls↔wiggle cross 2κm: value derivative l_u along u, plus the
720        // basis drift (B = B(q0) moves with the μ direction xi, δB = diag(xi)·B'
721        // = basis_u) carrying the un-differentiated coeff_lw_b = 2κm.
722        let l_u = &(2.0 * &rows.kappa_prime * &zeta * &rows.m) + &(2.0 * &rows.kappa * &dm_u);
723        let coeff_lw_b = 2.0 * &rows.kappa * &rows.m;
724
725        let h_mm = xt_diag_x_dense(xmu, &coeff_mm_u)?;
726        let h_ml = xt_diag_y_dense(xmu, &coeff_ml_u, x_ls)?;
727        let h_ll = xt_diag_x_dense(x_ls, &coeff_ll_u)?;
728        let h_mw = xt_diag_y_dense(xmu, &a_u, &geom.basis)?
729            + &xt_diag_y_dense(xmu, &(&rows.w * &geom.dq_dq0), &basis_u)?
730            + &xt_diag_y_dense(xmu, &c_u, &geom.basis_d1)?
731            + &xt_diag_y_dense(xmu, &(-&rows.m), &basis1_u)?;
732        let h_lw = xt_diag_y_dense(x_ls, &l_u, &geom.basis)?
733            + &xt_diag_y_dense(x_ls, &coeff_lw_b, &basis_u)?;
734        let a_ww = xt_diag_y_dense(&basis_u, &rows.w, &geom.basis)?;
735        let h_ww = &a_ww + &a_ww.t() + &xt_diag_x_dense(&geom.basis, &dw_u)?;
736        Ok(Some(gaussian_pack_wiggle_joint_symmetrichessian(
737            &h_mm, &h_ml, &h_mw, &h_ll, &h_lw, &h_ww,
738        )))
739    }
740
741    /// Build a matrix-free `RowCoeffOperator` for the GLS Wiggle joint
742    /// directional derivative `D_β H_L[u]`. Output dimension is
743    /// `pmu + p_ls + pw`. Channels (in order): X_mu, X_ls, B, B', B''.
744    pub(crate) fn gls_wiggle_directional_operator(
745        &self,
746        block_states: &[ParameterBlockState],
747        xmu_arc: Arc<Array2<f64>>,
748        x_ls_arc: Arc<Array2<f64>>,
749        d_beta_flat: &Array1<f64>,
750    ) -> Result<Option<Arc<dyn gam_problem::HyperOperator>>, String> {
751        validate_block_count::<GamlssError>(
752            "GaussianLocationScaleWiggleFamily",
753            3,
754            block_states.len(),
755        )?;
756        let pmu = xmu_arc.ncols();
757        let p_ls = x_ls_arc.ncols();
758        let q0_eta = &block_states[Self::BLOCK_MU].eta;
759        let eta_ls = &block_states[Self::BLOCK_LOG_SIGMA].eta;
760        let etaw = &block_states[Self::BLOCK_WIGGLE].eta;
761        let betaw = &block_states[Self::BLOCK_WIGGLE].beta;
762        let n = self.y.len();
763        let layout = GamlssBetaLayout::withwiggle(pmu, p_ls, betaw.len());
764        let (umu, u_ls, uw) =
765            layout.split_three(d_beta_flat, "GLS Wiggle joint dH operator d_beta")?;
766        if q0_eta.len() != n || eta_ls.len() != n || etaw.len() != n || self.weights.len() != n {
767            return Err(GamlssError::DimensionMismatch {
768                reason: "GaussianLocationScaleWiggleFamily input size mismatch".to_string(),
769            }
770            .into());
771        }
772        let q = q0_eta + etaw;
773        let geom = self.wiggle_geometry(q0_eta.view(), betaw.view())?;
774        let rows = self.get_or_compute_row_scalars(&q, eta_ls)?;
775        let xi = fast_av(xmu_arc.as_ref(), &umu);
776        let zeta = fast_av(x_ls_arc.as_ref(), &u_ls);
777        let szeta = &rows.kappa * &zeta;
778        let phi = fast_av(&geom.basis, &uw);
779        let mut q_u = &geom.dq_dq0 * &xi;
780        q_u += &phi;
781        let mut s1_u = &geom.d2q_dq02 * &xi;
782        s1_u += &fast_av(&geom.basis_d1, &uw);
783        let mut g2_u = &geom.d3q_dq03 * &xi;
784        g2_u += &fast_av(&geom.basis_d2, &uw);
785        let dw_u = -2.0 * &rows.w * &szeta;
786        let dm_u = -(&rows.w * &q_u) - &(2.0 * &rows.m * &szeta);
787
788        let coeff_mm_u = &(&dw_u * &geom.dq_dq0.mapv(|v| v * v))
789            + &(2.0 * &rows.w * &geom.dq_dq0 * &s1_u)
790            - &(&dm_u * &geom.d2q_dq02)
791            - &(&rows.m * &g2_u);
792        // OBSERVED μ↔ls cross 2κm·dq_dq0; its directional derivative differentiates
793        // κ (zeta), m (dm_u), and dq_dq0 (s1_u). (#1561)
794        let coeff_ml_u = &(2.0 * &rows.kappa_prime * &zeta * &rows.m * &geom.dq_dq0)
795            + &(2.0 * &rows.kappa * &dm_u * &geom.dq_dq0)
796            + &(2.0 * &rows.kappa * &rows.m * &s1_u);
797        // OBSERVED (ls,ls) κ'(a−n)+2κ²n directional derivative: κ''ζ(a−n) +
798        // 4κκ'ζn + (2κ²−κ')·dn, dn = δn along u (#1561).
799        let a_coef = 2.0 * &rows.kappa * &rows.kappa - &rows.kappa_prime;
800        let dn_u = -(2.0 * &rows.m * &q_u) - &(2.0 * &rows.n * &szeta);
801        let coeff_ll_u = &(&rows.kappa_dprime * &zeta * &(&rows.obs_weight - &rows.n))
802            + &(4.0 * &rows.kappa * &rows.kappa_prime * &zeta * &rows.n)
803            + &(&a_coef * &dn_u);
804        let a_u = &dw_u * &geom.dq_dq0 + &rows.w * &s1_u;
805        let c_u = -&dm_u;
806        // OBSERVED ls↔wiggle cross 2κm: B-channel derivative l_u, and the
807        // basis-drift B'-channel carries the un-differentiated 2κm.
808        let l_u = &(2.0 * &rows.kappa_prime * &zeta * &rows.m) + &(2.0 * &rows.kappa * &dm_u);
809
810        // Pair-coefficient bundles. For (0=X_mu, 3=B'): combine
811        // `xt_diag_y_dense(xmu, &(w·dq_dq0), &basis_u=diag(xi)·B')`
812        // (giving coeff `w·dq_dq0·xi`) with `xt_diag_y_dense(xmu, &c_u, &B')`
813        // (coeff `c_u`).
814        let coeff_m_b1 = &(&rows.w * &geom.dq_dq0 * &xi) + &c_u;
815        // (0=X_mu, 4=B''): from `xt_diag_y_dense(xmu, &(-m), &basis1_u=diag(xi)·B'')`.
816        let coeff_m_b2 = -(&rows.m * &xi);
817        // (1=X_ls, 3=B'): observed ls↔wiggle basis drift — coeff_lw_b·δB with
818        // δB = diag(xi)·B', giving coeff 2κm·xi.
819        let coeff_ls_b1 = 2.0 * &rows.kappa * &rows.m * &xi;
820        // (2=B, 3=B'): a_ww + a_ww^T where a_ww = (diag(xi)·B')^T diag(w) B
821        // = B'^T diag(w·xi) B. The symmetric pair contribution in
822        // `RowCoeffOperator` reproduces a_ww + a_ww^T with c = w·xi.
823        let coeff_b_b1 = &rows.w * &xi;
824
825        let basis: Arc<Array2<f64>> = Arc::new(geom.basis.clone());
826        let basis_d1: Arc<Array2<f64>> = Arc::new(geom.basis_d1.clone());
827        let basis_d2: Arc<Array2<f64>> = Arc::new(geom.basis_d2.clone());
828        let pw = basis.ncols();
829
830        Ok(Some(Arc::new(RowCoeffOperator::from_directions(
831            vec![pmu, p_ls, pw],
832            vec![
833                (0, xmu_arc),
834                (1, x_ls_arc),
835                (2, basis),
836                (2, basis_d1),
837                (2, basis_d2),
838            ],
839            vec![
840                // (X_mu, X_mu) ← `xt_diag_x_dense(xmu, &coeff_mm_u)`
841                (0, 0, coeff_mm_u),
842                // (X_mu, X_ls) ← `xt_diag_y_dense(xmu, &coeff_ml_u, x_ls)`
843                (0, 1, coeff_ml_u),
844                // (X_ls, X_ls) ← `xt_diag_x_dense(x_ls, &coeff_ll_u)`
845                (1, 1, coeff_ll_u),
846                // (X_mu, B) ← `xt_diag_y_dense(xmu, &a_u, &geom.basis)`
847                (0, 2, a_u),
848                // (X_mu, B') ← `xt_diag_y_dense(xmu, w·dq_dq0, basis_u=diag(ξ)·B') + xt_diag_y_dense(xmu, c_u, B')`
849                (0, 3, coeff_m_b1),
850                // (X_mu, B'') ← `xt_diag_y_dense(xmu, -m, basis1_u=diag(ξ)·B'')`
851                (0, 4, coeff_m_b2),
852                // (X_ls, B) ← `xt_diag_y_dense(x_ls, &l_u, &geom.basis)`
853                (1, 2, l_u),
854                // (X_ls, B') ← observed ls↔wiggle basis drift 2κm·xi (coeff_lw_b·δB)
855                (1, 3, coeff_ls_b1),
856                // (B, B) ← `xt_diag_x_dense(&geom.basis, &dw_u)`
857                (2, 2, dw_u),
858                // (B, B') ← a_ww + a_ww^T = B^T diag(w·ξ) B' + B'^T diag(w·ξ) B
859                (2, 3, coeff_b_b1),
860            ],
861            n,
862        ))))
863    }
864
865    /// Build a matrix-free `RowCoeffOperator` for the GLS Wiggle joint
866    /// second directional derivative `D²_β H_L[u, v]`. Channels: X_mu,
867    /// X_ls, B, B', B'', B'''. Pair list mirrors the 8-term `xt_diag_*`
868    /// assembly in `_from_designs`, with row-coefficient bundles that
869    /// absorb the `ξ_u, ξ_v, ξ_u·ξ_v` row factors arising from
870    /// `basis_u = diag(ξ_u)·B'`, `basis_uv = diag(ξ_u·ξ_v)·B''`, etc.
871    pub(crate) fn gls_wiggle_second_directional_operator(
872        &self,
873        block_states: &[ParameterBlockState],
874        xmu_arc: Arc<Array2<f64>>,
875        x_ls_arc: Arc<Array2<f64>>,
876        d_beta_u: &Array1<f64>,
877        d_beta_v: &Array1<f64>,
878    ) -> Result<Option<Arc<dyn gam_problem::HyperOperator>>, String> {
879        validate_block_count::<GamlssError>(
880            "GaussianLocationScaleWiggleFamily",
881            3,
882            block_states.len(),
883        )?;
884        let pmu = xmu_arc.ncols();
885        let p_ls = x_ls_arc.ncols();
886        let q0_eta = &block_states[Self::BLOCK_MU].eta;
887        let eta_ls = &block_states[Self::BLOCK_LOG_SIGMA].eta;
888        let etaw = &block_states[Self::BLOCK_WIGGLE].eta;
889        let betaw = &block_states[Self::BLOCK_WIGGLE].beta;
890        let n = self.y.len();
891        let layout = GamlssBetaLayout::withwiggle(pmu, p_ls, betaw.len());
892        let (umu, u_ls, uw) = layout.split_three(d_beta_u, "GLS Wiggle d2H operator (u)")?;
893        let (vmu, v_ls, vw) = layout.split_three(d_beta_v, "GLS Wiggle d2H operator (v)")?;
894        if q0_eta.len() != n || eta_ls.len() != n || etaw.len() != n || self.weights.len() != n {
895            return Err(GamlssError::DimensionMismatch {
896                reason: "GaussianLocationScaleWiggleFamily input size mismatch".to_string(),
897            }
898            .into());
899        }
900        let q = q0_eta + etaw;
901        let geom = self.wiggle_geometry(q0_eta.view(), betaw.view())?;
902        let rows = self.get_or_compute_row_scalars(&q, eta_ls)?;
903
904        let xi_u = fast_av(xmu_arc.as_ref(), &umu);
905        let xi_v = fast_av(xmu_arc.as_ref(), &vmu);
906        let zeta_u = fast_av(x_ls_arc.as_ref(), &u_ls);
907        let zeta_v = fast_av(x_ls_arc.as_ref(), &v_ls);
908        let phi_u = fast_av(&geom.basis, &uw);
909        let phi_v = fast_av(&geom.basis, &vw);
910        let b1u = fast_av(&geom.basis_d1, &uw);
911        let b1v = fast_av(&geom.basis_d1, &vw);
912        let b2u = fast_av(&geom.basis_d2, &uw);
913        let b2v = fast_av(&geom.basis_d2, &vw);
914        let b3u = fast_av(&geom.basis_d3, &uw);
915        let b3v = fast_av(&geom.basis_d3, &vw);
916
917        let mut q_u = &geom.dq_dq0 * &xi_u;
918        q_u += &phi_u;
919        let mut q_v = &geom.dq_dq0 * &xi_v;
920        q_v += &phi_v;
921        let mut s1_u = &geom.d2q_dq02 * &xi_u;
922        s1_u += &b1u;
923        let mut s1_v = &geom.d2q_dq02 * &xi_v;
924        s1_v += &b1v;
925        let mut g2_u = &geom.d3q_dq03 * &xi_u;
926        g2_u += &b2u;
927        let mut g2_v = &geom.d3q_dq03 * &xi_v;
928        g2_v += &b2v;
929        let q_uv = &(&geom.d2q_dq02 * &(&xi_u * &xi_v)) + &(&b1u * &xi_v) + &(&b1v * &xi_u);
930        let s1_uv = &(&geom.d3q_dq03 * &(&xi_u * &xi_v)) + &(&b2u * &xi_v) + &(&b2v * &xi_u);
931        let g2_uv = &(&geom.d4q_dq04 * &(&xi_u * &xi_v)) + &(&b3u * &xi_v) + &(&b3v * &xi_u);
932
933        let GlsWiggleSecondDirCoeffs {
934            coeff_mm_uv,
935            coeff_ml_uv,
936            coeff_ll_uv,
937            a_u,
938            a_v,
939            a_uv,
940            c_u,
941            c_v,
942            c_uv,
943            l_u,
944            l_v,
945            l_uv,
946            dw_u,
947            dw_v,
948            dw_uv,
949        } = gls_wiggle_second_directional_coeffs(
950            &rows,
951            &geom,
952            &GlsWiggleDirPieces {
953                zeta_u: &zeta_u,
954                zeta_v: &zeta_v,
955                q_u: &q_u,
956                q_v: &q_v,
957                q_uv: &q_uv,
958                s1_u: &s1_u,
959                s1_v: &s1_v,
960                s1_uv: &s1_uv,
961                g2_u: &g2_u,
962                g2_v: &g2_v,
963                g2_uv: &g2_uv,
964            },
965        );
966
967        // Pair-coefficient bundles. Cross-block (mu, B'/B'') absorb basis_u/v/uv row scaling.
968        let xi_u_xi_v = &xi_u * &xi_v;
969        let coeff_m_b1 = &(&a_u * &xi_v) + &(&a_v * &xi_u) + &c_uv;
970        let coeff_m_b2 = &(&rows.w * &geom.dq_dq0 * &xi_u_xi_v) + &(&c_u * &xi_v) + &(&c_v * &xi_u);
971        let coeff_m_b3 = -(&rows.m * &xi_u_xi_v);
972        // OBSERVED ls↔wiggle cross 2κm (#1561). B' channel = single-drift cross
973        // l_u·ξ_v + l_v·ξ_u (basis_{u,v} = diag(ξ)·B'); B'' channel = value coeff
974        // 2κm on the second basis drift basis_uv = diag(ξ_uξ_v)·B''.
975        let coeff_ls_b1 = &(&l_u * &xi_v) + &(&l_v * &xi_u);
976        let coeff_ls_b2 = 2.0 * &rows.kappa * &rows.m * &xi_u_xi_v;
977        // Wiggle-wiggle from a_ab + a_ab^T + a_ij + a_ij^T + a_iwj + a_iwj^T + a_jwi + a_jwi^T:
978        //   a_ab = B''^T diag(w·ξ_uξ_v) B    → pair (B, B'', w·ξ_uξ_v)
979        //   a_ij = B'^T diag(w·ξ_uξ_v) B'   → pair (B', B', 2·w·ξ_uξ_v)  (a_ij + a_ij^T)
980        //   a_iwj+a_jwi = B'^T diag(dw_v·ξ_u + dw_u·ξ_v) B → pair (B, B', sum)
981        let coeff_b_b1 = &(&dw_u * &xi_v) + &(&dw_v * &xi_u);
982        let coeff_b_b2 = &rows.w * &xi_u_xi_v;
983        let coeff_b1_b1 = 2.0 * &(&rows.w * &xi_u_xi_v);
984
985        let basis: Arc<Array2<f64>> = Arc::new(geom.basis.clone());
986        let basis_d1: Arc<Array2<f64>> = Arc::new(geom.basis_d1.clone());
987        let basis_d2: Arc<Array2<f64>> = Arc::new(geom.basis_d2.clone());
988        let basis_d3: Arc<Array2<f64>> = Arc::new(geom.basis_d3.clone());
989        let pw = basis.ncols();
990
991        Ok(Some(Arc::new(RowCoeffOperator::from_directions(
992            vec![pmu, p_ls, pw],
993            vec![
994                (0, xmu_arc),
995                (1, x_ls_arc),
996                (2, basis),
997                (2, basis_d1),
998                (2, basis_d2),
999                (2, basis_d3),
1000            ],
1001            vec![
1002                // (X_mu, X_mu) ← `xt_diag_x_dense(xmu, &coeff_mm_uv)`
1003                (0, 0, coeff_mm_uv),
1004                // (X_mu, X_ls) ← `xt_diag_y_dense(xmu, &coeff_ml_uv, x_ls)`
1005                (0, 1, coeff_ml_uv),
1006                // (X_ls, X_ls) ← `xt_diag_x_dense(x_ls, &coeff_ll_uv)`
1007                (1, 1, coeff_ll_uv),
1008                // (X_mu, B) ← `xt_diag_y_dense(xmu, &a_uv, &geom.basis)`
1009                (0, 2, a_uv),
1010                // (X_mu, B') ← combined `a_u·ξ_v + a_v·ξ_u + c_uv` from
1011                // `xt_diag_y_dense(xmu, a_u, basis_v) + xt_diag_y_dense(xmu,
1012                // a_v, basis_u) + xt_diag_y_dense(xmu, c_uv, B')`
1013                (0, 3, coeff_m_b1),
1014                // (X_mu, B'') ← `xt_diag_y_dense(xmu, w·dq_dq0, basis_uv) +
1015                // xt_diag_y_dense(xmu, c_u, basis1_v) + xt_diag_y_dense(xmu,
1016                // c_v, basis1_u)` (basis_uv = diag(ξ_uξ_v)·B'';
1017                // basis1_{u,v} = diag(ξ_{u,v})·B'')
1018                (0, 4, coeff_m_b2),
1019                // (X_mu, B''') ← `xt_diag_y_dense(xmu, -m, basis1_uv)`
1020                // with basis1_uv = diag(ξ_uξ_v)·B'''
1021                (0, 5, coeff_m_b3),
1022                // (X_ls, B) ← `xt_diag_y_dense(x_ls, &l_uv, &geom.basis)`
1023                (1, 2, l_uv),
1024                // (X_ls, B') ← combined from `xt_diag_y_dense(x_ls, l_u,
1025                // basis_v) + xt_diag_y_dense(x_ls, l_v, basis_u)` =
1026                // `l_u·ξ_v + l_v·ξ_u`
1027                (1, 3, coeff_ls_b1),
1028                // (X_ls, B'') ← observed ls↔wiggle basis 2nd drift 2κm·ξ_uξ_v
1029                (1, 4, coeff_ls_b2),
1030                // (B, B) ← `xt_diag_x_dense(&geom.basis, &dw_uv)`
1031                (2, 2, dw_uv),
1032                // (B, B') ← combined `a_iwj + a_iwj^T + a_jwi + a_jwi^T` =
1033                // B^T diag(dw_u·ξ_v + dw_v·ξ_u) B' + B'^T diag(...) B
1034                (2, 3, coeff_b_b1),
1035                // (B, B'') ← `a_ab + a_ab^T` with a_ab = B''^T diag(w·ξ_uξ_v) B
1036                (2, 4, coeff_b_b2),
1037                // (B', B') ← `a_ij + a_ij^T = 2·B'^T diag(w·ξ_uξ_v) B'`;
1038                // diagonal pair coeff doubles to absorb the factor of 2
1039                (3, 3, coeff_b1_b1),
1040            ],
1041            n,
1042        ))))
1043    }
1044
1045    pub(crate) fn exact_newton_joint_hessiansecond_directional_derivative_from_designs(
1046        &self,
1047        block_states: &[ParameterBlockState],
1048        xmu: &Array2<f64>,
1049        x_ls: &Array2<f64>,
1050        d_beta_u_flat: &Array1<f64>,
1051        d_beta_v_flat: &Array1<f64>,
1052    ) -> Result<Option<Array2<f64>>, String> {
1053        validate_block_count::<GamlssError>(
1054            "GaussianLocationScaleWiggleFamily",
1055            3,
1056            block_states.len(),
1057        )?;
1058        let pmu = xmu.ncols();
1059        let p_ls = x_ls.ncols();
1060        let q0 = &block_states[Self::BLOCK_MU].eta;
1061        let eta_ls = &block_states[Self::BLOCK_LOG_SIGMA].eta;
1062        let etaw = &block_states[Self::BLOCK_WIGGLE].eta;
1063        let betaw = &block_states[Self::BLOCK_WIGGLE].beta;
1064        let n = self.y.len();
1065        let layout = GamlssBetaLayout::withwiggle(pmu, p_ls, betaw.len());
1066        let (umu, u_ls, uw) = layout.split_three(
1067            d_beta_u_flat,
1068            "GaussianLocationScaleWiggleFamily exact joint second directional Hessian (u)",
1069        )?;
1070        let (vmu, v_ls, vw) = layout.split_three(
1071            d_beta_v_flat,
1072            "GaussianLocationScaleWiggleFamily exact joint second directional Hessian (v)",
1073        )?;
1074        if q0.len() != n || eta_ls.len() != n || etaw.len() != n || self.weights.len() != n {
1075            return Err(GamlssError::DimensionMismatch {
1076                reason: "GaussianLocationScaleWiggleFamily input size mismatch".to_string(),
1077            }
1078            .into());
1079        }
1080        let q = q0 + etaw;
1081        let geom = self.wiggle_geometry(q0.view(), betaw.view())?;
1082        let rows = self.get_or_compute_row_scalars(&q, eta_ls)?;
1083
1084        let xi_u = fast_av(xmu, &umu);
1085        let xi_v = fast_av(xmu, &vmu);
1086        let zeta_u = fast_av(x_ls, &u_ls);
1087        let zeta_v = fast_av(x_ls, &v_ls);
1088        let phi_u = fast_av(&geom.basis, &uw);
1089        let phi_v = fast_av(&geom.basis, &vw);
1090        let b1u = fast_av(&geom.basis_d1, &uw);
1091        let b1v = fast_av(&geom.basis_d1, &vw);
1092        let b2u = fast_av(&geom.basis_d2, &uw);
1093        let b2v = fast_av(&geom.basis_d2, &vw);
1094        let b3u = fast_av(&geom.basis_d3, &uw);
1095        let b3v = fast_av(&geom.basis_d3, &vw);
1096
1097        let mut q_u = &geom.dq_dq0 * &xi_u;
1098        q_u += &phi_u;
1099        let mut q_v = &geom.dq_dq0 * &xi_v;
1100        q_v += &phi_v;
1101        let mut s1_u = &geom.d2q_dq02 * &xi_u;
1102        s1_u += &b1u;
1103        let mut s1_v = &geom.d2q_dq02 * &xi_v;
1104        s1_v += &b1v;
1105        let mut g2_u = &geom.d3q_dq03 * &xi_u;
1106        g2_u += &b2u;
1107        let mut g2_v = &geom.d3q_dq03 * &xi_v;
1108        g2_v += &b2v;
1109        let q_uv = &(&geom.d2q_dq02 * &(&xi_u * &xi_v)) + &(&b1u * &xi_v) + &(&b1v * &xi_u);
1110        let s1_uv = &(&geom.d3q_dq03 * &(&xi_u * &xi_v)) + &(&b2u * &xi_v) + &(&b2v * &xi_u);
1111        let g2_uv = &(&geom.d4q_dq04 * &(&xi_u * &xi_v)) + &(&b3u * &xi_v) + &(&b3v * &xi_u);
1112
1113        let basis_u = scale_matrix_rows(&geom.basis_d1, &xi_u)?;
1114        let basis_v = scale_matrix_rows(&geom.basis_d1, &xi_v)?;
1115        let basis_uv = scale_matrix_rows(&geom.basis_d2, &(&xi_u * &xi_v))?;
1116        let basis1_u = scale_matrix_rows(&geom.basis_d2, &xi_u)?;
1117        let basis1_v = scale_matrix_rows(&geom.basis_d2, &xi_v)?;
1118        let basis1_uv = scale_matrix_rows(&geom.basis_d3, &(&xi_u * &xi_v))?;
1119
1120        // Shared κ-aware second-directional row coefficients (κ' = κ(1−κ),
1121        // κ'' = κ(1−κ)(1−2κ), κ''' = κ''(1−2κ) − 2(κ')²): identical to the
1122        // matrix-free operator path, factored into one helper.
1123        let GlsWiggleSecondDirCoeffs {
1124            coeff_mm_uv,
1125            coeff_ml_uv,
1126            coeff_ll_uv,
1127            a_u,
1128            a_v,
1129            a_uv,
1130            c_u,
1131            c_v,
1132            c_uv,
1133            l_u,
1134            l_v,
1135            l_uv,
1136            dw_u,
1137            dw_v,
1138            dw_uv,
1139        } = gls_wiggle_second_directional_coeffs(
1140            &rows,
1141            &geom,
1142            &GlsWiggleDirPieces {
1143                zeta_u: &zeta_u,
1144                zeta_v: &zeta_v,
1145                q_u: &q_u,
1146                q_v: &q_v,
1147                q_uv: &q_uv,
1148                s1_u: &s1_u,
1149                s1_v: &s1_v,
1150                s1_uv: &s1_uv,
1151                g2_u: &g2_u,
1152                g2_v: &g2_v,
1153                g2_uv: &g2_uv,
1154            },
1155        );
1156
1157        let h_mm = xt_diag_x_dense(xmu, &coeff_mm_uv)?;
1158        let h_ml = xt_diag_y_dense(xmu, &coeff_ml_uv, x_ls)?;
1159        let h_ll = xt_diag_x_dense(x_ls, &coeff_ll_uv)?;
1160        let h_mw = xt_diag_y_dense(xmu, &a_uv, &geom.basis)?
1161            + &xt_diag_y_dense(xmu, &a_u, &basis_v)?
1162            + &xt_diag_y_dense(xmu, &a_v, &basis_u)?
1163            + &xt_diag_y_dense(xmu, &(&rows.w * &geom.dq_dq0), &basis_uv)?
1164            + &xt_diag_y_dense(xmu, &c_uv, &geom.basis_d1)?
1165            + &xt_diag_y_dense(xmu, &c_u, &basis1_v)?
1166            + &xt_diag_y_dense(xmu, &c_v, &basis1_u)?
1167            + &xt_diag_y_dense(xmu, &(-&rows.m), &basis1_uv)?;
1168        // OBSERVED ls↔wiggle cross 2κm (#1561): D²[X_ls^T diag(2κm) B] over the
1169        // coeff (l_uv), the single-drift cross terms (l_u·δ_v B + l_v·δ_u B), and
1170        // the value coeff on the second basis drift δ²B = basis_uv.
1171        let coeff_lw_b = 2.0 * &rows.kappa * &rows.m;
1172        let h_lw = xt_diag_y_dense(x_ls, &l_uv, &geom.basis)?
1173            + &xt_diag_y_dense(x_ls, &l_u, &basis_v)?
1174            + &xt_diag_y_dense(x_ls, &l_v, &basis_u)?
1175            + &xt_diag_y_dense(x_ls, &coeff_lw_b, &basis_uv)?;
1176        let a_ab = xt_diag_y_dense(&basis_uv, &rows.w, &geom.basis)?;
1177        let a_ij = xt_diag_y_dense(&basis_u, &rows.w, &basis_v)?;
1178        let a_iwj = xt_diag_y_dense(&basis_u, &dw_v, &geom.basis)?;
1179        let a_jwi = xt_diag_y_dense(&basis_v, &dw_u, &geom.basis)?;
1180        let h_ww = &a_ab
1181            + &a_ab.t()
1182            + &a_ij
1183            + a_ij.t()
1184            + &a_iwj
1185            + a_iwj.t()
1186            + &a_jwi
1187            + a_jwi.t()
1188            + &xt_diag_x_dense(&geom.basis, &dw_uv)?;
1189        Ok(Some(gaussian_pack_wiggle_joint_symmetrichessian(
1190            &h_mm, &h_ml, &h_mw, &h_ll, &h_lw, &h_ww,
1191        )))
1192    }
1193
1194    pub(crate) fn exact_newton_joint_psi_terms_from_designs(
1195        &self,
1196        block_states: &[ParameterBlockState],
1197        derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
1198        psi_index: usize,
1199        xmu: &Array2<f64>,
1200        x_ls: &Array2<f64>,
1201    ) -> Result<Option<gam_problem::ExactNewtonJointPsiTerms>, String> {
1202        let Some(dir_a) = self.exact_newton_joint_psi_direction(
1203            block_states,
1204            derivative_blocks,
1205            psi_index,
1206            xmu,
1207            x_ls,
1208            &self.policy,
1209        )?
1210        else {
1211            return Ok(None);
1212        };
1213        let q0 = &block_states[Self::BLOCK_MU].eta;
1214        let eta_ls = &block_states[Self::BLOCK_LOG_SIGMA].eta;
1215        let etaw = &block_states[Self::BLOCK_WIGGLE].eta;
1216        let betaw = &block_states[Self::BLOCK_WIGGLE].beta;
1217        let q = q0 + etaw;
1218        let geom = self.wiggle_geometry(q0.view(), betaw.view())?;
1219        let rows = self.get_or_compute_row_scalars(&q, eta_ls)?;
1220        let xmu_map = dir_a.x_primary_psi.as_linear_map_ref();
1221        let x_ls_map = dir_a.x_ls_psi.as_linear_map_ref();
1222
1223        let q_a = &geom.dq_dq0 * &dir_a.z_primary_psi;
1224        let s1_a = &geom.d2q_dq02 * &dir_a.z_primary_psi;
1225        let g2_a = &geom.d3q_dq03 * &dir_a.z_primary_psi;
1226        let basis_a = scale_matrix_rows(&geom.basis_d1, &dir_a.z_primary_psi)?;
1227        let basis1_a = scale_matrix_rows(&geom.basis_d2, &dir_a.z_primary_psi)?;
1228        // logb κ-chain on η_ls; e_a = ∂η_ls/∂ψ_a row-direction.
1229        let e_a = &dir_a.z_ls_psi;
1230        let amn = &rows.obs_weight - &rows.n;
1231        let dw_a = -2.0 * &rows.w * &rows.kappa * e_a;
1232        let dm_a = -(&rows.w * &q_a) - &(2.0 * &rows.m * &rows.kappa * e_a);
1233        let dn_a = -(2.0 * &rows.m * &q_a) - &(2.0 * &rows.n * &rows.kappa * e_a);
1234        let s_mu = -&rows.m * &geom.dq_dq0;
1235        let s_mu_a = -(&dm_a * &geom.dq_dq0) - &(&rows.m * &s1_a);
1236        let s_ls = &rows.kappa * &amn;
1237        let s_ls_a = &rows.kappa_prime * &(e_a * &amn) - &rows.kappa * &dn_a;
1238        let s_w = -&rows.m;
1239        let s_w_a = -&dm_a;
1240
1241        let objective_psi = (-&rows.m * &q_a + &s_ls * e_a).sum();
1242        let score_psi = gaussian_pack_wiggle_joint_score(
1243            &(xmu_map.transpose_mul(s_mu.view()) + fast_atv(xmu, &s_mu_a)),
1244            &(x_ls_map.transpose_mul(s_ls.view()) + fast_atv(x_ls, &s_ls_a)),
1245            &(fast_atv(&basis_a, &s_w) + fast_atv(&geom.basis, &s_w_a)),
1246        );
1247
1248        // OBSERVED joint Hessian blocks under logb (#1561; Wood–Pya–Säfken 2016).
1249        // μ AND the wiggle both enter the mean q = q0 + B(q0)·βw, so the whole
1250        // mean side carries the observed cross-curvature 2κm against log σ. The
1251        // μ-side Jacobian carries dq_dq0 (cf. s_mu = −m·dq_dq0), so the μ↔ls cross
1252        // is coeff_ml = 2κm·dq_dq0; the wiggle-side Jacobian is B, so the ls↔wiggle
1253        // cross is l = 2κm. coeff_ll = κ'(a−n)+2κ²n (Fisher 2κ²a overstated the
1254        // σ-block information on the near-flat scale surface and over-smoothed
1255        // log σ). Their ψ-derivatives differentiate κ (via e_a), m (via dm_a),
1256        // dq_dq0 (via s1_a), and n (via dn_a). The SCORE (s_mu/s_ls/s_w) is the
1257        // exact gradient, unchanged. At a flat/true-null σ surface n→a, m→0 so
1258        // observed → Fisher and the null behavior is unchanged.
1259        let coeff_mm = &rows.w * &geom.dq_dq0.mapv(|v| v * v) - &rows.m * &geom.d2q_dq02;
1260        let coeff_mm_a = &(&dw_a * &geom.dq_dq0.mapv(|v| v * v))
1261            + &(2.0 * &rows.w * &geom.dq_dq0 * &s1_a)
1262            - &(&dm_a * &geom.d2q_dq02)
1263            - &(&rows.m * &g2_a);
1264        let a_coef = 2.0 * &rows.kappa * &rows.kappa - &rows.kappa_prime;
1265        let coeff_ml = 2.0 * &rows.kappa * &rows.m * &geom.dq_dq0;
1266        let coeff_ml_a = &(2.0 * &rows.kappa_prime * e_a * &rows.m * &geom.dq_dq0)
1267            + &(&(2.0 * &rows.kappa * &dm_a * &geom.dq_dq0)
1268                + &(2.0 * &rows.kappa * &rows.m * &s1_a));
1269        let coeff_ll = &rows.kappa_prime * &amn + 2.0 * &rows.kappa * &rows.kappa * &rows.n;
1270        let coeff_ll_a = &(&(&rows.kappa_dprime * e_a * &amn)
1271            + &(4.0 * &rows.kappa * &rows.kappa_prime * e_a * &rows.n))
1272            + &(&a_coef * &dn_a);
1273        let a = &rows.w * &geom.dq_dq0;
1274        let a_a = &dw_a * &geom.dq_dq0 + &rows.w * &s1_a;
1275        let c = -&rows.m;
1276        let c_a = -&dm_a;
1277        let l = 2.0 * &rows.kappa * &rows.m;
1278        let l_a = &(2.0 * &rows.kappa_prime * e_a * &rows.m) + &(2.0 * &rows.kappa * &dm_a);
1279        let h_mm_a1 = weighted_crossprod_psi_maps(
1280            xmu_map,
1281            coeff_mm.view(),
1282            CustomFamilyPsiLinearMapRef::Dense(xmu),
1283        )?;
1284        let h_mm = &h_mm_a1 + &h_mm_a1.t() + &xt_diag_x_dense(xmu, &coeff_mm_a)?;
1285        let h_ml = weighted_crossprod_psi_maps(
1286            xmu_map,
1287            coeff_ml.view(),
1288            CustomFamilyPsiLinearMapRef::Dense(x_ls),
1289        )? + &weighted_crossprod_psi_maps(
1290            CustomFamilyPsiLinearMapRef::Dense(xmu),
1291            coeff_ml.view(),
1292            x_ls_map,
1293        )? + &xt_diag_y_dense(xmu, &coeff_ml_a, x_ls)?;
1294        let h_ll_a1 = weighted_crossprod_psi_maps(
1295            x_ls_map,
1296            coeff_ll.view(),
1297            CustomFamilyPsiLinearMapRef::Dense(x_ls),
1298        )?;
1299        let h_ll = &h_ll_a1 + &h_ll_a1.t() + &xt_diag_x_dense(x_ls, &coeff_ll_a)?;
1300        let h_mw = weighted_crossprod_psi_maps(
1301            xmu_map,
1302            a.view(),
1303            CustomFamilyPsiLinearMapRef::Dense(&geom.basis),
1304        )? + &xt_diag_y_dense(xmu, &a_a, &geom.basis)?
1305            + &xt_diag_y_dense(xmu, &a, &basis_a)?
1306            + &weighted_crossprod_psi_maps(
1307                xmu_map,
1308                c.view(),
1309                CustomFamilyPsiLinearMapRef::Dense(&geom.basis_d1),
1310            )?
1311            + &xt_diag_y_dense(xmu, &c_a, &geom.basis_d1)?
1312            + &xt_diag_y_dense(xmu, &c, &basis1_a)?;
1313        let h_lw = weighted_crossprod_psi_maps(
1314            x_ls_map,
1315            l.view(),
1316            CustomFamilyPsiLinearMapRef::Dense(&geom.basis),
1317        )? + &xt_diag_y_dense(x_ls, &l_a, &geom.basis)?
1318            + &xt_diag_y_dense(x_ls, &l, &basis_a)?;
1319        let h_ww_a1 = xt_diag_y_dense(&basis_a, &rows.w, &geom.basis)?;
1320        let h_ww = &h_ww_a1 + &h_ww_a1.t() + &xt_diag_x_dense(&geom.basis, &dw_a)?;
1321
1322        Ok(Some(gam_problem::ExactNewtonJointPsiTerms {
1323            objective_psi,
1324            score_psi,
1325            hessian_psi: gaussian_pack_wiggle_joint_symmetrichessian(
1326                &h_mm, &h_ml, &h_mw, &h_ll, &h_lw, &h_ww,
1327            ),
1328            hessian_psi_operator: None,
1329        }))
1330    }
1331
1332    pub(crate) fn exact_newton_joint_psisecond_order_terms_from_designs(
1333        &self,
1334        block_states: &[ParameterBlockState],
1335        derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
1336        psi_i: usize,
1337        psi_j: usize,
1338        xmu: &Array2<f64>,
1339        x_ls: &Array2<f64>,
1340    ) -> Result<Option<gam_problem::ExactNewtonJointPsiSecondOrderTerms>, String> {
1341        let Some(dir_a) = self.exact_newton_joint_psi_direction(
1342            block_states,
1343            derivative_blocks,
1344            psi_i,
1345            xmu,
1346            x_ls,
1347            &self.policy,
1348        )?
1349        else {
1350            return Ok(None);
1351        };
1352        let Some(dir_b) = self.exact_newton_joint_psi_direction(
1353            block_states,
1354            derivative_blocks,
1355            psi_j,
1356            xmu,
1357            x_ls,
1358            &self.policy,
1359        )?
1360        else {
1361            return Ok(None);
1362        };
1363        Ok(Some(
1364            self.exact_newton_joint_psisecond_order_terms_from_parts(
1365                block_states,
1366                derivative_blocks,
1367                &dir_a,
1368                &dir_b,
1369                xmu,
1370                x_ls,
1371            )?,
1372        ))
1373    }
1374
1375    pub(crate) fn exact_newton_joint_psisecond_order_terms_from_parts(
1376        &self,
1377        block_states: &[ParameterBlockState],
1378        derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
1379        dir_a: &LocationScaleJointPsiDirection,
1380        dir_b: &LocationScaleJointPsiDirection,
1381        xmu: &Array2<f64>,
1382        x_ls: &Array2<f64>,
1383    ) -> Result<gam_problem::ExactNewtonJointPsiSecondOrderTerms, String> {
1384        let second_drifts = self.exact_newton_joint_psisecond_design_drifts(
1385            block_states,
1386            derivative_blocks,
1387            dir_a,
1388            dir_b,
1389            xmu,
1390            x_ls,
1391        )?;
1392        let n = self.y.len();
1393        let xmu_a_map = dir_a.x_primary_psi.as_linear_map_ref();
1394        let x_ls_a_map = dir_a.x_ls_psi.as_linear_map_ref();
1395        let xmu_b_map = dir_b.x_primary_psi.as_linear_map_ref();
1396        let x_ls_b_map = dir_b.x_ls_psi.as_linear_map_ref();
1397        let xmu_ab_map = second_psi_linear_map(
1398            second_drifts.x_primary_ab_action.as_ref(),
1399            second_drifts.x_primary_ab.as_ref(),
1400            n,
1401            xmu.ncols(),
1402        );
1403        let x_ls_ab_map = second_psi_linear_map(
1404            second_drifts.x_ls_ab_action.as_ref(),
1405            second_drifts.x_ls_ab.as_ref(),
1406            n,
1407            x_ls.ncols(),
1408        );
1409        let q0 = &block_states[Self::BLOCK_MU].eta;
1410        let eta_ls = &block_states[Self::BLOCK_LOG_SIGMA].eta;
1411        let etaw = &block_states[Self::BLOCK_WIGGLE].eta;
1412        let betaw = &block_states[Self::BLOCK_WIGGLE].beta;
1413        let q = q0 + etaw;
1414        let geom = self.wiggle_geometry(q0.view(), betaw.view())?;
1415        let rows = self.get_or_compute_row_scalars(&q, eta_ls)?;
1416
1417        let q_a = &geom.dq_dq0 * &dir_a.z_primary_psi;
1418        let q_b = &geom.dq_dq0 * &dir_b.z_primary_psi;
1419        let q_ab = &(&geom.dq_dq0 * &second_drifts.z_primary_ab)
1420            + &(&geom.d2q_dq02 * &(&dir_a.z_primary_psi * &dir_b.z_primary_psi));
1421        let s1_a = &geom.d2q_dq02 * &dir_a.z_primary_psi;
1422        let s1_b = &geom.d2q_dq02 * &dir_b.z_primary_psi;
1423        let s1_ab = &(&geom.d3q_dq03 * &(&dir_a.z_primary_psi * &dir_b.z_primary_psi))
1424            + &(&geom.d2q_dq02 * &second_drifts.z_primary_ab);
1425        let g2_a = &geom.d3q_dq03 * &dir_a.z_primary_psi;
1426        let g2_b = &geom.d3q_dq03 * &dir_b.z_primary_psi;
1427        let g2_ab = &(&geom.d4q_dq04 * &(&dir_a.z_primary_psi * &dir_b.z_primary_psi))
1428            + &(&geom.d3q_dq03 * &second_drifts.z_primary_ab);
1429        let basis_a = scale_matrix_rows(&geom.basis_d1, &dir_a.z_primary_psi)?;
1430        let basis_b = scale_matrix_rows(&geom.basis_d1, &dir_b.z_primary_psi)?;
1431        let basis_ab = scale_matrix_rows(&geom.basis_d1, &second_drifts.z_primary_ab)?
1432            + &scale_matrix_rows(
1433                &geom.basis_d2,
1434                &(&dir_a.z_primary_psi * &dir_b.z_primary_psi),
1435            )?;
1436        let basis1_a = scale_matrix_rows(&geom.basis_d2, &dir_a.z_primary_psi)?;
1437        let basis1_b = scale_matrix_rows(&geom.basis_d2, &dir_b.z_primary_psi)?;
1438        let basis1_ab = scale_matrix_rows(&geom.basis_d2, &second_drifts.z_primary_ab)?
1439            + &scale_matrix_rows(
1440                &geom.basis_d3,
1441                &(&dir_a.z_primary_psi * &dir_b.z_primary_psi),
1442            )?;
1443
1444        // logb κ-chain on η_ls; κ' = κ(1−κ), κ'' = κ(1−κ)(1−2κ),
1445        // κ''' = κ''(1−2κ) − 2(κ')².
1446        let e_a = &dir_a.z_ls_psi;
1447        let e_b = &dir_b.z_ls_psi;
1448        let e_ab = &second_drifts.z_ls_ab;
1449        let amn = &rows.obs_weight - &rows.n;
1450        // 4κ² − 2κ' (∂²w/∂η² style coefficient when both directions hit η_ls).
1451        let four_k2_minus_2kpi = 4.0 * &rows.kappa * &rows.kappa - 2.0 * &rows.kappa_prime;
1452
1453        // Row drifts under logb. The η_ls direction picks up a κ on each step,
1454        // and η_ls·η_ls picks up (4κ²−2κ') from differentiating κ on the
1455        // second leg. The η_ab (z_ls_ab) leg uses just one κ from the chain.
1456        let dw_a = -2.0 * &rows.w * &rows.kappa * e_a;
1457        let dw_b = -2.0 * &rows.w * &rows.kappa * e_b;
1458        let dw_ab =
1459            &four_k2_minus_2kpi * &rows.w * &(e_a * e_b) - &(2.0 * &rows.w * &rows.kappa * e_ab);
1460        let dm_a = -(&rows.w * &q_a) - &(2.0 * &rows.m * &rows.kappa * e_a);
1461        let dm_b = -(&rows.w * &q_b) - &(2.0 * &rows.m * &rows.kappa * e_b);
1462        let dm_ab = &(2.0 * &rows.w * &rows.kappa * &(&q_a * e_b + &q_b * e_a))
1463            - &(&rows.w * &q_ab)
1464            + &(&four_k2_minus_2kpi * &rows.m * &(e_a * e_b))
1465            - &(2.0 * &rows.m * &rows.kappa * e_ab);
1466        let dn_a = -(2.0 * &rows.m * &q_a) - &(2.0 * &rows.n * &rows.kappa * e_a);
1467        let dn_b = -(2.0 * &rows.m * &q_b) - &(2.0 * &rows.n * &rows.kappa * e_b);
1468        let dn_ab = &(2.0 * &rows.w * &(&q_a * &q_b))
1469            + &(4.0 * &rows.m * &rows.kappa * &(&q_a * e_b + &q_b * e_a))
1470            - &(2.0 * &rows.m * &q_ab)
1471            + &(&four_k2_minus_2kpi * &rows.n * &(e_a * e_b))
1472            - &(2.0 * &rows.n * &rows.kappa * e_ab);
1473
1474        let s_mu = -&rows.m * &geom.dq_dq0;
1475        let s_mu_a = -(&dm_a * &geom.dq_dq0) - &(&rows.m * &s1_a);
1476        let s_mu_b = -(&dm_b * &geom.dq_dq0) - &(&rows.m * &s1_b);
1477        let s_mu_ab =
1478            -(&dm_ab * &geom.dq_dq0) - &(&dm_a * &s1_b) - &(&dm_b * &s1_a) - &(&rows.m * &s1_ab);
1479        // score_ls = κ(a−n); ψ derivatives carry κ' / κ'' from chain on κ.
1480        let s_ls = &rows.kappa * &amn;
1481        let s_ls_a = &rows.kappa_prime * &(e_a * &amn) - &rows.kappa * &dn_a;
1482        let s_ls_b = &rows.kappa_prime * &(e_b * &amn) - &rows.kappa * &dn_b;
1483        // s_ls_ab = κ''·e_a·e_b·(a−n) + κ'·e_ab·(a−n)
1484        //         − κ'·(e_a·n_b + e_b·n_a) − κ·n_ab
1485        let s_ls_ab = &rows.kappa_dprime * &(e_a * e_b) * &amn + &rows.kappa_prime * e_ab * &amn
1486            - &rows.kappa_prime * &(e_a * &dn_b + e_b * &dn_a)
1487            - &rows.kappa * &dn_ab;
1488        let s_w = -&rows.m;
1489        let s_w_a = -&dm_a;
1490        let s_w_b = -&dm_b;
1491        let s_w_ab = -&dm_ab;
1492
1493        let objective_psi_psi = (&rows.w * &(&q_a * &q_b)
1494            + &(2.0 * &rows.m * &rows.kappa * &(&q_a * e_b + &q_b * e_a))
1495            + &((2.0 * &rows.kappa * &rows.kappa * &rows.n + &rows.kappa_prime * &amn)
1496                * &(e_a * e_b))
1497            - &(&rows.m * &q_ab)
1498            + &(&rows.kappa * &amn * e_ab))
1499            .sum();
1500
1501        let score_psi_psi = gaussian_pack_wiggle_joint_score(
1502            &(xmu_ab_map.transpose_mul(s_mu.view())
1503                + xmu_a_map.transpose_mul(s_mu_b.view())
1504                + xmu_b_map.transpose_mul(s_mu_a.view())
1505                + fast_atv(xmu, &s_mu_ab)),
1506            &(x_ls_ab_map.transpose_mul(s_ls.view())
1507                + x_ls_a_map.transpose_mul(s_ls_b.view())
1508                + x_ls_b_map.transpose_mul(s_ls_a.view())
1509                + fast_atv(x_ls, &s_ls_ab)),
1510            &(fast_atv(&basis_ab, &s_w)
1511                + fast_atv(&basis_a, &s_w_b)
1512                + fast_atv(&basis_b, &s_w_a)
1513                + fast_atv(&geom.basis, &s_w_ab)),
1514        );
1515
1516        // OBSERVED joint Hessian blocks under logb (#1561; Wood–Pya–Säfken 2016).
1517        // μ AND the wiggle enter the mean q = q0 + B·βw, so the whole mean side
1518        // carries the observed cross-curvature 2κm against log σ: the μ-side
1519        // Jacobian carries dq_dq0 (cf. s_mu = −m·dq_dq0) → coeff_ml = 2κm·dq_dq0,
1520        // the wiggle-side Jacobian is B → l = 2κm. coeff_ll = κ'(a−n)+2κ²n
1521        // (Fisher 2κ²a over-smoothed log σ). Their 1st/2nd ψ-derivatives
1522        // differentiate κ (e_a/e_b/e_ab), m (dm_*), dq_dq0 (s1_*), and n (dn_*);
1523        // κ''' = κ''(1−2κ)−2κ'². The SCORE channels stay the exact gradient.
1524        let d = &geom.dq_dq0;
1525        let a_coef = 2.0 * &rows.kappa * &rows.kappa - &rows.kappa_prime;
1526        let ktp = &(&rows.kappa_dprime * &(1.0 - 2.0 * &rows.kappa))
1527            - &(2.0 * &rows.kappa_prime * &rows.kappa_prime);
1528        let four_kkp_minus_kdp = 4.0 * &rows.kappa * &rows.kappa_prime - &rows.kappa_dprime;
1529        let coeff_mm = &rows.w * &geom.dq_dq0.mapv(|v| v * v) - &rows.m * &geom.d2q_dq02;
1530        let coeff_ml = 2.0 * &rows.kappa * &rows.m * d;
1531        let coeff_ll = &rows.kappa_prime * &amn + 2.0 * &rows.kappa * &rows.kappa * &rows.n;
1532        // coeff_mm_a/b/ab: structurally κ-free; correctness now follows from
1533        // dw_a/_b/_ab and dm_a/_b/_ab carrying the κ chain on η_ls (above).
1534        let coeff_mm_a = &(&dw_a * &geom.dq_dq0.mapv(|v| v * v))
1535            + &(2.0 * &rows.w * &geom.dq_dq0 * &s1_a)
1536            - &(&dm_a * &geom.d2q_dq02)
1537            - &(&rows.m * &g2_a);
1538        let coeff_mm_b = &(&dw_b * &geom.dq_dq0.mapv(|v| v * v))
1539            + &(2.0 * &rows.w * &geom.dq_dq0 * &s1_b)
1540            - &(&dm_b * &geom.d2q_dq02)
1541            - &(&rows.m * &g2_b);
1542        let coeff_mm_ab = &(&dw_ab * &geom.dq_dq0.mapv(|v| v * v))
1543            + &(2.0 * &dw_a * &geom.dq_dq0 * &s1_b)
1544            + &(2.0 * &dw_b * &geom.dq_dq0 * &s1_a)
1545            + &(2.0 * &rows.w * &s1_a * &s1_b)
1546            + &(2.0 * &rows.w * &geom.dq_dq0 * &s1_ab)
1547            - &(&dm_ab * &geom.d2q_dq02)
1548            - &(&dm_a * &g2_b)
1549            - &(&dm_b * &g2_a)
1550            - &(&rows.m * &g2_ab);
1551        // coeff_ml = 2κm·dq_dq0 ψ-derivatives (D²(2κmD)).
1552        let coeff_ml_a = &(2.0 * &rows.kappa_prime * e_a * &rows.m * d)
1553            + &(&(2.0 * &rows.kappa * &dm_a * d) + &(2.0 * &rows.kappa * &rows.m * &s1_a));
1554        let coeff_ml_b = &(2.0 * &rows.kappa_prime * e_b * &rows.m * d)
1555            + &(&(2.0 * &rows.kappa * &dm_b * d) + &(2.0 * &rows.kappa * &rows.m * &s1_b));
1556        let coeff_ml_ab = 2.0
1557            * &(&(&(&(&(&(&rows.kappa_dprime * &(e_a * e_b) * &rows.m * d)
1558                + &(&rows.kappa_prime * e_ab * &rows.m * d))
1559                + &(&rows.kappa_prime * d * &(&(e_a * &dm_b) + &(e_b * &dm_a))))
1560                + &(&rows.kappa_prime * &rows.m * &(&(e_a * &s1_b) + &(e_b * &s1_a))))
1561                + &(&rows.kappa * d * &dm_ab))
1562                + &(&rows.kappa * &(&(&dm_a * &s1_b) + &(&dm_b * &s1_a))))
1563            + &(2.0 * &rows.kappa * &rows.m * &s1_ab);
1564        // coeff_ll = κ'(a−n)+2κ²n ψ-derivatives (via dn_*).
1565        let coeff_ll_a = &(&(&rows.kappa_dprime * e_a * &amn)
1566            + &(4.0 * &rows.kappa * &rows.kappa_prime * e_a * &rows.n))
1567            + &(&a_coef * &dn_a);
1568        let coeff_ll_b = &(&(&rows.kappa_dprime * e_b * &amn)
1569            + &(4.0 * &rows.kappa * &rows.kappa_prime * e_b * &rows.n))
1570            + &(&a_coef * &dn_b);
1571        // κ'''(a−n)e_ae_b + 4(κ'²+κκ'')n·e_ae_b + (4κκ'−κ'')(e_a dn_b + e_b dn_a)
1572        // + (2κ²−κ')dn_ab + (κ''(a−n)+4κκ'n)e_ab.
1573        let coeff_ll_ab = &(&(&(&(&ktp * &amn * &(e_a * e_b))
1574            + &(4.0
1575                * &(&(&rows.kappa_prime * &rows.kappa_prime)
1576                    + &(&rows.kappa * &rows.kappa_dprime))
1577                * &rows.n
1578                * &(e_a * e_b)))
1579            + &(&four_kkp_minus_kdp * &(&(e_a * &dn_b) + &(e_b * &dn_a))))
1580            + &(&a_coef * &dn_ab))
1581            + &(&(&(&rows.kappa_dprime * &amn)
1582                + &(4.0 * &rows.kappa * &rows.kappa_prime * &rows.n))
1583                * e_ab);
1584        let a = &rows.w * &geom.dq_dq0;
1585        let a_a = &dw_a * &geom.dq_dq0 + &rows.w * &s1_a;
1586        let a_b = &dw_b * &geom.dq_dq0 + &rows.w * &s1_b;
1587        let a_ab = &dw_ab * &geom.dq_dq0 + &dw_a * &s1_b + &dw_b * &s1_a + &rows.w * &s1_ab;
1588        let c = -&rows.m;
1589        let c_a = -&dm_a;
1590        let c_b = -&dm_b;
1591        let c_ab = -&dm_ab;
1592        // l = 2κm (ls↔wiggle) ψ-derivatives (D²(2κm)).
1593        let l = 2.0 * &rows.kappa * &rows.m;
1594        let l_a = &(2.0 * &rows.kappa_prime * e_a * &rows.m) + &(2.0 * &rows.kappa * &dm_a);
1595        let l_b = &(2.0 * &rows.kappa_prime * e_b * &rows.m) + &(2.0 * &rows.kappa * &dm_b);
1596        let l_ab = &(&(2.0 * &rows.kappa_dprime * &(e_a * e_b) * &rows.m)
1597            + &(2.0 * &rows.kappa_prime * &(&(e_a * &dm_b) + &(e_b * &dm_a))))
1598            + &(2.0 * &rows.kappa * &dm_ab);
1599
1600        let hmm_ab = weighted_crossprod_psi_maps(
1601            xmu_ab_map,
1602            coeff_mm.view(),
1603            CustomFamilyPsiLinearMapRef::Dense(xmu),
1604        )?;
1605        let hmm_ij = weighted_crossprod_psi_maps(xmu_a_map, coeff_mm.view(), xmu_b_map)?;
1606        let hmm_iwj = weighted_crossprod_psi_maps(
1607            xmu_a_map,
1608            coeff_mm_b.view(),
1609            CustomFamilyPsiLinearMapRef::Dense(xmu),
1610        )?;
1611        let hmm_jwi = weighted_crossprod_psi_maps(
1612            xmu_b_map,
1613            coeff_mm_a.view(),
1614            CustomFamilyPsiLinearMapRef::Dense(xmu),
1615        )?;
1616        let h_mm = &hmm_ab
1617            + &hmm_ab.t()
1618            + &hmm_ij
1619            + hmm_ij.t()
1620            + &hmm_iwj
1621            + hmm_iwj.t()
1622            + &hmm_jwi
1623            + hmm_jwi.t()
1624            + &xt_diag_x_dense(xmu, &coeff_mm_ab)?;
1625        let h_ml = weighted_crossprod_psi_maps(
1626            xmu_ab_map,
1627            coeff_ml.view(),
1628            CustomFamilyPsiLinearMapRef::Dense(x_ls),
1629        )? + &weighted_crossprod_psi_maps(xmu_a_map, coeff_ml.view(), x_ls_b_map)?
1630            + &weighted_crossprod_psi_maps(xmu_b_map, coeff_ml.view(), x_ls_a_map)?
1631            + &weighted_crossprod_psi_maps(
1632                xmu_a_map,
1633                coeff_ml_b.view(),
1634                CustomFamilyPsiLinearMapRef::Dense(x_ls),
1635            )?
1636            + &weighted_crossprod_psi_maps(
1637                xmu_b_map,
1638                coeff_ml_a.view(),
1639                CustomFamilyPsiLinearMapRef::Dense(x_ls),
1640            )?
1641            + &weighted_crossprod_psi_maps(
1642                CustomFamilyPsiLinearMapRef::Dense(xmu),
1643                coeff_ml_a.view(),
1644                x_ls_b_map,
1645            )?
1646            + &weighted_crossprod_psi_maps(
1647                CustomFamilyPsiLinearMapRef::Dense(xmu),
1648                coeff_ml_b.view(),
1649                x_ls_a_map,
1650            )?
1651            + &xt_diag_y_dense(xmu, &coeff_ml_ab, x_ls)?
1652            + &weighted_crossprod_psi_maps(
1653                CustomFamilyPsiLinearMapRef::Dense(xmu),
1654                coeff_ml.view(),
1655                x_ls_ab_map,
1656            )?;
1657        let hll_ab = weighted_crossprod_psi_maps(
1658            x_ls_ab_map,
1659            coeff_ll.view(),
1660            CustomFamilyPsiLinearMapRef::Dense(x_ls),
1661        )?;
1662        let hll_ij = weighted_crossprod_psi_maps(x_ls_a_map, coeff_ll.view(), x_ls_b_map)?;
1663        let hll_iwj = weighted_crossprod_psi_maps(
1664            x_ls_a_map,
1665            coeff_ll_b.view(),
1666            CustomFamilyPsiLinearMapRef::Dense(x_ls),
1667        )?;
1668        let hll_jwi = weighted_crossprod_psi_maps(
1669            x_ls_b_map,
1670            coeff_ll_a.view(),
1671            CustomFamilyPsiLinearMapRef::Dense(x_ls),
1672        )?;
1673        let h_ll = &hll_ab
1674            + &hll_ab.t()
1675            + &hll_ij
1676            + hll_ij.t()
1677            + &hll_iwj
1678            + hll_iwj.t()
1679            + &hll_jwi
1680            + hll_jwi.t()
1681            + &xt_diag_x_dense(x_ls, &coeff_ll_ab)?;
1682        let h_mw = weighted_crossprod_psi_maps(
1683            xmu_ab_map,
1684            a.view(),
1685            CustomFamilyPsiLinearMapRef::Dense(&geom.basis),
1686        )? + &weighted_crossprod_psi_maps(
1687            xmu_a_map,
1688            a_b.view(),
1689            CustomFamilyPsiLinearMapRef::Dense(&geom.basis),
1690        )? + &weighted_crossprod_psi_maps(
1691            xmu_a_map,
1692            a.view(),
1693            CustomFamilyPsiLinearMapRef::Dense(&basis_b),
1694        )? + &weighted_crossprod_psi_maps(
1695            xmu_b_map,
1696            a_a.view(),
1697            CustomFamilyPsiLinearMapRef::Dense(&geom.basis),
1698        )? + &xt_diag_y_dense(xmu, &a_ab, &geom.basis)?
1699            + &xt_diag_y_dense(xmu, &a_a, &basis_b)?
1700            + &weighted_crossprod_psi_maps(
1701                xmu_b_map,
1702                a.view(),
1703                CustomFamilyPsiLinearMapRef::Dense(&basis_a),
1704            )?
1705            + &xt_diag_y_dense(xmu, &a_b, &basis_a)?
1706            + &xt_diag_y_dense(xmu, &a, &basis_ab)?
1707            + &weighted_crossprod_psi_maps(
1708                xmu_ab_map,
1709                c.view(),
1710                CustomFamilyPsiLinearMapRef::Dense(&geom.basis_d1),
1711            )?
1712            + &weighted_crossprod_psi_maps(
1713                xmu_a_map,
1714                c_b.view(),
1715                CustomFamilyPsiLinearMapRef::Dense(&geom.basis_d1),
1716            )?
1717            + &weighted_crossprod_psi_maps(
1718                xmu_a_map,
1719                c.view(),
1720                CustomFamilyPsiLinearMapRef::Dense(&basis1_b),
1721            )?
1722            + &weighted_crossprod_psi_maps(
1723                xmu_b_map,
1724                c_a.view(),
1725                CustomFamilyPsiLinearMapRef::Dense(&geom.basis_d1),
1726            )?
1727            + &xt_diag_y_dense(xmu, &c_ab, &geom.basis_d1)?
1728            + &xt_diag_y_dense(xmu, &c_a, &basis1_b)?
1729            + &weighted_crossprod_psi_maps(
1730                xmu_b_map,
1731                c.view(),
1732                CustomFamilyPsiLinearMapRef::Dense(&basis1_a),
1733            )?
1734            + &xt_diag_y_dense(xmu, &c_b, &basis1_a)?
1735            + &xt_diag_y_dense(xmu, &c, &basis1_ab)?;
1736        let h_lw = weighted_crossprod_psi_maps(
1737            x_ls_ab_map,
1738            l.view(),
1739            CustomFamilyPsiLinearMapRef::Dense(&geom.basis),
1740        )? + &weighted_crossprod_psi_maps(
1741            x_ls_a_map,
1742            l_b.view(),
1743            CustomFamilyPsiLinearMapRef::Dense(&geom.basis),
1744        )? + &weighted_crossprod_psi_maps(
1745            x_ls_a_map,
1746            l.view(),
1747            CustomFamilyPsiLinearMapRef::Dense(&basis_b),
1748        )? + &weighted_crossprod_psi_maps(
1749            x_ls_b_map,
1750            l_a.view(),
1751            CustomFamilyPsiLinearMapRef::Dense(&geom.basis),
1752        )? + &xt_diag_y_dense(x_ls, &l_ab, &geom.basis)?
1753            + &xt_diag_y_dense(x_ls, &l_a, &basis_b)?
1754            + &weighted_crossprod_psi_maps(
1755                x_ls_b_map,
1756                l.view(),
1757                CustomFamilyPsiLinearMapRef::Dense(&basis_a),
1758            )?
1759            + &xt_diag_y_dense(x_ls, &l_b, &basis_a)?
1760            + &xt_diag_y_dense(x_ls, &l, &basis_ab)?;
1761        let hww_ab = xt_diag_y_dense(&basis_ab, &rows.w, &geom.basis)?;
1762        let hww_ij = xt_diag_y_dense(&basis_a, &rows.w, &basis_b)?;
1763        let hww_iwj = xt_diag_y_dense(&basis_a, &dw_b, &geom.basis)?;
1764        let hww_jwi = xt_diag_y_dense(&basis_b, &dw_a, &geom.basis)?;
1765        let h_ww = &hww_ab
1766            + &hww_ab.t()
1767            + &hww_ij
1768            + hww_ij.t()
1769            + &hww_iwj
1770            + hww_iwj.t()
1771            + &hww_jwi
1772            + hww_jwi.t()
1773            + &xt_diag_x_dense(&geom.basis, &dw_ab)?;
1774
1775        Ok(gam_problem::ExactNewtonJointPsiSecondOrderTerms {
1776            objective_psi_psi,
1777            score_psi_psi,
1778            hessian_psi_psi: gaussian_pack_wiggle_joint_symmetrichessian(
1779                &h_mm, &h_ml, &h_mw, &h_ll, &h_lw, &h_ww,
1780            ),
1781            hessian_psi_psi_operator: None,
1782        })
1783    }
1784
1785    pub(crate) fn exact_newton_joint_psihessian_directional_derivative_from_designs(
1786        &self,
1787        block_states: &[ParameterBlockState],
1788        derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
1789        psi_index: usize,
1790        d_beta_flat: &Array1<f64>,
1791        xmu: &Array2<f64>,
1792        x_ls: &Array2<f64>,
1793    ) -> Result<Option<Array2<f64>>, String> {
1794        let Some(dir_a) = self.exact_newton_joint_psi_direction(
1795            block_states,
1796            derivative_blocks,
1797            psi_index,
1798            xmu,
1799            x_ls,
1800            &self.policy,
1801        )?
1802        else {
1803            return Ok(None);
1804        };
1805        Ok(Some(
1806            self.exact_newton_joint_psihessian_directional_derivative_from_parts(
1807                block_states,
1808                &dir_a,
1809                d_beta_flat,
1810                xmu,
1811                x_ls,
1812            )?,
1813        ))
1814    }
1815
1816    pub(crate) fn exact_newton_joint_psihessian_directional_derivative_from_parts(
1817        &self,
1818        block_states: &[ParameterBlockState],
1819        dir_a: &LocationScaleJointPsiDirection,
1820        d_beta_flat: &Array1<f64>,
1821        xmu: &Array2<f64>,
1822        x_ls: &Array2<f64>,
1823    ) -> Result<Array2<f64>, String> {
1824        let pmu = xmu.ncols();
1825        let p_ls = x_ls.ncols();
1826        let xmu_map = dir_a.x_primary_psi.as_linear_map_ref();
1827        let x_ls_map = dir_a.x_ls_psi.as_linear_map_ref();
1828        let q0 = &block_states[Self::BLOCK_MU].eta;
1829        let eta_ls = &block_states[Self::BLOCK_LOG_SIGMA].eta;
1830        let etaw = &block_states[Self::BLOCK_WIGGLE].eta;
1831        let betaw = &block_states[Self::BLOCK_WIGGLE].beta;
1832        let layout = GamlssBetaLayout::withwiggle(pmu, p_ls, betaw.len());
1833        let (umu, u_ls, uw) = layout.split_three(
1834            d_beta_flat,
1835            "GaussianLocationScaleWiggleFamily joint psi hessian directional derivative",
1836        )?;
1837        let q = q0 + etaw;
1838        let geom = self.wiggle_geometry(q0.view(), betaw.view())?;
1839        let rows = self.get_or_compute_row_scalars(&q, eta_ls)?;
1840
1841        let xi = fast_av(xmu, &umu);
1842        let zeta = fast_av(x_ls, &u_ls);
1843        let zmu_a_u = xmu_map.forward_mul(umu.view());
1844        let zls_a_u = x_ls_map.forward_mul(u_ls.view());
1845        let b1u = fast_av(&geom.basis_d1, &uw);
1846        let b2u = fast_av(&geom.basis_d2, &uw);
1847        let b3u = fast_av(&geom.basis_d3, &uw);
1848
1849        let q_u = &(&geom.dq_dq0 * &xi) + &fast_av(&geom.basis, &uw);
1850        let s1_u = &(&geom.d2q_dq02 * &xi) + &b1u;
1851        let g2_u = &(&geom.d3q_dq03 * &xi) + &b2u;
1852        let g3_u = &(&geom.d4q_dq04 * &xi) + &b3u;
1853
1854        let q_a = &geom.dq_dq0 * &dir_a.z_primary_psi;
1855        let s1_a = &geom.d2q_dq02 * &dir_a.z_primary_psi;
1856        let g2_a = &geom.d3q_dq03 * &dir_a.z_primary_psi;
1857        let q_a_u = &(&s1_u * &dir_a.z_primary_psi) + &(&geom.dq_dq0 * &zmu_a_u);
1858        let s1_a_u = &(&g2_u * &dir_a.z_primary_psi) + &(&geom.d2q_dq02 * &zmu_a_u);
1859        let g2_a_u = &(&g3_u * &dir_a.z_primary_psi) + &(&geom.d3q_dq03 * &zmu_a_u);
1860
1861        let basis_u = scale_matrix_rows(&geom.basis_d1, &xi)?;
1862        let basis1_u = scale_matrix_rows(&geom.basis_d2, &xi)?;
1863        let basis_a = scale_matrix_rows(&geom.basis_d1, &dir_a.z_primary_psi)?;
1864        let basis1_a = scale_matrix_rows(&geom.basis_d2, &dir_a.z_primary_psi)?;
1865        let basis_a_u = scale_matrix_rows(&geom.basis_d2, &(&xi * &dir_a.z_primary_psi))?
1866            + &scale_matrix_rows(&geom.basis_d1, &zmu_a_u)?;
1867        let basis1_a_u = scale_matrix_rows(&geom.basis_d3, &(&xi * &dir_a.z_primary_psi))?
1868            + &scale_matrix_rows(&geom.basis_d2, &zmu_a_u)?;
1869
1870        // logb κ-chain on η_ls; e_a = ψ_a's η_ls direction, ζ = β-direction.
1871        // η_au = zls_a_u is the second mixed derivative (β·ψ).
1872        let e_a = &dir_a.z_ls_psi;
1873        let four_k2_minus_2kpi = 4.0 * &rows.kappa * &rows.kappa - 2.0 * &rows.kappa_prime;
1874        let dw_u = -2.0 * &rows.w * &rows.kappa * &zeta;
1875        let dm_u = -(&rows.w * &q_u) - &(2.0 * &rows.m * &rows.kappa * &zeta);
1876        let dw_a = -2.0 * &rows.w * &rows.kappa * e_a;
1877        let dm_a = -(&rows.w * &q_a) - &(2.0 * &rows.m * &rows.kappa * e_a);
1878        let dw_a_u = &four_k2_minus_2kpi * &rows.w * &(e_a * &zeta)
1879            - &(2.0 * &rows.w * &rows.kappa * &zls_a_u);
1880        let dm_a_u = &(2.0 * &rows.w * &rows.kappa * &(&q_a * &zeta + &q_u * e_a))
1881            - &(&rows.w * &q_a_u)
1882            + &(&four_k2_minus_2kpi * &rows.m * &(e_a * &zeta))
1883            - &(2.0 * &rows.m * &rows.kappa * &zls_a_u);
1884
1885        let coeff_mm_u = &(&dw_u * &geom.dq_dq0.mapv(|v| v * v))
1886            + &(2.0 * &rows.w * &geom.dq_dq0 * &s1_u)
1887            - &(&dm_u * &geom.d2q_dq02)
1888            - &(&rows.m * &g2_u);
1889        // OBSERVED joint Hessian blocks (#1561). coeff_ml = 2κm·dq_dq0, l = 2κm,
1890        // coeff_ll = κ'(a−n)+2κ²n. This path is the β-directional derivative
1891        // (along u) of H, further differentiated by ψ (direction a). δn is
1892        // needed along u, a, and mixed a_u; κ''' = κ''(1−2κ)−2κ'².
1893        let d = &geom.dq_dq0;
1894        let amn = &rows.obs_weight - &rows.n;
1895        let a_coef = 2.0 * &rows.kappa * &rows.kappa - &rows.kappa_prime;
1896        let ktp = &(&rows.kappa_dprime * &(1.0 - 2.0 * &rows.kappa))
1897            - &(2.0 * &rows.kappa_prime * &rows.kappa_prime);
1898        let four_kkp_minus_kdp = 4.0 * &rows.kappa * &rows.kappa_prime - &rows.kappa_dprime;
1899        let dn_u = -(2.0 * &rows.m * &q_u) - &(2.0 * &rows.n * &rows.kappa * &zeta);
1900        let dn_a = -(2.0 * &rows.m * &q_a) - &(2.0 * &rows.n * &rows.kappa * e_a);
1901        let dn_a_u = &(&(&(2.0 * &rows.w * &(&q_a * &q_u))
1902            + &(4.0 * &rows.m * &rows.kappa * &(&(&q_a * &zeta) + &(&q_u * e_a))))
1903            - &(2.0 * &rows.m * &q_a_u))
1904            + &(&(&four_k2_minus_2kpi * &rows.n * &(e_a * &zeta))
1905                - &(2.0 * &rows.n * &rows.kappa * &zls_a_u));
1906        // coeff_ml_u = D_u(2κm·dq_dq0): differentiate κ (ζ), m (dm_u), dq_dq0 (s1_u).
1907        let coeff_ml_u = &(2.0 * &rows.kappa_prime * &zeta * &rows.m * d)
1908            + &(&(2.0 * &rows.kappa * &dm_u * d) + &(2.0 * &rows.kappa * &rows.m * &s1_u));
1909        // coeff_ll_u = D_u(κ'(a−n)+2κ²n) = κ''ζ(a−n)+4κκ'ζn+(2κ²−κ')dn_u.
1910        let coeff_ll_u = &(&(&rows.kappa_dprime * &zeta * &amn)
1911            + &(4.0 * &rows.kappa * &rows.kappa_prime * &zeta * &rows.n))
1912            + &(&a_coef * &dn_u);
1913        let coeff_mm_a_u = &(&dw_a_u * &geom.dq_dq0.mapv(|v| v * v))
1914            + &(2.0 * &dw_a * &geom.dq_dq0 * &s1_u)
1915            + &(2.0 * &dw_u * &geom.dq_dq0 * &s1_a)
1916            + &(2.0 * &rows.w * &s1_u * &s1_a)
1917            + &(2.0 * &rows.w * &geom.dq_dq0 * &s1_a_u)
1918            - &(&dm_a_u * &geom.d2q_dq02)
1919            - &(&dm_a * &g2_u)
1920            - &(&dm_u * &g2_a)
1921            - &(&rows.m * &g2_a_u);
1922        // coeff_ml_a_u = D_a D_u(2κm·dq_dq0): mixed second, with the zls_a_u leg
1923        // for the ψ×β η_ls second drift.
1924        let coeff_ml_a_u = 2.0
1925            * &(&(&(&(&(&(&rows.kappa_dprime * &(e_a * &zeta) * &rows.m * d)
1926                + &(&rows.kappa_prime * &zls_a_u * &rows.m * d))
1927                + &(&rows.kappa_prime * d * &(&(e_a * &dm_u) + &(&zeta * &dm_a))))
1928                + &(&rows.kappa_prime * &rows.m * &(&(e_a * &s1_u) + &(&zeta * &s1_a))))
1929                + &(&rows.kappa * d * &dm_a_u))
1930                + &(&rows.kappa * &(&(&dm_a * &s1_u) + &(&dm_u * &s1_a))))
1931            + &(2.0 * &rows.kappa * &rows.m * &s1_a_u);
1932        // coeff_ll_a_u = D_a D_u(κ'(a−n)+2κ²n): κ'''(a−n)e_aζ + 4(κ'²+κκ'')n·e_aζ
1933        // + (4κκ'−κ'')(e_a dn_u + ζ dn_a) + (2κ²−κ')dn_a_u + (κ''(a−n)+4κκ'n)zls_a_u.
1934        let coeff_ll_a_u = &(&(&(&(&ktp * &amn * &(e_a * &zeta))
1935            + &(4.0
1936                * &(&(&rows.kappa_prime * &rows.kappa_prime)
1937                    + &(&rows.kappa * &rows.kappa_dprime))
1938                * &rows.n
1939                * &(e_a * &zeta)))
1940            + &(&four_kkp_minus_kdp * &(&(e_a * &dn_u) + &(&zeta * &dn_a))))
1941            + &(&a_coef * &dn_a_u))
1942            + &(&(&(&rows.kappa_dprime * &amn)
1943                + &(4.0 * &rows.kappa * &rows.kappa_prime * &rows.n))
1944                * &zls_a_u);
1945
1946        let a = &rows.w * &geom.dq_dq0;
1947        let a_u = &dw_u * &geom.dq_dq0 + &rows.w * &s1_u;
1948        let a_a = &dw_a * &geom.dq_dq0 + &rows.w * &s1_a;
1949        let a_a_u = &dw_a_u * &geom.dq_dq0 + &dw_a * &s1_u + &dw_u * &s1_a + &rows.w * &s1_a_u;
1950        let c = -&rows.m;
1951        let c_u = -&dm_u;
1952        let c_a = -&dm_a;
1953        let c_a_u = -&dm_a_u;
1954        // OBSERVED ls↔wiggle cross l = 2κm (#1561): β-drift l_u, ψ l_a, mixed l_a_u.
1955        let l = 2.0 * &rows.kappa * &rows.m;
1956        let l_u = &(2.0 * &rows.kappa_prime * &zeta * &rows.m) + &(2.0 * &rows.kappa * &dm_u);
1957        let l_a = &(2.0 * &rows.kappa_prime * e_a * &rows.m) + &(2.0 * &rows.kappa * &dm_a);
1958        let l_a_u = &(&(2.0 * &rows.kappa_dprime * &(e_a * &zeta) * &rows.m)
1959            + &(&(2.0 * &rows.kappa_prime * &zls_a_u * &rows.m)
1960                + &(2.0 * &rows.kappa_prime * &(&(e_a * &dm_u) + &(&zeta * &dm_a)))))
1961            + &(2.0 * &rows.kappa * &dm_a_u);
1962
1963        let hmm_a1 = weighted_crossprod_psi_maps(
1964            xmu_map,
1965            coeff_mm_u.view(),
1966            CustomFamilyPsiLinearMapRef::Dense(xmu),
1967        )?;
1968        let h_mm = &hmm_a1 + &hmm_a1.t() + &xt_diag_x_dense(xmu, &coeff_mm_a_u)?;
1969        let h_ml = weighted_crossprod_psi_maps(
1970            xmu_map,
1971            coeff_ml_u.view(),
1972            CustomFamilyPsiLinearMapRef::Dense(x_ls),
1973        )? + &weighted_crossprod_psi_maps(
1974            CustomFamilyPsiLinearMapRef::Dense(xmu),
1975            coeff_ml_u.view(),
1976            x_ls_map,
1977        )? + &xt_diag_y_dense(xmu, &coeff_ml_a_u, x_ls)?;
1978        let hll_a1 = weighted_crossprod_psi_maps(
1979            x_ls_map,
1980            coeff_ll_u.view(),
1981            CustomFamilyPsiLinearMapRef::Dense(x_ls),
1982        )?;
1983        let h_ll = &hll_a1 + &hll_a1.t() + &xt_diag_x_dense(x_ls, &coeff_ll_a_u)?;
1984        let h_mw = weighted_crossprod_psi_maps(
1985            xmu_map,
1986            a_u.view(),
1987            CustomFamilyPsiLinearMapRef::Dense(&geom.basis),
1988        )? + &weighted_crossprod_psi_maps(
1989            xmu_map,
1990            a.view(),
1991            CustomFamilyPsiLinearMapRef::Dense(&basis_u),
1992        )? + &xt_diag_y_dense(xmu, &a_a_u, &geom.basis)?
1993            + &xt_diag_y_dense(xmu, &a_a, &basis_u)?
1994            + &xt_diag_y_dense(xmu, &a_u, &basis_a)?
1995            + &xt_diag_y_dense(xmu, &a, &basis_a_u)?
1996            + &weighted_crossprod_psi_maps(
1997                xmu_map,
1998                c_u.view(),
1999                CustomFamilyPsiLinearMapRef::Dense(&geom.basis_d1),
2000            )?
2001            + &weighted_crossprod_psi_maps(
2002                xmu_map,
2003                c.view(),
2004                CustomFamilyPsiLinearMapRef::Dense(&basis1_u),
2005            )?
2006            + &xt_diag_y_dense(xmu, &c_a_u, &geom.basis_d1)?
2007            + &xt_diag_y_dense(xmu, &c_a, &basis1_u)?
2008            + &xt_diag_y_dense(xmu, &c_u, &basis1_a)?
2009            + &xt_diag_y_dense(xmu, &c, &basis1_a_u)?;
2010        let h_lw = weighted_crossprod_psi_maps(
2011            x_ls_map,
2012            l_u.view(),
2013            CustomFamilyPsiLinearMapRef::Dense(&geom.basis),
2014        )? + &weighted_crossprod_psi_maps(
2015            x_ls_map,
2016            l.view(),
2017            CustomFamilyPsiLinearMapRef::Dense(&basis_u),
2018        )? + &xt_diag_y_dense(x_ls, &l_a_u, &geom.basis)?
2019            + &xt_diag_y_dense(x_ls, &l_a, &basis_u)?
2020            + &xt_diag_y_dense(x_ls, &l_u, &basis_a)?
2021            + &xt_diag_y_dense(x_ls, &l, &basis_a_u)?;
2022        let hww_a_u = xt_diag_y_dense(&basis_a_u, &rows.w, &geom.basis)?;
2023        let hww_aw = xt_diag_y_dense(&basis_a, &dw_u, &geom.basis)?;
2024        let hww_au = xt_diag_y_dense(&basis_a, &rows.w, &basis_u)?;
2025        let h_ww = &hww_a_u
2026            + &hww_a_u.t()
2027            + &hww_aw
2028            + hww_aw.t()
2029            + &hww_au
2030            + hww_au.t()
2031            + &xt_diag_x_dense(&geom.basis, &dw_a_u)?;
2032
2033        Ok(gaussian_pack_wiggle_joint_symmetrichessian(
2034            &h_mm, &h_ml, &h_mw, &h_ll, &h_lw, &h_ww,
2035        ))
2036    }
2037
2038    pub(crate) fn exact_newton_joint_psi_terms_for_specs(
2039        &self,
2040        block_states: &[ParameterBlockState],
2041        specs: &[ParameterBlockSpec],
2042        derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
2043        psi_index: usize,
2044    ) -> Result<Option<gam_problem::ExactNewtonJointPsiTerms>, String> {
2045        let Some((xmu, x_ls)) = self.exact_joint_dense_block_designs(Some(specs))? else {
2046            return Ok(None);
2047        };
2048        self.exact_newton_joint_psi_terms_from_designs(
2049            block_states,
2050            derivative_blocks,
2051            psi_index,
2052            &xmu,
2053            &x_ls,
2054        )
2055    }
2056
2057    pub(crate) fn exact_newton_joint_psisecond_order_terms_for_specs(
2058        &self,
2059        block_states: &[ParameterBlockState],
2060        specs: &[ParameterBlockSpec],
2061        derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
2062        psi_i: usize,
2063        psi_j: usize,
2064    ) -> Result<Option<gam_problem::ExactNewtonJointPsiSecondOrderTerms>, String> {
2065        let Some((xmu, x_ls)) = self.exact_joint_dense_block_designs(Some(specs))? else {
2066            return Ok(None);
2067        };
2068        self.exact_newton_joint_psisecond_order_terms_from_designs(
2069            block_states,
2070            derivative_blocks,
2071            psi_i,
2072            psi_j,
2073            &xmu,
2074            &x_ls,
2075        )
2076    }
2077
2078    pub(crate) fn exact_newton_joint_psihessian_directional_derivative_for_specs(
2079        &self,
2080        block_states: &[ParameterBlockState],
2081        specs: &[ParameterBlockSpec],
2082        derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
2083        psi_index: usize,
2084        d_beta_flat: &Array1<f64>,
2085    ) -> Result<Option<Array2<f64>>, String> {
2086        let Some((xmu, x_ls)) = self.exact_joint_dense_block_designs(Some(specs))? else {
2087            return Ok(None);
2088        };
2089        self.exact_newton_joint_psihessian_directional_derivative_from_designs(
2090            block_states,
2091            derivative_blocks,
2092            psi_index,
2093            d_beta_flat,
2094            &xmu,
2095            &x_ls,
2096        )
2097    }
2098}
2099
2100impl CustomFamily for GaussianLocationScaleWiggleFamily {
2101    // Preserve the pre-gam#1395 behavior: the trait default flipped to OFF (the
2102    // flat-prior exact-Newton objective carries no Jeffreys term), so families
2103    // that historically armed the term by default opt back in explicitly.
2104    fn joint_jeffreys_term_required(&self) -> bool {
2105        true
2106    }
2107
2108    fn exact_newton_joint_hessian_beta_dependent(&self) -> bool {
2109        true
2110    }
2111
2112    /// Non-spatial location-scale seeding classification — see
2113    /// `GaussianLocationScaleFamily::outer_seed_config` for the full rationale.
2114    /// The wiggle variant has the identical non-profiled log-σ predictor, so it
2115    /// needs the same `GaussianLocationScale` classification (flexible Gaussian
2116    /// seed grid + lowest-cost keep-best + interior-extreme promotion) on the
2117    /// rho-only path; without it the log-σ block over-smooths exactly as the
2118    /// non-wiggle family does.
2119    fn outer_seed_config(&self, n_params: usize) -> crate::seeding::SeedConfig {
2120        if n_params == 0 {
2121            return crate::seeding::SeedConfig::default();
2122        }
2123        let mut config = crate::seeding::SeedConfig::default();
2124        config.risk_profile = crate::seeding::SeedRiskProfile::GaussianLocationScale;
2125        config.max_seeds = 4;
2126        config.seed_budget = 2;
2127        config
2128    }
2129
2130    fn coefficient_hessian_cost(&self, specs: &[ParameterBlockSpec]) -> u64 {
2131        // Operator-aware (see GaussianLocationScaleFamily for derivation): when
2132        // `use_joint_matrix_free_path` selects the workspace operator, joint
2133        // Hv apply is O(n · (p_t + p_ℓ + p_w)) — the row-streaming RowCoeffOperator
2134        // never materializes the dense (p_t + p_ℓ + p_w)² matrix.
2135        crate::location_scale_engine::location_scale_coefficient_hessian_cost(
2136            self.y.len() as u64,
2137            specs,
2138        )
2139    }
2140
2141    fn block_linear_constraints(
2142        &self,
2143        _: &[ParameterBlockState],
2144        block_idx: usize,
2145        spec: &ParameterBlockSpec,
2146    ) -> Result<Option<LinearInequalityConstraints>, String> {
2147        if block_idx != Self::BLOCK_WIGGLE {
2148            return Ok(None);
2149        }
2150        Ok(monotone_wiggle_nonnegative_constraints(spec.design.ncols()))
2151    }
2152
2153    fn post_update_block_beta(
2154        &self,
2155        _: &[ParameterBlockState],
2156        block_idx: usize,
2157        block_spec: &ParameterBlockSpec,
2158        beta: Array1<f64>,
2159    ) -> Result<Array1<f64>, String> {
2160        assert!(!block_spec.name.is_empty());
2161        if block_idx != Self::BLOCK_WIGGLE {
2162            return Ok(beta);
2163        }
2164        let beta = project_monotone_wiggle_beta_nonnegative(beta);
2165        validate_monotone_wiggle_beta_nonnegative(
2166            &beta,
2167            "GaussianLocationScaleWiggleFamily post-update",
2168        )?;
2169        Ok(beta)
2170    }
2171
2172    fn evaluate(&self, block_states: &[ParameterBlockState]) -> Result<FamilyEvaluation, String> {
2173        validate_block_count::<GamlssError>(
2174            "GaussianLocationScaleWiggleFamily",
2175            3,
2176            block_states.len(),
2177        )?;
2178        let n = self.y.len();
2179        let eta_mu = &block_states[Self::BLOCK_MU].eta;
2180        let eta_ls = &block_states[Self::BLOCK_LOG_SIGMA].eta;
2181        let etaw = &block_states[Self::BLOCK_WIGGLE].eta;
2182        if eta_mu.len() != n || eta_ls.len() != n || etaw.len() != n || self.weights.len() != n {
2183            return Err(GamlssError::DimensionMismatch {
2184                reason: "GaussianLocationScaleWiggleFamily input size mismatch".to_string(),
2185            }
2186            .into());
2187        }
2188        let ln2pi = (2.0 * std::f64::consts::PI).ln();
2189        // Per-row kernel emits 6 working values into pre-allocated outputs;
2190        // ll is reduced via Rayon's sum. Independent across rows. Note
2191        // wmu == ww (both equal location_working_weight) and the mean+wiggle
2192        // working responses share row.location_working_shift, applied to
2193        // eta_mu[i] and etaw[i] respectively. The previous `q = eta_mu + etaw`
2194        // intermediate is inlined to avoid an extra n-vector allocation.
2195        let mut zmu = Array1::<f64>::zeros(n);
2196        let mut wmu = Array1::<f64>::zeros(n);
2197        let mut zls = Array1::<f64>::zeros(n);
2198        let mut wls = Array1::<f64>::zeros(n);
2199        let mut zw = Array1::<f64>::zeros(n);
2200        let mut ww = Array1::<f64>::zeros(n);
2201        const CHUNK: usize = 1024;
2202        let zmu_s = zmu
2203            .as_slice_memory_order_mut()
2204            .expect("zeros is contiguous");
2205        let wmu_s = wmu
2206            .as_slice_memory_order_mut()
2207            .expect("zeros is contiguous");
2208        let zls_s = zls
2209            .as_slice_memory_order_mut()
2210            .expect("zeros is contiguous");
2211        let wls_s = wls
2212            .as_slice_memory_order_mut()
2213            .expect("zeros is contiguous");
2214        let zw_s = zw.as_slice_memory_order_mut().expect("zeros is contiguous");
2215        let ww_s = ww.as_slice_memory_order_mut().expect("zeros is contiguous");
2216        let y_view = self.y.view();
2217        let w_view = self.weights.view();
2218        let eta_mu_view = eta_mu.view();
2219        let eta_ls_view = eta_ls.view();
2220        let etaw_view = etaw.view();
2221        let ll: f64 = zmu_s
2222            .par_chunks_mut(CHUNK)
2223            .zip(wmu_s.par_chunks_mut(CHUNK))
2224            .zip(zls_s.par_chunks_mut(CHUNK))
2225            .zip(wls_s.par_chunks_mut(CHUNK))
2226            .zip(zw_s.par_chunks_mut(CHUNK))
2227            .zip(ww_s.par_chunks_mut(CHUNK))
2228            .enumerate()
2229            .map(
2230                |(chunk_idx, (((((zmu_c, wmu_c), zls_c), wls_c), zw_c), ww_c))| {
2231                    let start = chunk_idx * CHUNK;
2232                    let mut local_ll = 0.0;
2233                    for local in 0..zmu_c.len() {
2234                        let i = start + local;
2235                        let q_i = eta_mu_view[i] + etaw_view[i];
2236                        let row = gaussian_diagonal_row_kernel(
2237                            y_view[i],
2238                            q_i,
2239                            eta_ls_view[i],
2240                            w_view[i],
2241                            ln2pi,
2242                        );
2243                        let w_i = row.location_working_weight;
2244                        let shift = row.location_working_shift;
2245                        zmu_c[local] = eta_mu_view[i] + shift;
2246                        wmu_c[local] = w_i;
2247                        zw_c[local] = etaw_view[i] + shift;
2248                        ww_c[local] = w_i;
2249                        zls_c[local] = row.log_sigma_working_response;
2250                        wls_c[local] = row.log_sigma_working_weight;
2251                        local_ll += row.log_likelihood;
2252                    }
2253                    local_ll
2254                },
2255            )
2256            .sum();
2257
2258        Ok(FamilyEvaluation {
2259            log_likelihood: ll,
2260            blockworking_sets: vec![
2261                BlockWorkingSet::diagonal_checked(zmu, wmu)?,
2262                BlockWorkingSet::diagonal_checked(zls, wls)?,
2263                BlockWorkingSet::diagonal_checked(zw, ww)?,
2264            ],
2265        })
2266    }
2267
2268    fn log_likelihood_only(&self, block_states: &[ParameterBlockState]) -> Result<f64, String> {
2269        validate_block_count::<GamlssError>(
2270            "GaussianLocationScaleWiggleFamily",
2271            3,
2272            block_states.len(),
2273        )?;
2274        let eta_mu = &block_states[Self::BLOCK_MU].eta;
2275        let eta_ls = &block_states[Self::BLOCK_LOG_SIGMA].eta;
2276        let etaw = &block_states[Self::BLOCK_WIGGLE].eta;
2277        if eta_mu.len() != self.y.len()
2278            || eta_ls.len() != self.y.len()
2279            || etaw.len() != self.y.len()
2280            || self.weights.len() != self.y.len()
2281        {
2282            return Err(GamlssError::DimensionMismatch {
2283                reason: "GaussianLocationScaleWiggleFamily input size mismatch".to_string(),
2284            }
2285            .into());
2286        }
2287        let q = eta_mu + etaw;
2288        let ln2pi = (2.0 * std::f64::consts::PI).ln();
2289        let mut ll = 0.0;
2290        for i in 0..self.y.len() {
2291            let sigma_i = logb_sigma_from_eta_scalar(eta_ls[i]);
2292            let inv_s2 = (sigma_i * sigma_i).recip();
2293            let r = self.y[i] - q[i];
2294            ll += self.weights[i] * (-0.5 * (r * r * inv_s2 + ln2pi + 2.0 * sigma_i.ln()));
2295        }
2296        Ok(ll)
2297    }
2298
2299    /// Outer-only log-likelihood with optional row subsample.
2300    ///
2301    /// When `options.outer_score_subsample` is `Some`, only the sampled rows
2302    /// contribute; each row's per-row log-likelihood term is multiplied by
2303    /// `WeightedOuterRow.weight`, the Horvitz–Thompson inverse-inclusion
2304    /// factor 1/π_i (uniform or stratified sampling both supported), so the
2305    /// partial sum is an unbiased estimator of the full-data log-likelihood.
2306    /// When `None`, this returns the full-data `log_likelihood_only`. Inner
2307    /// PIRLS line searches never install the subsample option, so they
2308    /// continue to score the exact full-data log-likelihood.
2309    fn log_likelihood_only_with_options(
2310        &self,
2311        block_states: &[ParameterBlockState],
2312        options: &BlockwiseFitOptions,
2313    ) -> Result<f64, String> {
2314        let Some(subsample) = options.outer_score_subsample.as_ref() else {
2315            return self.log_likelihood_only(block_states);
2316        };
2317        validate_block_count::<GamlssError>(
2318            "GaussianLocationScaleWiggleFamily",
2319            3,
2320            block_states.len(),
2321        )?;
2322        let n = self.y.len();
2323        let eta_mu = &block_states[Self::BLOCK_MU].eta;
2324        let eta_ls = &block_states[Self::BLOCK_LOG_SIGMA].eta;
2325        let etaw = &block_states[Self::BLOCK_WIGGLE].eta;
2326        if eta_mu.len() != n || eta_ls.len() != n || etaw.len() != n || self.weights.len() != n {
2327            return Err(GamlssError::DimensionMismatch {
2328                reason: "GaussianLocationScaleWiggleFamily input size mismatch".to_string(),
2329            }
2330            .into());
2331        }
2332        let ln2pi = (2.0 * std::f64::consts::PI).ln();
2333        use rayon::iter::ParallelIterator;
2334        let ll: f64 = subsample
2335            .rows
2336            .par_iter()
2337            .map(|row| {
2338                let i = row.index;
2339                let wi = self.weights[i];
2340                if wi == 0.0 {
2341                    return 0.0;
2342                }
2343                let sigma_i = logb_sigma_from_eta_scalar(eta_ls[i]);
2344                let inv_s2 = (sigma_i * sigma_i).recip();
2345                let r = self.y[i] - eta_mu[i] - etaw[i];
2346                row.weight * wi * (-0.5 * (r * r * inv_s2 + ln2pi + 2.0 * sigma_i.ln()))
2347            })
2348            .sum();
2349        Ok(ll)
2350    }
2351
2352    fn requires_joint_outer_hyper_path(&self) -> bool {
2353        true
2354    }
2355
2356    fn exact_newton_hessian_directional_derivative(
2357        &self,
2358        block_states: &[ParameterBlockState],
2359        block_idx: usize,
2360        d_beta: &Array1<f64>,
2361    ) -> Result<Option<Array2<f64>>, String> {
2362        validate_block_count::<GamlssError>(
2363            "GaussianLocationScaleWiggleFamily",
2364            3,
2365            block_states.len(),
2366        )?;
2367        let pmu = self
2368            .mu_design
2369            .as_ref()
2370            .ok_or_else(|| {
2371                "GaussianLocationScaleWiggleFamily exact path is missing mu design".to_string()
2372            })?
2373            .ncols();
2374        let p_ls = self
2375            .log_sigma_design
2376            .as_ref()
2377            .ok_or_else(|| {
2378                "GaussianLocationScaleWiggleFamily exact path is missing log-sigma design"
2379                    .to_string()
2380            })?
2381            .ncols();
2382        let pw = block_states[Self::BLOCK_WIGGLE].beta.len();
2383        let total = pmu + p_ls + pw;
2384        let (start, end) = match block_idx {
2385            Self::BLOCK_MU => (0usize, pmu),
2386            Self::BLOCK_LOG_SIGMA => (pmu, pmu + p_ls),
2387            Self::BLOCK_WIGGLE => (pmu + p_ls, total),
2388            _ => return Ok(None),
2389        };
2390        if d_beta.len() != end - start {
2391            return Err(GamlssError::DimensionMismatch { reason: format!(
2392                "GaussianLocationScaleWiggleFamily block {block_idx} d_beta length mismatch: got {}, expected {}",
2393                d_beta.len(),
2394                end - start
2395            ) }.into());
2396        }
2397        let mut d_beta_flat = Array1::<f64>::zeros(total);
2398        d_beta_flat.slice_mut(s![start..end]).assign(d_beta);
2399        let (xmu, x_ls) = self.dense_block_designs()?;
2400        let d_joint = self
2401            .exact_newton_joint_hessian_directional_derivative_from_designs(
2402                block_states,
2403                &xmu,
2404                &x_ls,
2405                &d_beta_flat,
2406            )?
2407            .ok_or_else(|| "missing Gaussian wiggle exact joint directional Hessian".to_string())?;
2408        Ok(Some(d_joint.slice(s![start..end, start..end]).to_owned()))
2409    }
2410
2411    fn exact_newton_joint_hessian(
2412        &self,
2413        block_states: &[ParameterBlockState],
2414    ) -> Result<Option<Array2<f64>>, String> {
2415        self.exact_newton_joint_hessian_for_specs(block_states, None)
2416    }
2417
2418    fn has_explicit_joint_hessian(&self) -> bool {
2419        true
2420    }
2421
2422    fn exact_newton_joint_hessian_directional_derivative(
2423        &self,
2424        block_states: &[ParameterBlockState],
2425        d_beta_flat: &Array1<f64>,
2426    ) -> Result<Option<Array2<f64>>, String> {
2427        self.exact_newton_joint_hessian_directional_derivative_for_specs(
2428            block_states,
2429            None,
2430            d_beta_flat,
2431        )
2432    }
2433
2434    fn exact_newton_joint_hessiansecond_directional_derivative(
2435        &self,
2436        block_states: &[ParameterBlockState],
2437        d_beta_u_flat: &Array1<f64>,
2438        d_beta_v_flat: &Array1<f64>,
2439    ) -> Result<Option<Array2<f64>>, String> {
2440        self.exact_newton_joint_hessian_second_directional_derivative_for_specs(
2441            block_states,
2442            None,
2443            d_beta_u_flat,
2444            d_beta_v_flat,
2445        )
2446    }
2447
2448    fn exact_newton_joint_hessian_with_specs(
2449        &self,
2450        block_states: &[ParameterBlockState],
2451        specs: &[ParameterBlockSpec],
2452    ) -> Result<Option<Array2<f64>>, String> {
2453        self.exact_newton_joint_hessian_for_specs(block_states, Some(specs))
2454    }
2455
2456    fn exact_newton_joint_hessian_directional_derivative_with_specs(
2457        &self,
2458        block_states: &[ParameterBlockState],
2459        specs: &[ParameterBlockSpec],
2460        d_beta_flat: &Array1<f64>,
2461    ) -> Result<Option<Array2<f64>>, String> {
2462        self.exact_newton_joint_hessian_directional_derivative_for_specs(
2463            block_states,
2464            Some(specs),
2465            d_beta_flat,
2466        )
2467    }
2468
2469    fn exact_newton_joint_hessian_second_directional_derivative_with_specs(
2470        &self,
2471        block_states: &[ParameterBlockState],
2472        specs: &[ParameterBlockSpec],
2473        d_beta_u_flat: &Array1<f64>,
2474        d_beta_v_flat: &Array1<f64>,
2475    ) -> Result<Option<Array2<f64>>, String> {
2476        self.exact_newton_joint_hessian_second_directional_derivative_for_specs(
2477            block_states,
2478            Some(specs),
2479            d_beta_u_flat,
2480            d_beta_v_flat,
2481        )
2482    }
2483
2484    fn exact_newton_joint_psi_terms(
2485        &self,
2486        block_states: &[ParameterBlockState],
2487        specs: &[ParameterBlockSpec],
2488        derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
2489        psi_index: usize,
2490    ) -> Result<Option<gam_problem::ExactNewtonJointPsiTerms>, String> {
2491        self.exact_newton_joint_psi_terms_for_specs(
2492            block_states,
2493            specs,
2494            derivative_blocks,
2495            psi_index,
2496        )
2497    }
2498
2499    fn exact_newton_joint_psisecond_order_terms(
2500        &self,
2501        block_states: &[ParameterBlockState],
2502        specs: &[ParameterBlockSpec],
2503        derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
2504        psi_i: usize,
2505        psi_j: usize,
2506    ) -> Result<Option<gam_problem::ExactNewtonJointPsiSecondOrderTerms>, String> {
2507        self.exact_newton_joint_psisecond_order_terms_for_specs(
2508            block_states,
2509            specs,
2510            derivative_blocks,
2511            psi_i,
2512            psi_j,
2513        )
2514    }
2515
2516    fn exact_newton_joint_psihessian_directional_derivative(
2517        &self,
2518        block_states: &[ParameterBlockState],
2519        specs: &[ParameterBlockSpec],
2520        derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
2521        psi_index: usize,
2522        d_beta_flat: &Array1<f64>,
2523    ) -> Result<Option<Array2<f64>>, String> {
2524        self.exact_newton_joint_psihessian_directional_derivative_for_specs(
2525            block_states,
2526            specs,
2527            derivative_blocks,
2528            psi_index,
2529            d_beta_flat,
2530        )
2531    }
2532
2533    fn exact_newton_joint_psi_workspace(
2534        &self,
2535        block_states: &[ParameterBlockState],
2536        specs: &[ParameterBlockSpec],
2537        derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
2538    ) -> Result<Option<Arc<dyn ExactNewtonJointPsiWorkspace>>, String> {
2539        if !self.exact_joint_supported() {
2540            return Ok(None);
2541        }
2542        Ok(Some(Arc::new(
2543            GaussianLocationScaleWiggleExactNewtonJointPsiWorkspace::new(
2544                self.clone(),
2545                block_states.to_vec(),
2546                specs,
2547                derivative_blocks.to_vec(),
2548            )?,
2549        )))
2550    }
2551
2552    /// Outer-aware joint ψ workspace with optional row subsample.
2553    ///
2554    /// The wiggle ψ workspace shares the generic `LocationScaleJointPsiWorkspace`
2555    /// with the non-wiggle GLS family, and the subsample is plumbed through
2556    /// the trait. The wiggle's `ws_psi_*_from_parts` impls currently drop the
2557    /// subsample and fall back to the full-data exact wiggle ψ path; see
2558    /// their inline rationale and the `apply_ht_mask_*` helpers used by the
2559    /// non-wiggle GLS family. Storing the subsample here keeps the workspace
2560    /// signature uniform across both families and leaves a hook for the
2561    /// follow-up that refactors the wiggle inline arrays into a weights
2562    /// struct so HT masking can be applied in one place. Even without that
2563    /// refactor, the total outer score under subsampling remains an unbiased
2564    /// estimator of the full-data outer score: HT-unbiased LL
2565    /// (`log_likelihood_only_with_options`) + HT-unbiased ρ-Hessian
2566    /// (`exact_newton_joint_hessian_workspace_with_options`) + exact-unbiased
2567    /// ψ (the wiggle workspace path) = unbiased.
2568    fn exact_newton_joint_psi_workspace_with_options(
2569        &self,
2570        block_states: &[ParameterBlockState],
2571        specs: &[ParameterBlockSpec],
2572        derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
2573        options: &BlockwiseFitOptions,
2574    ) -> Result<Option<Arc<dyn ExactNewtonJointPsiWorkspace>>, String> {
2575        if !self.exact_joint_supported() {
2576            return Ok(None);
2577        }
2578        Ok(Some(Arc::new(
2579            GaussianLocationScaleWiggleExactNewtonJointPsiWorkspace::new_with_subsample(
2580                self.clone(),
2581                block_states.to_vec(),
2582                specs,
2583                derivative_blocks.to_vec(),
2584                options.outer_score_subsample.clone(),
2585            )?,
2586        )))
2587    }
2588
2589    fn block_geometry(
2590        &self,
2591        block_states: &[ParameterBlockState],
2592        spec: &ParameterBlockSpec,
2593    ) -> Result<(DesignMatrix, Array1<f64>), String> {
2594        if spec.name != "wiggle" {
2595            return Ok((spec.design.clone(), spec.offset.clone()));
2596        }
2597        if block_states.is_empty() {
2598            return Err(GamlssError::UnsupportedConfiguration {
2599                reason: "Gaussian wiggle geometry requires mean block".to_string(),
2600            }
2601            .into());
2602        }
2603        let eta_mu = &block_states[Self::BLOCK_MU].eta;
2604        if eta_mu.len() != self.y.len() {
2605            return Err(GamlssError::DimensionMismatch {
2606                reason: "Gaussian wiggle geometry input size mismatch".to_string(),
2607            }
2608            .into());
2609        }
2610        let x = self.wiggle_design(eta_mu.view())?;
2611        if x.ncols() != spec.design.ncols() {
2612            return Err(GamlssError::DimensionMismatch {
2613                reason: format!(
2614                    "Gaussian dynamic wiggle design col mismatch: got {}, expected {}",
2615                    x.ncols(),
2616                    spec.design.ncols()
2617                ),
2618            }
2619            .into());
2620        }
2621        let nrows = x.nrows();
2622        Ok((
2623            DesignMatrix::Dense(gam_linalg::matrix::DenseDesignMatrix::from(x)),
2624            Array1::zeros(nrows),
2625        ))
2626    }
2627
2628    fn block_geometry_is_dynamic(&self) -> bool {
2629        true
2630    }
2631
2632    fn exact_newton_joint_hessian_workspace(
2633        &self,
2634        block_states: &[ParameterBlockState],
2635        specs: &[ParameterBlockSpec],
2636    ) -> Result<Option<Arc<dyn ExactNewtonJointHessianWorkspace>>, String> {
2637        let Some((xmu, x_ls)) = self.exact_joint_dense_block_designs(Some(specs))? else {
2638            return Ok(None);
2639        };
2640        let workspace = GaussianLocationScaleWiggleHessianWorkspace::new(
2641            self.clone(),
2642            block_states.to_vec(),
2643            xmu.into_owned(),
2644            x_ls.into_owned(),
2645        )?;
2646        Ok(Some(Arc::new(workspace)))
2647    }
2648
2649    /// Outer-aware joint-Hessian workspace with optional row subsample.
2650    ///
2651    /// When `options.outer_score_subsample` is `None`, this is byte-identical
2652    /// to `exact_newton_joint_hessian_workspace`. When `Some`, the precomputed
2653    /// per-row coefficient arrays in `pieces` (`coeff_mm`, `coeff_ml`,
2654    /// `coeff_ll`, `coeff_mw_b`, `coeff_mw_d`, `coeff_lw_b`, `coeff_ww`) —
2655    /// which every downstream assembly (`hessian_dense`, `hessian_matvec`,
2656    /// `hessian_diagonal`) consumes row-linearly via `Xᵀ diag(W) Y` — are
2657    /// replaced by a Horvitz–Thompson mask: each sampled row's coefficient
2658    /// is multiplied by `WeightedOuterRow.weight` (the inverse-inclusion
2659    /// factor 1/π_i; uniform or stratified sampling both supported), and
2660    /// non-sampled rows are zeroed. The `basis`/`basis_d1` matrices are
2661    /// row-weight-independent and remain unchanged. Note that the Gaussian
2662    /// wiggle has one fewer cross-coefficient than the binomial wiggle
2663    /// (no `coeff_lw_d`) because the wiggle enters the Gaussian likelihood
2664    /// only through `q = η_μ + η_w` (no σ-chain). The resulting joint Hessian
2665    /// is an unbiased estimator of the full-data joint Hessian. Inner PIRLS
2666    /// never installs the option, so the inner solve continues to consume
2667    /// the exact full-data Hessian.
2668    fn exact_newton_joint_hessian_workspace_with_options(
2669        &self,
2670        block_states: &[ParameterBlockState],
2671        specs: &[ParameterBlockSpec],
2672        options: &BlockwiseFitOptions,
2673    ) -> Result<Option<Arc<dyn ExactNewtonJointHessianWorkspace>>, String> {
2674        let Some((xmu, x_ls)) = self.exact_joint_dense_block_designs(Some(specs))? else {
2675            return Ok(None);
2676        };
2677        let mut workspace = GaussianLocationScaleWiggleHessianWorkspace::new(
2678            self.clone(),
2679            block_states.to_vec(),
2680            xmu.into_owned(),
2681            x_ls.into_owned(),
2682        )?;
2683        if let Some(subsample) = options.outer_score_subsample.as_ref() {
2684            workspace.apply_outer_subsample(subsample.rows.as_ref());
2685        }
2686        Ok(Some(Arc::new(workspace)))
2687    }
2688
2689    /// Outer-derivative policy: declare HT-subsample capability.
2690    ///
2691    /// GaussianLocationScaleWiggleFamily overrides
2692    /// `log_likelihood_only_with_options` and
2693    /// `exact_newton_joint_hessian_workspace_with_options` to consume
2694    /// `options.outer_score_subsample` with per-row Horvitz–Thompson weights
2695    /// (each sampled row's contribution is multiplied by
2696    /// `WeightedOuterRow.weight = 1/π_i`; non-sampled rows are zeroed),
2697    /// yielding unbiased estimators of the full-data log-likelihood and
2698    /// joint Hessian. The ψ-workspace path is also subsample-aware via
2699    /// `exact_newton_joint_psi_workspace_with_options`, which threads the
2700    /// subsample down to per-row weight masking inside the joint-ψ second-
2701    /// order and directional-derivative reductions. Inner-PIRLS and final-
2702    /// covariance paths never install the option, so they continue to
2703    /// consume the exact full-data quantities.
2704    fn outer_derivative_subsample_capable(&self) -> bool {
2705        true
2706    }
2707
2708    fn inner_coefficient_hessian_hvp_available(&self, specs: &[ParameterBlockSpec]) -> bool {
2709        // Same gating as the workspace impl above: matrix-free fires when
2710        // `exact_joint_dense_block_designs` is satisfiable, which requires
2711        // both location and scale block designs to be present.  The wiggle
2712        // block is folded into the operator via the per-row pieces — its
2713        // presence is implied by reaching the wiggle family in the first
2714        // place — so the predicate matches the non-wiggle case.
2715        self.exact_joint_supported()
2716            && matches!(
2717                self.exact_joint_dense_block_designs(Some(specs)),
2718                Ok(Some(_))
2719            )
2720    }
2721}
2722
2723/// Matrix-free joint-Hessian operator for the 3-block Gaussian
2724/// location-scale wiggle family. See `GaussianLocationScaleWiggleHessianRowPieces`
2725/// for the per-row weight structure. The matvec applies
2726///
2727///   r_μ  = D_mm u_μ + D_ml u_ls + D_mw_b (B v_w) + D_mw_d (B' v_w),
2728///   r_ls = D_ml u_μ + D_ll u_ls + D_lw_b (B v_w),
2729///   r_b  = D_mw_b u_μ + D_lw_b u_ls + D_ww (B v_w),
2730///   r_d  = D_mw_d u_μ,
2731///
2732/// then forms `out_w = B^T r_b + (B')^T r_d`. The ls-wiggle cross block has
2733/// no B' contribution because the wiggle enters the Gaussian likelihood only
2734/// through `q = η_μ + η_w` (no σ-chain), so the Gaussian wiggle has one
2735/// fewer cross-coefficient than the binomial wiggle.
2736pub(crate) struct GaussianLocationScaleWiggleHessianWorkspace {
2737    pub(crate) family: GaussianLocationScaleWiggleFamily,
2738    pub(crate) block_states: Vec<ParameterBlockState>,
2739    pub(crate) xmu: Arc<Array2<f64>>,
2740    pub(crate) x_ls: Arc<Array2<f64>>,
2741    pub(crate) pieces: GaussianLocationScaleWiggleHessianRowPieces,
2742}
2743
2744impl GaussianLocationScaleWiggleHessianWorkspace {
2745    pub(crate) fn new(
2746        family: GaussianLocationScaleWiggleFamily,
2747        block_states: Vec<ParameterBlockState>,
2748        xmu: Array2<f64>,
2749        x_ls: Array2<f64>,
2750    ) -> Result<Self, String> {
2751        let pieces = family.wiggle_hessian_row_pieces(&block_states)?;
2752        Ok(Self {
2753            family,
2754            block_states,
2755            xmu: Arc::new(xmu),
2756            x_ls: Arc::new(x_ls),
2757            pieces,
2758        })
2759    }
2760
2761    /// Apply a Horvitz–Thompson outer-row subsample mask to the precomputed
2762    /// per-row coefficient arrays in place.
2763    ///
2764    /// Each sampled row's `coeff_*[i]` is multiplied by its
2765    /// `WeightedOuterRow.weight` (the HT inverse-inclusion factor 1/π_i —
2766    /// uniform or stratified sampling both supported). All non-sampled rows
2767    /// are zeroed. Because every downstream assembly (`hessian_dense`,
2768    /// `hessian_matvec`, `hessian_diagonal`) is row-linear in these arrays
2769    /// via `Xᵀ diag(W) Y`, the resulting joint-Hessian is an unbiased
2770    /// estimator of the full-data joint Hessian. The `basis`/`basis_d1`
2771    /// matrices are independent of the per-row weights and remain unchanged.
2772    /// The Gaussian wiggle has 7 coefficient arrays (no `coeff_lw_d`, unlike
2773    /// the binomial wiggle's 8) because the wiggle enters the Gaussian
2774    /// likelihood only through `q = η_μ + η_w` (no σ-chain).
2775    pub(crate) fn apply_outer_subsample(
2776        &mut self,
2777        rows: &[crate::outer_subsample::WeightedOuterRow],
2778    ) {
2779        let n = self.pieces.coeff_mm.len();
2780        let mut mask_mm = Array1::<f64>::zeros(n);
2781        let mut mask_ml = Array1::<f64>::zeros(n);
2782        let mut mask_ll = Array1::<f64>::zeros(n);
2783        let mut mask_mw_b = Array1::<f64>::zeros(n);
2784        let mut mask_mw_d = Array1::<f64>::zeros(n);
2785        let mut mask_lw_b = Array1::<f64>::zeros(n);
2786        let mut maskww = Array1::<f64>::zeros(n);
2787        for r in rows {
2788            let i = r.index;
2789            let w = r.weight;
2790            mask_mm[i] = self.pieces.coeff_mm[i] * w;
2791            mask_ml[i] = self.pieces.coeff_ml[i] * w;
2792            mask_ll[i] = self.pieces.coeff_ll[i] * w;
2793            mask_mw_b[i] = self.pieces.coeff_mw_b[i] * w;
2794            mask_mw_d[i] = self.pieces.coeff_mw_d[i] * w;
2795            mask_lw_b[i] = self.pieces.coeff_lw_b[i] * w;
2796            maskww[i] = self.pieces.coeff_ww[i] * w;
2797        }
2798        self.pieces.coeff_mm = mask_mm;
2799        self.pieces.coeff_ml = mask_ml;
2800        self.pieces.coeff_ll = mask_ll;
2801        self.pieces.coeff_mw_b = mask_mw_b;
2802        self.pieces.coeff_mw_d = mask_mw_d;
2803        self.pieces.coeff_lw_b = mask_lw_b;
2804        self.pieces.coeff_ww = maskww;
2805    }
2806}
2807
2808impl ExactNewtonJointHessianWorkspace for GaussianLocationScaleWiggleHessianWorkspace {
2809    fn warm_up_outer_caches_for_mode(
2810        &self,
2811        eval_mode: gam_problem::EvalMode,
2812    ) -> Result<(), String> {
2813        match eval_mode {
2814            gam_problem::EvalMode::ValueOnly
2815            | gam_problem::EvalMode::ValueAndGradient
2816            | gam_problem::EvalMode::ValueGradientHessian => Ok(()),
2817        }
2818    }
2819
2820    fn hessian_dense(&self) -> Result<Option<Array2<f64>>, String> {
2821        // Same Hv structure as `hessian_matvec`, but routed through the
2822        // already-existing `assemble_dense` row-pieces helper (six GEMMs:
2823        // h_mm, h_ml, h_mw_b, h_mw_d, h_lw, h_ww). Avoids `total` canonical-
2824        // basis HVPs in `MatrixFreeSpdOperator::materialize_dense_operator`,
2825        // which at large scale (n≈320k, p_total≈82) costs ~568s per κ-iter
2826        // versus ~1s for the dense build.
2827        let dense = self
2828            .pieces
2829            .assemble_dense(self.xmu.as_ref(), self.x_ls.as_ref())?;
2830        Ok(Some(dense))
2831    }
2832
2833    fn hessian_matvec_available(&self) -> bool {
2834        true
2835    }
2836
2837    fn hessian_matvec(&self, v: &Array1<f64>) -> Result<Option<Array1<f64>>, String> {
2838        let pmu = self.xmu.ncols();
2839        let p_ls = self.x_ls.ncols();
2840        let pw = self.pieces.basis.ncols();
2841        let total = pmu + p_ls + pw;
2842        if v.len() != total {
2843            return Err(GamlssError::DimensionMismatch {
2844                reason: format!(
2845                    "GaussianLocationScaleWiggle matvec dimension mismatch: got {}, expected {}",
2846                    v.len(),
2847                    total
2848                ),
2849            }
2850            .into());
2851        }
2852        let v_mu = v.slice(s![0..pmu]);
2853        let v_ls = v.slice(s![pmu..pmu + p_ls]);
2854        let v_w = v.slice(s![pmu + p_ls..total]);
2855
2856        let u_mu = fast_av(self.xmu.as_ref(), &v_mu);
2857        let u_ls = fast_av(self.x_ls.as_ref(), &v_ls);
2858        let u_b = fast_av(&self.pieces.basis, &v_w);
2859        let u_d = fast_av(&self.pieces.basis_d1, &v_w);
2860
2861        let r_mu = &self.pieces.coeff_mm * &u_mu
2862            + &self.pieces.coeff_ml * &u_ls
2863            + &self.pieces.coeff_mw_b * &u_b
2864            + &self.pieces.coeff_mw_d * &u_d;
2865        let r_ls = &self.pieces.coeff_ml * &u_mu
2866            + &self.pieces.coeff_ll * &u_ls
2867            + &self.pieces.coeff_lw_b * &u_b;
2868        let r_b = &self.pieces.coeff_mw_b * &u_mu
2869            + &self.pieces.coeff_lw_b * &u_ls
2870            + &self.pieces.coeff_ww * &u_b;
2871        let r_d = &self.pieces.coeff_mw_d * &u_mu;
2872
2873        let out_mu = fast_atv(self.xmu.as_ref(), &r_mu);
2874        let out_ls = fast_atv(self.x_ls.as_ref(), &r_ls);
2875        let out_w = fast_atv(&self.pieces.basis, &r_b) + &fast_atv(&self.pieces.basis_d1, &r_d);
2876
2877        let mut out = Array1::<f64>::zeros(total);
2878        out.slice_mut(s![0..pmu]).assign(&out_mu);
2879        out.slice_mut(s![pmu..pmu + p_ls]).assign(&out_ls);
2880        out.slice_mut(s![pmu + p_ls..total]).assign(&out_w);
2881        Ok(Some(out))
2882    }
2883
2884    fn hessian_diagonal(&self) -> Result<Option<Array1<f64>>, String> {
2885        let pmu = self.xmu.ncols();
2886        let p_ls = self.x_ls.ncols();
2887        let pw = self.pieces.basis.ncols();
2888        let total = pmu + p_ls + pw;
2889        // Diagonals are independent column-wise reductions: parallelize.
2890        use rayon::iter::{IntoParallelIterator, ParallelIterator};
2891        let diag_mu: Vec<f64> = (0..pmu)
2892            .into_par_iter()
2893            .map(|j| {
2894                let col = self.xmu.column(j);
2895                col.iter()
2896                    .zip(self.pieces.coeff_mm.iter())
2897                    .map(|(&v, &c)| c * v * v)
2898                    .sum()
2899            })
2900            .collect();
2901        let diag_ls: Vec<f64> = (0..p_ls)
2902            .into_par_iter()
2903            .map(|j| {
2904                let col = self.x_ls.column(j);
2905                col.iter()
2906                    .zip(self.pieces.coeff_ll.iter())
2907                    .map(|(&v, &c)| c * v * v)
2908                    .sum()
2909            })
2910            .collect();
2911        let diag_w: Vec<f64> = (0..pw)
2912            .into_par_iter()
2913            .map(|j| {
2914                let col = self.pieces.basis.column(j);
2915                col.iter()
2916                    .zip(self.pieces.coeff_ww.iter())
2917                    .map(|(&v, &c)| c * v * v)
2918                    .sum()
2919            })
2920            .collect();
2921        let mut diag = Array1::<f64>::zeros(total);
2922        for (j, v) in diag_mu.into_iter().enumerate() {
2923            diag[j] = v;
2924        }
2925        for (j, v) in diag_ls.into_iter().enumerate() {
2926            diag[pmu + j] = v;
2927        }
2928        for (j, v) in diag_w.into_iter().enumerate() {
2929            diag[pmu + p_ls + j] = v;
2930        }
2931        Ok(Some(diag))
2932    }
2933
2934    fn directional_derivative(
2935        &self,
2936        d_beta_flat: &Array1<f64>,
2937    ) -> Result<Option<Array2<f64>>, String> {
2938        self.family
2939            .exact_newton_joint_hessian_directional_derivative_from_designs(
2940                &self.block_states,
2941                self.xmu.as_ref(),
2942                self.x_ls.as_ref(),
2943                d_beta_flat,
2944            )
2945    }
2946
2947    fn directional_derivative_operator(
2948        &self,
2949        d_beta_flat: &Array1<f64>,
2950    ) -> Result<Option<Arc<dyn gam_problem::HyperOperator>>, String> {
2951        self.family.gls_wiggle_directional_operator(
2952            &self.block_states,
2953            self.xmu.clone(),
2954            self.x_ls.clone(),
2955            d_beta_flat,
2956        )
2957    }
2958
2959    fn second_directional_derivative(
2960        &self,
2961        d_beta_u_flat: &Array1<f64>,
2962        d_beta_v_flat: &Array1<f64>,
2963    ) -> Result<Option<Array2<f64>>, String> {
2964        self.family
2965            .exact_newton_joint_hessiansecond_directional_derivative_from_designs(
2966                &self.block_states,
2967                self.xmu.as_ref(),
2968                self.x_ls.as_ref(),
2969                d_beta_u_flat,
2970                d_beta_v_flat,
2971            )
2972    }
2973
2974    fn second_directional_derivative_operator(
2975        &self,
2976        d_beta_u: &Array1<f64>,
2977        d_beta_v: &Array1<f64>,
2978    ) -> Result<Option<Arc<dyn gam_problem::HyperOperator>>, String> {
2979        self.family.gls_wiggle_second_directional_operator(
2980            &self.block_states,
2981            self.xmu.clone(),
2982            self.x_ls.clone(),
2983            d_beta_u,
2984            d_beta_v,
2985        )
2986    }
2987}
2988
2989impl CustomFamilyGenerative for GaussianLocationScaleWiggleFamily {
2990    fn generativespec(
2991        &self,
2992        block_states: &[ParameterBlockState],
2993    ) -> Result<GenerativeSpec, String> {
2994        validate_block_count::<GamlssError>(
2995            "GaussianLocationScaleWiggleFamily",
2996            3,
2997            block_states.len(),
2998        )?;
2999        let eta_mu = &block_states[Self::BLOCK_MU].eta;
3000        let eta_wiggle = &block_states[Self::BLOCK_WIGGLE].eta;
3001        let eta_log_sigma = &block_states[Self::BLOCK_LOG_SIGMA].eta;
3002        let n = eta_mu.len();
3003        let mean = gamlss_rowwise_map(n, |i| eta_mu[i] + eta_wiggle[i]);
3004        let sigma = gamlss_rowwise_map(n, |i| logb_sigma_from_eta_scalar(eta_log_sigma[i]));
3005        Ok(GenerativeSpec {
3006            mean,
3007            noise: NoiseModel::Gaussian { sigma },
3008        })
3009    }
3010}
3011
3012pub(crate) fn expect_single_block<'a>(
3013    block_states: &'a [ParameterBlockState],
3014    family_name: &str,
3015) -> Result<&'a ParameterBlockState, String> {
3016    validate_block_count::<GamlssError>(family_name, 1, block_states.len())?;
3017    Ok(&block_states[0])
3018}