Skip to main content

gam_models/gamlss/gaussian/
wiggle.rs

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