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