Skip to main content

gam_models/gamlss/gaussian/
binomial_mean_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
7#[derive(Clone)]
8pub struct BinomialMeanWiggleFamily {
9    pub y: Array1<f64>,
10    pub weights: Array1<f64>,
11    pub link_kind: InverseLink,
12    pub wiggle_knots: Array1<f64>,
13    pub wiggle_degree: usize,
14    /// Resource policy threaded into PsiDesignMap construction during
15    /// exact-Newton joint psi evaluation. Defaults to
16    /// `ResourcePolicy::default_library()` when the family is built without
17    /// an explicit policy.
18    pub policy: gam_runtime::resource::ResourcePolicy,
19}
20
21pub(crate) struct BinomialMeanWiggleGeometry {
22    pub(crate) basis: Array2<f64>,
23    pub(crate) basis_d1: Array2<f64>,
24    pub(crate) basis_d2: Array2<f64>,
25    pub(crate) basis_d3: Array2<f64>,
26    pub(crate) dq_dq0: Array1<f64>,
27    pub(crate) d2q_dq02: Array1<f64>,
28    pub(crate) d3q_dq03: Array1<f64>,
29    pub(crate) d4q_dq04: Array1<f64>,
30}
31
32pub(crate) struct BinomialMeanWiggleJointPsiDirection {
33    pub(crate) x_eta_psi: Option<Array2<f64>>,
34    pub(crate) z_eta_psi: Array1<f64>,
35}
36
37impl BinomialMeanWiggleFamily {
38    pub const BLOCK_ETA: usize = 0;
39    pub const BLOCK_WIGGLE: usize = 1;
40
41    pub(crate) fn wiggle_basiswith_options(
42        &self,
43        q0: ArrayView1<'_, f64>,
44        options: BasisOptions,
45    ) -> Result<Array2<f64>, String> {
46        monotone_wiggle_basis_with_derivative_order(
47            q0,
48            &self.wiggle_knots,
49            self.wiggle_degree,
50            options.derivative_order,
51        )
52    }
53
54    pub(crate) fn wiggle_design(&self, q0: ArrayView1<'_, f64>) -> Result<Array2<f64>, String> {
55        self.wiggle_basiswith_options(q0, BasisOptions::value())
56    }
57
58    pub(crate) fn wiggle_dq_dq0(
59        &self,
60        q0: ArrayView1<'_, f64>,
61        beta_link_wiggle: ArrayView1<'_, f64>,
62    ) -> Result<Array1<f64>, String> {
63        let d_constrained = self.wiggle_basiswith_options(q0, BasisOptions::first_derivative())?;
64        if d_constrained.ncols() != beta_link_wiggle.len() {
65            return Err(GamlssError::DimensionMismatch { reason: format!(
66                "wiggle derivative/beta mismatch: basis has {} columns but beta_link_wiggle has {} coefficients",
67                d_constrained.ncols(),
68                beta_link_wiggle.len()
69            ) }.into());
70        }
71        Ok(d_constrained.dot(&beta_link_wiggle) + 1.0)
72    }
73
74    pub(crate) fn wiggle_d2q_dq02(
75        &self,
76        q0: ArrayView1<'_, f64>,
77        beta_link_wiggle: ArrayView1<'_, f64>,
78    ) -> Result<Array1<f64>, String> {
79        let d2 = self.wiggle_basiswith_options(q0, BasisOptions::second_derivative())?;
80        if d2.ncols() != beta_link_wiggle.len() {
81            return Err(GamlssError::DimensionMismatch { reason: format!(
82                "wiggle second-derivative/beta mismatch: basis has {} columns but beta_link_wiggle has {} coefficients",
83                d2.ncols(),
84                beta_link_wiggle.len()
85            ) }.into());
86        }
87        Ok(d2.dot(&beta_link_wiggle))
88    }
89
90    pub(crate) fn wiggle_d3basis_constrained(
91        &self,
92        q0: ArrayView1<'_, f64>,
93    ) -> Result<Array2<f64>, String> {
94        monotone_wiggle_basis_with_derivative_order(q0, &self.wiggle_knots, self.wiggle_degree, 3)
95    }
96
97    pub(crate) fn wiggle_d3q_dq03(
98        &self,
99        q0: ArrayView1<'_, f64>,
100        beta_link_wiggle: ArrayView1<'_, f64>,
101    ) -> Result<Array1<f64>, String> {
102        let d3 = self.wiggle_d3basis_constrained(q0)?;
103        if d3.ncols() != beta_link_wiggle.len() {
104            return Err(GamlssError::DimensionMismatch { reason: format!(
105                "wiggle third-derivative/beta mismatch: basis has {} columns but beta_link_wiggle has {} coefficients",
106                d3.ncols(),
107                beta_link_wiggle.len()
108            ) }.into());
109        }
110        Ok(d3.dot(&beta_link_wiggle))
111    }
112
113    pub(crate) fn wiggle_d4q_dq04(
114        &self,
115        q0: ArrayView1<'_, f64>,
116        beta_link_wiggle: ArrayView1<'_, f64>,
117    ) -> Result<Array1<f64>, String> {
118        let d4 = monotone_wiggle_basis_with_derivative_order(
119            q0,
120            &self.wiggle_knots,
121            self.wiggle_degree,
122            4,
123        )?;
124        if d4.ncols() != beta_link_wiggle.len() {
125            return Err(GamlssError::DimensionMismatch { reason: format!(
126                "wiggle fourth-derivative/beta mismatch: basis has {} columns but beta_link_wiggle has {} coefficients",
127                d4.ncols(),
128                beta_link_wiggle.len()
129            ) }.into());
130        }
131        Ok(d4.dot(&beta_link_wiggle))
132    }
133
134    pub(crate) fn wiggle_geometry(
135        &self,
136        q0: ArrayView1<'_, f64>,
137        beta_link_wiggle: ArrayView1<'_, f64>,
138    ) -> Result<BinomialMeanWiggleGeometry, String> {
139        let basis = self.wiggle_design(q0)?;
140        let basis_d1 = self.wiggle_basiswith_options(q0, BasisOptions::first_derivative())?;
141        let basis_d2 = self.wiggle_basiswith_options(q0, BasisOptions::second_derivative())?;
142        let basis_d3 = self.wiggle_d3basis_constrained(q0)?;
143        let dq_dq0 = self.wiggle_dq_dq0(q0, beta_link_wiggle)?;
144        let d2q_dq02 = self.wiggle_d2q_dq02(q0, beta_link_wiggle)?;
145        let d3q_dq03 = self.wiggle_d3q_dq03(q0, beta_link_wiggle)?;
146        let d4q_dq04 = self.wiggle_d4q_dq04(q0, beta_link_wiggle)?;
147        Ok(BinomialMeanWiggleGeometry {
148            basis,
149            basis_d1,
150            basis_d2,
151            basis_d3,
152            dq_dq0,
153            d2q_dq02,
154            d3q_dq03,
155            d4q_dq04,
156        })
157    }
158
159    pub(crate) fn neglog_q_derivatives(
160        &self,
161        y: f64,
162        weight: f64,
163        q: f64,
164    ) -> Result<(f64, f64, f64), String> {
165        let jet = inverse_link_jet_for_inverse_link(&self.link_kind, q)
166            .map_err(|e| format!("fixed-link wiggle inverse-link evaluation failed: {e}"))?;
167        // Pass μ RAW: the dispatch returns the exact q-derivatives of the
168        // evaluated loss for every representable μ in (0,1) and handles the
169        // saturated boundary itself. See binomial_location_scalerow (#948).
170        Ok(binomial_neglog_q_derivatives_dispatch(
171            y,
172            weight,
173            q,
174            jet.mu,
175            jet.d1,
176            jet.d2,
177            jet.d3,
178            &self.link_kind,
179        ))
180    }
181
182    pub(crate) fn neglog_q_fourth_derivative(
183        &self,
184        y: f64,
185        weight: f64,
186        q: f64,
187    ) -> Result<f64, String> {
188        let jet = inverse_link_jet_for_inverse_link(&self.link_kind, q)
189            .map_err(|e| format!("fixed-link wiggle inverse-link evaluation failed: {e}"))?;
190        // Pass μ RAW — see neglog_q_derivatives above (#948).
191        binomial_neglog_q_fourth_derivative_dispatch(
192            y,
193            weight,
194            q,
195            jet.mu,
196            jet.d1,
197            jet.d2,
198            jet.d3,
199            &self.link_kind,
200        )
201    }
202
203    pub(crate) fn dense_eta_design_fromspecs<'a>(
204        &self,
205        specs: &'a [ParameterBlockSpec],
206    ) -> Result<Cow<'a, Array2<f64>>, String> {
207        if specs.len() != 2 {
208            return Err(GamlssError::DimensionMismatch {
209                reason: format!(
210                    "BinomialMeanWiggleFamily expects 2 specs, got {}",
211                    specs.len()
212                ),
213            }
214            .into());
215        }
216        Ok(match specs[Self::BLOCK_ETA].design.as_dense_ref() {
217            Some(d) => Cow::Borrowed(d),
218            None => Cow::Owned(
219                specs[Self::BLOCK_ETA]
220                    .design
221                    .try_to_dense_with_policy(
222                        &self.policy.material_policy(),
223                        "BinomialMeanWiggle dense_eta_design_fromspecs eta",
224                    )
225                    .map_err(|e| e.to_string())?
226                    .as_ref()
227                    .clone(),
228            ),
229        })
230    }
231
232    pub(crate) fn exact_newton_joint_psi_direction(
233        &self,
234        block_states: &[ParameterBlockState],
235        derivative_blocks: &[Vec<CustomFamilyBlockPsiDerivative>],
236        psi_index: usize,
237        x_eta: &Array2<f64>,
238    ) -> Result<Option<BinomialMeanWiggleJointPsiDirection>, String> {
239        validate_block_count::<GamlssError>("BinomialMeanWiggleFamily", 2, block_states.len())?;
240        if derivative_blocks.len() != 2 {
241            return Err(GamlssError::DimensionMismatch { reason: format!(
242                "BinomialMeanWiggleFamily joint psi direction expects 2 derivative block lists, got {}",
243                derivative_blocks.len()
244            ) }.into());
245        }
246        let n = self.y.len();
247        let p_eta = x_eta.ncols();
248        let beta_eta = &block_states[Self::BLOCK_ETA].beta;
249        let mut global = 0usize;
250        for (block_idx, block_derivs) in derivative_blocks.iter().enumerate() {
251            for deriv in block_derivs {
252                if global == psi_index {
253                    if block_idx != Self::BLOCK_ETA {
254                        return Ok(None);
255                    }
256                    let x_eta_psi_map = resolve_custom_family_x_psi_map(
257                        deriv,
258                        n,
259                        p_eta,
260                        0..n,
261                        "BinomialMeanWiggleFamily eta",
262                        &self.policy,
263                    )?;
264                    let x_eta_psi = x_eta_psi_map.row_chunk(0..n)?;
265                    let z_eta_psi = x_eta_psi.dot(beta_eta);
266                    return Ok(Some(BinomialMeanWiggleJointPsiDirection {
267                        x_eta_psi: Some(x_eta_psi),
268                        z_eta_psi,
269                    }));
270                }
271                global += 1;
272            }
273        }
274        Ok(None)
275    }
276
277    pub(crate) fn exact_newton_joint_psi_action(
278        &self,
279        block_states: &[ParameterBlockState],
280        derivative_blocks: &[Vec<CustomFamilyBlockPsiDerivative>],
281        psi_index: usize,
282        p_eta: usize,
283    ) -> Result<Option<(CustomFamilyPsiDesignAction, Array1<f64>)>, String> {
284        validate_block_count::<GamlssError>("BinomialMeanWiggleFamily", 2, block_states.len())?;
285        if derivative_blocks.len() != 2 {
286            return Err(GamlssError::DimensionMismatch { reason: format!(
287                "BinomialMeanWiggleFamily joint psi action expects 2 derivative block lists, got {}",
288                derivative_blocks.len()
289            ) }.into());
290        }
291        let n = self.y.len();
292        let beta_eta = &block_states[Self::BLOCK_ETA].beta;
293        let mut global = 0usize;
294        for (block_idx, block_derivs) in derivative_blocks.iter().enumerate() {
295            for deriv in block_derivs {
296                if global == psi_index {
297                    if block_idx != Self::BLOCK_ETA {
298                        return Ok(None);
299                    }
300                    let action = match CustomFamilyPsiDesignAction::from_first_derivative(
301                        deriv,
302                        n,
303                        p_eta,
304                        0..n,
305                        "BinomialMeanWiggleFamily eta",
306                    ) {
307                        Ok(action) => action,
308                        Err(_) => return Ok(None),
309                    };
310                    let z_eta_psi = action.forward_mul(beta_eta.view());
311                    return Ok(Some((action, z_eta_psi)));
312                }
313                global += 1;
314            }
315        }
316        Ok(None)
317    }
318
319    pub(crate) fn bmw_static_hessian_operator(
320        &self,
321        block_states: &[ParameterBlockState],
322        x_eta_arc: Arc<Array2<f64>>,
323    ) -> Result<Arc<RowCoeffOperator>, String> {
324        validate_block_count::<GamlssError>("BinomialMeanWiggleFamily", 2, block_states.len())?;
325        let eta = &block_states[Self::BLOCK_ETA].eta;
326        let etaw = &block_states[Self::BLOCK_WIGGLE].eta;
327        let betaw = &block_states[Self::BLOCK_WIGGLE].beta;
328        let n = self.y.len();
329        if eta.len() != n || etaw.len() != n || self.weights.len() != n {
330            return Err(GamlssError::DimensionMismatch {
331                reason: "BinomialMeanWiggleFamily input size mismatch".to_string(),
332            }
333            .into());
334        }
335        let geom = self.wiggle_geometry(eta.view(), betaw.view())?;
336        let p_eta = x_eta_arc.ncols();
337        let pw = geom.basis.ncols();
338        let mut coeff_eta = Array1::<f64>::zeros(n);
339        let mut coeff_etaw_b = Array1::<f64>::zeros(n);
340        let mut coeff_etaw_d1 = Array1::<f64>::zeros(n);
341        let mut coeff_ww = Array1::<f64>::zeros(n);
342        for row in 0..n {
343            let q = eta[row] + etaw[row];
344            let (m1, m2, _) = self.neglog_q_derivatives(self.y[row], self.weights[row], q)?;
345            let a = geom.dq_dq0[row];
346            let b = geom.d2q_dq02[row];
347            coeff_eta[row] = hessian_coeff_fromobjective_q_terms(m1, m2, a, a, b);
348            coeff_etaw_b[row] = m2 * a;
349            coeff_etaw_d1[row] = m1;
350            coeff_ww[row] = m2;
351        }
352        Ok(Arc::new(RowCoeffOperator::from_directions(
353            vec![p_eta, pw],
354            vec![
355                (0, x_eta_arc),
356                (1, Arc::new(geom.basis)),
357                (1, Arc::new(geom.basis_d1)),
358            ],
359            vec![
360                (0, 0, coeff_eta),
361                (0, 1, coeff_etaw_b),
362                (0, 2, coeff_etaw_d1),
363                (1, 1, coeff_ww),
364            ],
365            n,
366        )))
367    }
368
369    pub(crate) fn bmw_directional_operator(
370        &self,
371        block_states: &[ParameterBlockState],
372        x_eta_arc: Arc<Array2<f64>>,
373        d_beta_flat: &Array1<f64>,
374    ) -> Result<Option<Arc<dyn gam_problem::HyperOperator>>, String> {
375        validate_block_count::<GamlssError>("BinomialMeanWiggleFamily", 2, block_states.len())?;
376        let eta = &block_states[Self::BLOCK_ETA].eta;
377        let etaw = &block_states[Self::BLOCK_WIGGLE].eta;
378        let betaw = &block_states[Self::BLOCK_WIGGLE].beta;
379        let n = self.y.len();
380        if eta.len() != n || etaw.len() != n || self.weights.len() != n {
381            return Err(GamlssError::DimensionMismatch {
382                reason: "BinomialMeanWiggleFamily input size mismatch".to_string(),
383            }
384            .into());
385        }
386        let geom = self.wiggle_geometry(eta.view(), betaw.view())?;
387        let p_eta = x_eta_arc.ncols();
388        let pw = geom.basis.ncols();
389        let total = p_eta + pw;
390        if d_beta_flat.len() != total {
391            return Err(GamlssError::DimensionMismatch {
392                reason: format!(
393                    "BinomialMeanWiggleFamily joint d_beta length mismatch: got {}, expected {}",
394                    d_beta_flat.len(),
395                    total
396                ),
397            }
398            .into());
399        }
400        let u_eta = d_beta_flat.slice(s![0..p_eta]).to_owned();
401        let uw = d_beta_flat.slice(s![p_eta..total]).to_owned();
402        let xi = fast_av(x_eta_arc.as_ref(), &u_eta);
403        let phi = fast_av(&geom.basis, &uw);
404        let basis1_u = fast_av(&geom.basis_d1, &uw);
405        let basis2_u = fast_av(&geom.basis_d2, &uw);
406
407        let mut coeff_eta = Array1::<f64>::zeros(n);
408        let mut coeff_etaw_b = Array1::<f64>::zeros(n);
409        let mut coeff_etaw_d1 = Array1::<f64>::zeros(n);
410        let mut coeff_etaw_d2 = Array1::<f64>::zeros(n);
411        let mut coeff_ww_bb = Array1::<f64>::zeros(n);
412        let mut coeff_ww_db = Array1::<f64>::zeros(n);
413        for row in 0..n {
414            let q = eta[row] + etaw[row];
415            let (m1, m2, m3) = self.neglog_q_derivatives(self.y[row], self.weights[row], q)?;
416            let a = geom.dq_dq0[row];
417            let b = geom.d2q_dq02[row];
418            let c = geom.d3q_dq03[row];
419            let q_u = a * xi[row] + phi[row];
420            let a_u = b * xi[row] + basis1_u[row];
421            let b_u = c * xi[row] + basis2_u[row];
422            coeff_eta[row] = directionalhessian_coeff_fromobjective_q_terms(
423                m1, m2, m3, q_u, a, a, b, a_u, a_u, b_u,
424            );
425            coeff_etaw_b[row] = m3 * q_u * a + m2 * a_u;
426            coeff_etaw_d1[row] = m2 * (a * xi[row] + q_u);
427            coeff_etaw_d2[row] = m1 * xi[row];
428            coeff_ww_bb[row] = m3 * q_u;
429            coeff_ww_db[row] = m2 * xi[row];
430        }
431        Ok(Some(Arc::new(RowCoeffOperator::from_directions(
432            vec![p_eta, pw],
433            vec![
434                (0, x_eta_arc),
435                (1, Arc::new(geom.basis)),
436                (1, Arc::new(geom.basis_d1)),
437                (1, Arc::new(geom.basis_d2)),
438            ],
439            vec![
440                (0, 0, coeff_eta),
441                (0, 1, coeff_etaw_b),
442                (0, 2, coeff_etaw_d1),
443                (0, 3, coeff_etaw_d2),
444                (1, 1, coeff_ww_bb),
445                (1, 2, coeff_ww_db),
446            ],
447            n,
448        ))))
449    }
450
451    pub(crate) fn bmw_second_directional_operator(
452        &self,
453        block_states: &[ParameterBlockState],
454        x_eta_arc: Arc<Array2<f64>>,
455        d_beta_u_flat: &Array1<f64>,
456        d_beta_v_flat: &Array1<f64>,
457    ) -> Result<Option<Arc<dyn gam_problem::HyperOperator>>, String> {
458        validate_block_count::<GamlssError>("BinomialMeanWiggleFamily", 2, block_states.len())?;
459        let eta = &block_states[Self::BLOCK_ETA].eta;
460        let etaw = &block_states[Self::BLOCK_WIGGLE].eta;
461        let betaw = &block_states[Self::BLOCK_WIGGLE].beta;
462        let n = self.y.len();
463        if eta.len() != n || etaw.len() != n || self.weights.len() != n {
464            return Err(GamlssError::DimensionMismatch {
465                reason: "BinomialMeanWiggleFamily input size mismatch".to_string(),
466            }
467            .into());
468        }
469        let geom = self.wiggle_geometry(eta.view(), betaw.view())?;
470        let p_eta = x_eta_arc.ncols();
471        let pw = geom.basis.ncols();
472        let total = p_eta + pw;
473        if d_beta_u_flat.len() != total || d_beta_v_flat.len() != total {
474            return Err(GamlssError::DimensionMismatch { reason: format!(
475                "BinomialMeanWiggleFamily joint second d_beta length mismatch: got {} and {}, expected {}",
476                d_beta_u_flat.len(),
477                d_beta_v_flat.len(),
478                total
479            ) }.into());
480        }
481        let u_eta = d_beta_u_flat.slice(s![0..p_eta]).to_owned();
482        let v_eta = d_beta_v_flat.slice(s![0..p_eta]).to_owned();
483        let uw = d_beta_u_flat.slice(s![p_eta..total]).to_owned();
484        let vw = d_beta_v_flat.slice(s![p_eta..total]).to_owned();
485
486        let xi_u = fast_av(x_eta_arc.as_ref(), &u_eta);
487        let xi_v = fast_av(x_eta_arc.as_ref(), &v_eta);
488        let phi_u = fast_av(&geom.basis, &uw);
489        let phi_v = fast_av(&geom.basis, &vw);
490        let b1u = fast_av(&geom.basis_d1, &uw);
491        let b1v = fast_av(&geom.basis_d1, &vw);
492        let b2u = fast_av(&geom.basis_d2, &uw);
493        let b2v = fast_av(&geom.basis_d2, &vw);
494        let b3u = fast_av(&geom.basis_d3, &uw);
495        let b3v = fast_av(&geom.basis_d3, &vw);
496
497        let mut coeff_eta = Array1::<f64>::zeros(n);
498        let mut coeff_etaw_b = Array1::<f64>::zeros(n);
499        let mut coeff_etaw_d1 = Array1::<f64>::zeros(n);
500        let mut coeff_etaw_d2 = Array1::<f64>::zeros(n);
501        let mut coeff_etaw_d3 = Array1::<f64>::zeros(n);
502        let mut coeff_ww_bb = Array1::<f64>::zeros(n);
503        let mut coeff_ww_db = Array1::<f64>::zeros(n);
504        let mut coeff_ww_ddb = Array1::<f64>::zeros(n);
505        let mut coeff_ww_dd = Array1::<f64>::zeros(n);
506
507        for row in 0..n {
508            let q = eta[row] + etaw[row];
509            let (m1, m2, m3) = self.neglog_q_derivatives(self.y[row], self.weights[row], q)?;
510            let m4 = self.neglog_q_fourth_derivative(self.y[row], self.weights[row], q)?;
511            let a = geom.dq_dq0[row];
512            let b = geom.d2q_dq02[row];
513            let c = geom.d3q_dq03[row];
514            let d = geom.d4q_dq04[row];
515
516            let q_u = a * xi_u[row] + phi_u[row];
517            let a_u = b * xi_u[row] + b1u[row];
518            let b_u = c * xi_u[row] + b2u[row];
519            let q_v = a * xi_v[row] + phi_v[row];
520            let a_v = b * xi_v[row] + b1v[row];
521            let b_v = c * xi_v[row] + b2v[row];
522            let q_uv = b * xi_u[row] * xi_v[row] + b1u[row] * xi_v[row] + b1v[row] * xi_u[row];
523            let a_uv = c * xi_u[row] * xi_v[row] + b2u[row] * xi_v[row] + b2v[row] * xi_u[row];
524            let b_uv = d * xi_u[row] * xi_v[row] + b3u[row] * xi_v[row] + b3v[row] * xi_u[row];
525
526            coeff_eta[row] = second_directionalhessian_coeff_fromobjective_q_terms(
527                m1, m2, m3, m4, q_u, q_v, q_uv, a, a, b, a_u, a_v, a_u, a_v, a_uv, a_uv, b_u, b_v,
528                b_uv,
529            );
530            let d2_c_b = m4 * q_u * q_v * a + m3 * (q_uv * a + q_u * a_v + q_v * a_u) + m2 * a_uv;
531            let dc_b_u = m3 * q_u * a + m2 * a_u;
532            let dc_b_v = m3 * q_v * a + m2 * a_v;
533            let c_b_static = m2 * a;
534            let d2_c_b1 = m3 * q_u * q_v + m2 * q_uv;
535            let dc_b1_u = m2 * q_u;
536            let dc_b1_v = m2 * q_v;
537
538            coeff_etaw_b[row] = d2_c_b;
539            coeff_etaw_d1[row] = dc_b_u * xi_v[row] + dc_b_v * xi_u[row] + d2_c_b1;
540            coeff_etaw_d2[row] =
541                c_b_static * xi_u[row] * xi_v[row] + dc_b1_u * xi_v[row] + dc_b1_v * xi_u[row];
542            coeff_etaw_d3[row] = m1 * xi_u[row] * xi_v[row];
543
544            let dw = m2;
545            let dw_u = m3 * q_u;
546            let dw_v = m3 * q_v;
547            let dw_uv = m4 * q_u * q_v + m3 * q_uv;
548            let xixj = xi_u[row] * xi_v[row];
549            coeff_ww_bb[row] = dw_uv;
550            coeff_ww_db[row] = dw_v * xi_u[row] + dw_u * xi_v[row];
551            coeff_ww_ddb[row] = dw * xixj;
552            coeff_ww_dd[row] = 2.0 * dw * xixj;
553        }
554
555        Ok(Some(Arc::new(RowCoeffOperator::from_directions(
556            vec![p_eta, pw],
557            vec![
558                (0, x_eta_arc),
559                (1, Arc::new(geom.basis)),
560                (1, Arc::new(geom.basis_d1)),
561                (1, Arc::new(geom.basis_d2)),
562                (1, Arc::new(geom.basis_d3)),
563            ],
564            vec![
565                (0, 0, coeff_eta),
566                (0, 1, coeff_etaw_b),
567                (0, 2, coeff_etaw_d1),
568                (0, 3, coeff_etaw_d2),
569                (0, 4, coeff_etaw_d3),
570                (1, 1, coeff_ww_bb),
571                (1, 2, coeff_ww_db),
572                (1, 3, coeff_ww_ddb),
573                (2, 2, coeff_ww_dd),
574            ],
575            n,
576        ))))
577    }
578
579    /// Build the [`BlockEffectiveJacobian`] for block `block_idx`.
580    ///
581    /// `BinomialMeanWiggle` has a single location output (n_outputs = 1):
582    /// - block 0 (eta):    output 0 = design rows
583    /// - block 1 (wiggle): all zeros (nonlinear link modulation)
584    pub fn block_effective_jacobian(
585        specs: &[ParameterBlockSpec],
586        block_idx: usize,
587    ) -> Result<Box<dyn BlockEffectiveJacobian>, String> {
588        crate::block_layout::block_jacobian::AdditiveWiggleBlockLayout {
589            family: "BinomialMeanWiggleFamily",
590            n_outputs: 1,
591            additive_blocks: &[Self::BLOCK_ETA],
592            wiggle_block: Some(Self::BLOCK_WIGGLE),
593        }
594        .block_effective_jacobian(specs, block_idx)
595    }
596}
597
598impl CustomFamily for BinomialMeanWiggleFamily {
599    fn exact_newton_joint_hessian_beta_dependent(&self) -> bool {
600        true
601    }
602
603    /// The binomial mean link-wiggle refit must NOT carry the full-span
604    /// Jeffreys/Firth augmentation, for the same structural reason
605    /// `GaussianLocationScaleWiggleFamily` opts out (#684–#688) — and the
606    /// binomial wiggle hits it harder. This is a *second-stage* refit: the
607    /// pilot binomial mean fit has already converged through the ordinary
608    /// PIRLS path (which is itself un-Firthed unless the user opts in — the
609    /// standard binomial fit logs `firth=false` / `jeffreys_logdet=none`), so
610    /// the wiggle refit only adds a *penalized*, *monotone-constrained*
611    /// I-spline link-shape correction `q = η + B(η)·β_w` around an
612    /// already-finite mode. Two failure modes follow from leaving the term on
613    /// (default `true`):
614    ///
615    /// 1. **Phantom stationarity residual.** When `H_pen` is full-rank and
616    ///    well-conditioned (the normal case — e.g. `cond ≈ 5.5e2` on the #872
617    ///    pure-probit repro) the Jeffreys gate smooth-steps the curvature
618    ///    `H_Φ → 0`, but the matching score `∇Φ` does not vanish in lock-step,
619    ///    so it leaks a nonzero `|∇L − Sβ + ∇Φ|` into the inner joint-Newton
620    ///    KKT residual. The certificate then refuses every iterate and the
621    ///    outer REML rejects all seeds (exactly the #684–#688 abort signature).
622    /// 2. **Saturation barrier / divergence.** `−Φ = −½log|I_J|` is folded into
623    ///    the objective and `∇Φ ∝ I_J⁻¹` into the gradient. The I-spline warp
624    ///    can drive the binomial linear predictor toward saturation, where the
625    ///    reduced Fisher information `I_J` goes singular: `−Φ → +∞` and
626    ///    `∇Φ → ∞`. The augmented objective grows a barrier that the joint
627    ///    Newton diverges into — the #872 repro runs the full 1200-cycle budget
628    ///    with the augmented objective pinned at ~4.6e9 and the augmented
629    ///    residual at ~5.8e9 while the plain data gradient is only ~2.3e2,
630    ///    aborting the documented `link(type=flexible(...)) + linkwiggle(...)`
631    ///    fit.
632    ///
633    /// Separation robustness is not lost: the wiggle block carries both a
634    /// difference penalty (λ selected by REML) and a hard non-negativity
635    /// constraint, and the underlying mean is fit by the pilot; a penalized,
636    /// constrained refit around a finite pilot mode does not run away to
637    /// `β → ∞` the way an unpenalized MLE can. Turning the term off here makes
638    /// the wiggle refit consistent with the un-Firthed pilot and removes the
639    /// phantom residual that blocked convergence.
640    fn joint_jeffreys_term_required(&self) -> bool {
641        false
642    }
643
644    fn coefficient_hessian_cost(&self, specs: &[ParameterBlockSpec]) -> u64 {
645        // The mean-wiggle Hessian is exposed as a row-coefficient operator,
646        // so the hot representation cost is one Θ(n · (p_eta + p_w)) HVP
647        // rather than dense Θ(n · (p_eta + p_w)^2) assembly.
648        let p_total = specs
649            .iter()
650            .map(|s| s.design.ncols() as u64)
651            .fold(0u64, |acc, p| acc.saturating_add(p));
652        (self.y.len() as u64).saturating_mul(p_total.max(1))
653    }
654
655    fn block_linear_constraints(
656        &self,
657        _: &[ParameterBlockState],
658        block_idx: usize,
659        spec: &ParameterBlockSpec,
660    ) -> Result<Option<LinearInequalityConstraints>, String> {
661        if block_idx != Self::BLOCK_WIGGLE {
662            return Ok(None);
663        }
664        Ok(monotone_wiggle_nonnegative_constraints(spec.design.ncols()))
665    }
666
667    fn post_update_block_beta(
668        &self,
669        _: &[ParameterBlockState],
670        block_idx: usize,
671        block_spec: &ParameterBlockSpec,
672        beta: Array1<f64>,
673    ) -> Result<Array1<f64>, String> {
674        assert!(!block_spec.name.is_empty());
675        if block_idx != Self::BLOCK_WIGGLE {
676            return Ok(beta);
677        }
678        let beta = project_monotone_wiggle_beta_nonnegative(beta);
679        validate_monotone_wiggle_beta_nonnegative(&beta, "BinomialMeanWiggleFamily post-update")?;
680        Ok(beta)
681    }
682
683    fn evaluate(&self, block_states: &[ParameterBlockState]) -> Result<FamilyEvaluation, String> {
684        validate_block_count::<GamlssError>("BinomialMeanWiggleFamily", 2, block_states.len())?;
685        let eta = &block_states[Self::BLOCK_ETA].eta;
686        let etaw = &block_states[Self::BLOCK_WIGGLE].eta;
687        let betaw = &block_states[Self::BLOCK_WIGGLE].beta;
688        let n = self.y.len();
689        if eta.len() != n || etaw.len() != n || self.weights.len() != n {
690            return Err(GamlssError::DimensionMismatch {
691                reason: "BinomialMeanWiggleFamily input size mismatch".to_string(),
692            }
693            .into());
694        }
695        let dq_dq0 = self.wiggle_dq_dq0(eta.view(), betaw.view())?;
696        if dq_dq0.len() != n {
697            return Err(GamlssError::DimensionMismatch {
698                reason: format!(
699                    "BinomialMeanWiggleFamily dq/dq0 length mismatch: got {}, expected {}",
700                    dq_dq0.len(),
701                    n
702                ),
703            }
704            .into());
705        }
706
707        let mut ll = 0.0;
708        let mut z_eta = Array1::<f64>::zeros(n);
709        let mut w_eta = Array1::<f64>::zeros(n);
710        let mut z_wiggle = Array1::<f64>::zeros(n);
711        let mut w_wiggle = Array1::<f64>::zeros(n);
712        for i in 0..n {
713            let q = eta[i] + etaw[i];
714            let (mu_q, d1_q) = inverse_link_mu_d1_for_inverse_link(&self.link_kind, q)
715                .map_err(|e| format!("fixed-link wiggle inverse-link evaluation failed: {e}"))?;
716            let yi = self.y[i];
717            let wi = self.weights[i];
718            ll += binomial_location_scale_log_likelihood(yi, wi, q, &self.link_kind, mu_q)?;
719
720            let mu = mu_q.clamp(1e-12, 1.0 - 1e-12);
721            let var = (mu * (1.0 - mu)).max(MIN_PROB);
722            let dmu_deta = d1_q * dq_dq0[i];
723            let dmu_dw = d1_q;
724            if wi == 0.0 || !var.is_finite() {
725                z_eta[i] = eta[i];
726                z_wiggle[i] = etaw[i];
727                continue;
728            }
729
730            if dmu_deta.is_finite() {
731                w_eta[i] = floor_positiveweight(wi * (dmu_deta * dmu_deta / var), MIN_WEIGHT);
732                z_eta[i] = eta[i] + (yi - mu) / signedwith_floor(dmu_deta, MIN_DERIV);
733            } else {
734                z_eta[i] = eta[i];
735            }
736
737            if dmu_dw.is_finite() {
738                w_wiggle[i] = floor_positiveweight(wi * (dmu_dw * dmu_dw / var), MIN_WEIGHT);
739                z_wiggle[i] = etaw[i] + (yi - mu) / signedwith_floor(dmu_dw, MIN_DERIV);
740            } else {
741                z_wiggle[i] = etaw[i];
742            }
743        }
744
745        Ok(FamilyEvaluation {
746            log_likelihood: ll,
747            blockworking_sets: vec![
748                BlockWorkingSet::diagonal_checked(z_eta, w_eta)?,
749                BlockWorkingSet::diagonal_checked(z_wiggle, w_wiggle)?,
750            ],
751        })
752    }
753
754    fn block_geometry(
755        &self,
756        block_states: &[ParameterBlockState],
757        spec: &ParameterBlockSpec,
758    ) -> Result<(DesignMatrix, Array1<f64>), String> {
759        if spec.name != "wiggle" {
760            return Ok((spec.design.clone(), spec.offset.clone()));
761        }
762        if block_states.is_empty() {
763            return Err(GamlssError::UnsupportedConfiguration {
764                reason: "wiggle geometry requires eta block".to_string(),
765            }
766            .into());
767        }
768        let eta = &block_states[Self::BLOCK_ETA].eta;
769        if eta.len() != self.y.len() {
770            return Err(GamlssError::DimensionMismatch {
771                reason: "BinomialMeanWiggleFamily eta size mismatch".to_string(),
772            }
773            .into());
774        }
775        let x = self.wiggle_design(eta.view())?;
776        if x.ncols() != spec.design.ncols() {
777            return Err(GamlssError::DimensionMismatch {
778                reason: format!(
779                    "dynamic wiggle design col mismatch: got {}, expected {}",
780                    x.ncols(),
781                    spec.design.ncols()
782                ),
783            }
784            .into());
785        }
786        let nrows = x.nrows();
787        Ok((
788            DesignMatrix::Dense(gam_linalg::matrix::DenseDesignMatrix::from(x)),
789            Array1::zeros(nrows),
790        ))
791    }
792
793    fn block_geometry_is_dynamic(&self) -> bool {
794        true
795    }
796
797    fn exact_newton_joint_hessian_workspace(
798        &self,
799        block_states: &[ParameterBlockState],
800        specs: &[ParameterBlockSpec],
801    ) -> Result<Option<Arc<dyn ExactNewtonJointHessianWorkspace>>, String> {
802        let x_eta = self.dense_eta_design_fromspecs(specs)?.into_owned();
803        let workspace =
804            BinomialMeanWiggleHessianWorkspace::new(self.clone(), block_states.to_vec(), x_eta)?;
805        Ok(Some(Arc::new(workspace)))
806    }
807
808    fn inner_coefficient_hessian_hvp_available(&self, specs: &[ParameterBlockSpec]) -> bool {
809        self.dense_eta_design_fromspecs(specs).is_ok()
810    }
811
812    fn exact_newton_joint_hessian_with_specs(
813        &self,
814        block_states: &[ParameterBlockState],
815        specs: &[ParameterBlockSpec],
816    ) -> Result<Option<Array2<f64>>, String> {
817        validate_block_count::<GamlssError>("BinomialMeanWiggleFamily", 2, block_states.len())?;
818        let x_eta = self.dense_eta_design_fromspecs(specs)?;
819        let eta = &block_states[Self::BLOCK_ETA].eta;
820        let etaw = &block_states[Self::BLOCK_WIGGLE].eta;
821        let betaw = &block_states[Self::BLOCK_WIGGLE].beta;
822        let n = self.y.len();
823        if eta.len() != n || etaw.len() != n || self.weights.len() != n {
824            return Err(GamlssError::DimensionMismatch {
825                reason: "BinomialMeanWiggleFamily input size mismatch".to_string(),
826            }
827            .into());
828        }
829        let geom = self.wiggle_geometry(eta.view(), betaw.view())?;
830        let p_eta = x_eta.ncols();
831        let pw = geom.basis.ncols();
832        let mut coeff_eta = Array1::<f64>::zeros(n);
833        let mut coeff_etaw_b = Array1::<f64>::zeros(n);
834        let mut coeff_etaw_d1 = Array1::<f64>::zeros(n);
835        let mut coeff_ww = Array1::<f64>::zeros(n);
836        for row in 0..n {
837            let q = eta[row] + etaw[row];
838            let (m1, m2, _) = self.neglog_q_derivatives(self.y[row], self.weights[row], q)?;
839            let a = geom.dq_dq0[row];
840            let b = geom.d2q_dq02[row];
841            coeff_eta[row] = hessian_coeff_fromobjective_q_terms(m1, m2, a, a, b);
842            coeff_etaw_b[row] = m2 * a;
843            coeff_etaw_d1[row] = m1;
844            coeff_ww[row] = m2;
845        }
846        let h_eta_eta = xt_diag_x_dense(&x_eta, &coeff_eta)?;
847        let h_eta_w = xt_diag_y_dense(&x_eta, &coeff_etaw_b, &geom.basis)?
848            + &xt_diag_y_dense(&x_eta, &coeff_etaw_d1, &geom.basis_d1)?;
849        let h_ww = xt_diag_x_dense(&geom.basis, &coeff_ww)?;
850        assert_eq!(h_eta_eta.nrows(), p_eta);
851        assert_eq!(h_ww.nrows(), pw);
852        Ok(Some(binomial_pack_mean_wiggle_joint_symmetrichessian(
853            &h_eta_eta, &h_eta_w, &h_ww,
854        )))
855    }
856
857    fn exact_newton_joint_hessian_directional_derivative_with_specs(
858        &self,
859        block_states: &[ParameterBlockState],
860        specs: &[ParameterBlockSpec],
861        d_beta_flat: &Array1<f64>,
862    ) -> Result<Option<Array2<f64>>, String> {
863        validate_block_count::<GamlssError>("BinomialMeanWiggleFamily", 2, block_states.len())?;
864        let x_eta = self.dense_eta_design_fromspecs(specs)?;
865        let eta = &block_states[Self::BLOCK_ETA].eta;
866        let etaw = &block_states[Self::BLOCK_WIGGLE].eta;
867        let betaw = &block_states[Self::BLOCK_WIGGLE].beta;
868        let n = self.y.len();
869        if eta.len() != n || etaw.len() != n || self.weights.len() != n {
870            return Err(GamlssError::DimensionMismatch {
871                reason: "BinomialMeanWiggleFamily input size mismatch".to_string(),
872            }
873            .into());
874        }
875        let geom = self.wiggle_geometry(eta.view(), betaw.view())?;
876        let p_eta = x_eta.ncols();
877        let pw = geom.basis.ncols();
878        if d_beta_flat.len() != p_eta + pw {
879            return Err(GamlssError::DimensionMismatch {
880                reason: format!(
881                    "BinomialMeanWiggleFamily joint d_beta length mismatch: got {}, expected {}",
882                    d_beta_flat.len(),
883                    p_eta + pw
884                ),
885            }
886            .into());
887        }
888        let u_eta = d_beta_flat.slice(s![0..p_eta]).to_owned();
889        let uw = d_beta_flat.slice(s![p_eta..p_eta + pw]).to_owned();
890        let xi = x_eta.dot(&u_eta);
891        let phi = geom.basis.dot(&uw);
892        let basis1_u = geom.basis_d1.dot(&uw);
893        let basis2_u = geom.basis_d2.dot(&uw);
894
895        let mut coeff_eta = Array1::<f64>::zeros(n);
896        let mut coeff_etaw_b = Array1::<f64>::zeros(n);
897        let mut coeff_etaw_d1 = Array1::<f64>::zeros(n);
898        let mut coeff_etaw_d2 = Array1::<f64>::zeros(n);
899        let mut coeff_ww_bb = Array1::<f64>::zeros(n);
900        let mut coeff_ww_db = Array1::<f64>::zeros(n);
901        for row in 0..n {
902            let q = eta[row] + etaw[row];
903            let (m1, m2, m3) = self.neglog_q_derivatives(self.y[row], self.weights[row], q)?;
904            let a = geom.dq_dq0[row];
905            let b = geom.d2q_dq02[row];
906            let c = geom.d3q_dq03[row];
907            let q_u = a * xi[row] + phi[row];
908            let a_u = b * xi[row] + basis1_u[row];
909            let b_u = c * xi[row] + basis2_u[row];
910            coeff_eta[row] = directionalhessian_coeff_fromobjective_q_terms(
911                m1, m2, m3, q_u, a, a, b, a_u, a_u, b_u,
912            );
913            coeff_etaw_b[row] = m3 * q_u * a + m2 * a_u;
914            coeff_etaw_d1[row] = m2 * (a * xi[row] + q_u);
915            coeff_etaw_d2[row] = m1 * xi[row];
916            coeff_ww_bb[row] = m3 * q_u;
917            coeff_ww_db[row] = m2 * xi[row];
918        }
919
920        let d_h_eta_eta = xt_diag_x_dense(&x_eta, &coeff_eta)?;
921        let d_h_eta_w = xt_diag_y_dense(&x_eta, &coeff_etaw_b, &geom.basis)?
922            + &xt_diag_y_dense(&x_eta, &coeff_etaw_d1, &geom.basis_d1)?
923            + &xt_diag_y_dense(&x_eta, &coeff_etaw_d2, &geom.basis_d2)?;
924        let a_ww = xt_diag_y_dense(&geom.basis_d1, &coeff_ww_db, &geom.basis)?;
925        let d_h_ww = xt_diag_x_dense(&geom.basis, &coeff_ww_bb)? + &a_ww + a_ww.t();
926        Ok(Some(binomial_pack_mean_wiggle_joint_symmetrichessian(
927            &d_h_eta_eta,
928            &d_h_eta_w,
929            &d_h_ww,
930        )))
931    }
932
933    /// Exact second-order directional derivative D²H[u,v] of the joint Hessian
934    /// for the BinomialMeanWiggle two-block model (eta, wiggle).
935    ///
936    /// # Mathematical derivation
937    ///
938    /// The negative log-likelihood Hessian element for indices (a, b) in the
939    /// joint coefficient vector is:
940    ///
941    ///   H_ab = m2 * q_a * q_b + m1 * q_ab
942    ///
943    /// where m_k = d^k F / dq^k (k-th derivative of the negative log-likelihood
944    /// w.r.t. the effective predictor q), q_a = dq/d(beta_a), and q_ab =
945    /// d²q/(d(beta_a) d(beta_b)).
946    ///
947    /// The effective predictor is q = q0 + w(q0) where q0 = X_eta * beta_eta
948    /// and w(q0) = B(q0) * beta_w is the link wiggle.  Write:
949    ///   a = dq/dq0 = 1 + B'·beta_w       (geometry first derivative)
950    ///   b = d²q/dq0² = B''·beta_w         (geometry second derivative)
951    ///   c = d³q/dq0³ = B'''·beta_w        (geometry third derivative)
952    ///   d = d⁴q/dq0⁴ = B''''·beta_w       (geometry fourth derivative)
953    ///
954    /// For a perturbation direction u = (u_eta, u_w), the chain-rule
955    /// perturbations are:
956    ///   q_u   = a·xi_u + phi_u             (first-order predictor perturbation)
957    ///   a_u   = b·xi_u + basis1_u          (perturbation of geometry factor a)
958    ///   b_u   = c·xi_u + basis2_u          (perturbation of geometry factor b)
959    ///   c_u   = d·xi_u + basis3_u          (perturbation of geometry factor c)
960    ///
961    /// where xi_u = X_eta·u_eta, phi_u = B·u_w, basis_k_u = B^(k)·u_w.
962    ///
963    /// Mixed second-order perturbations (u,v) are:
964    ///   q_uv  = b·xi_u·xi_v + basis1_u·xi_v + basis1_v·xi_u
965    ///   a_uv  = c·xi_u·xi_v + basis2_u·xi_v + basis2_v·xi_u
966    ///   b_uv  = d·xi_u·xi_v + basis3_u·xi_v + basis3_v·xi_u
967    ///
968    /// ## Block decomposition
969    ///
970    /// **eta-eta block** (X_eta' diag(coeff) X_eta):
971    ///   The Hessian element for eta indices (i,j) factors as
972    ///     H(eta_i, eta_j) = [m2·a² + m1·b] · x_eta(i)·x_eta(j)
973    ///   so D²H_eta_eta[u,v] = X_eta' diag(coeff_eta) X_eta
974    ///   where coeff_eta uses `second_directionalhessian_coeff_fromobjective_q_terms`
975    ///   with q_a=a, q_b=a, q_ab=b and their chain-rule perturbations.
976    ///
977    /// **eta-w block** (X_eta' diag(...) [B, B', B'', B''']):
978    ///   The static Hessian is:
979    ///     H(eta_i, w_j) = (m2·a)·x_eta(i)·B_j + m1·x_eta(i)·B'_j
980    ///   Taking D²[u,v] requires differentiating both the scalar coefficients
981    ///   (m2·a, m1) and the basis matrices (B, B' depend on q0 via the chain
982    ///   rule dB_j/du = B'_j·xi_u).  The full product rule gives four basis-matrix
983    ///   tiers: B, B', B'', B'''.
984    ///
985    /// **w-w block** (B' diag(...) B, etc.):
986    ///   The static Hessian is H(w_i, w_j) = m2·B_i·B_j.
987    ///   D²[u,v] expands via the product rule on m2, B_i, B_j, each of which
988    ///   depends on beta through q and q0.  This gives terms involving
989    ///   B·B, B'·B, B'·B', and B''·B (all symmetrised).
990    fn exact_newton_joint_hessian_second_directional_derivative_with_specs(
991        &self,
992        block_states: &[ParameterBlockState],
993        specs: &[ParameterBlockSpec],
994        d_beta_u_flat: &Array1<f64>,
995        d_beta_v_flat: &Array1<f64>,
996    ) -> Result<Option<Array2<f64>>, String> {
997        validate_block_count::<GamlssError>("BinomialMeanWiggleFamily", 2, block_states.len())?;
998        let x_eta = self.dense_eta_design_fromspecs(specs)?;
999        let eta = &block_states[Self::BLOCK_ETA].eta;
1000        let etaw = &block_states[Self::BLOCK_WIGGLE].eta;
1001        let betaw = &block_states[Self::BLOCK_WIGGLE].beta;
1002        let n = self.y.len();
1003        if eta.len() != n || etaw.len() != n || self.weights.len() != n {
1004            return Err(GamlssError::DimensionMismatch {
1005                reason: "BinomialMeanWiggleFamily input size mismatch".to_string(),
1006            }
1007            .into());
1008        }
1009        let geom = self.wiggle_geometry(eta.view(), betaw.view())?;
1010        let p_eta = x_eta.ncols();
1011        let pw = geom.basis.ncols();
1012        let total = p_eta + pw;
1013        if d_beta_u_flat.len() != total || d_beta_v_flat.len() != total {
1014            return Err(GamlssError::DimensionMismatch { reason: format!(
1015                "BinomialMeanWiggleFamily joint second d_beta length mismatch: got {} and {}, expected {}",
1016                d_beta_u_flat.len(),
1017                d_beta_v_flat.len(),
1018                total
1019            ) }.into());
1020        }
1021
1022        // Split directions into eta and wiggle components.
1023        let u_eta = d_beta_u_flat.slice(s![0..p_eta]).to_owned();
1024        let v_eta = d_beta_v_flat.slice(s![0..p_eta]).to_owned();
1025        let uw = d_beta_u_flat.slice(s![p_eta..total]).to_owned();
1026        let vw = d_beta_v_flat.slice(s![p_eta..total]).to_owned();
1027
1028        // Per-row linear-predictor perturbations from each direction.
1029        let xi_u = x_eta.dot(&u_eta); // eta perturbation in direction u
1030        let xi_v = x_eta.dot(&v_eta); // eta perturbation in direction v
1031        let phi_u = geom.basis.dot(&uw); // direct wiggle basis, direction u
1032        let phi_v = geom.basis.dot(&vw); // direct wiggle basis, direction v
1033        let b1u = geom.basis_d1.dot(&uw); // first-derivative basis, direction u
1034        let b1v = geom.basis_d1.dot(&vw);
1035        let b2u = geom.basis_d2.dot(&uw); // second-derivative basis, direction u
1036        let b2v = geom.basis_d2.dot(&vw);
1037        let b3u = geom.basis_d3.dot(&uw); // third-derivative basis, direction u
1038        let b3v = geom.basis_d3.dot(&vw);
1039
1040        // Per-row chain-rule perturbations of q, a = dq/dq0, b = d²q/dq0²:
1041        //   q_u = a·xi_u + phi_u
1042        //   a_u = b·xi_u + basis1_u
1043        //   b_u = c·xi_u + basis2_u
1044        //   c_u = d·xi_u + basis3_u
1045        // Mixed second-order perturbations:
1046        //   q_uv = b·xi_u·xi_v + basis1_u·xi_v + basis1_v·xi_u
1047        //   a_uv = c·xi_u·xi_v + basis2_u·xi_v + basis2_v·xi_u
1048        //   b_uv = d·xi_u·xi_v + basis3_u·xi_v + basis3_v·xi_u
1049
1050        // Scaled basis matrices for the cross-product terms in the w-w and eta-w
1051        // blocks (same pattern as GaussianLocationScaleWiggleFamily).
1052        let basis_u = scale_matrix_rows(&geom.basis_d1, &xi_u)?; // dB/du = B'·xi_u
1053        let basis_v = scale_matrix_rows(&geom.basis_d1, &xi_v)?; // dB/dv = B'·xi_v
1054        let basis_uv = scale_matrix_rows(&geom.basis_d2, &(&xi_u * &xi_v))?; // d²B/dudv = B''·xi_u·xi_v
1055        // Per-row coefficient arrays for assembling the block-matrix products.
1056        let mut coeff_eta = Array1::<f64>::zeros(n);
1057
1058        // Coefficients for the eta-w block: X_eta' diag(c_*) M where M ∈ {B, B', B'', B'''}
1059        //
1060        // The static cross-Hessian is:
1061        //   H(eta_i, w_j) = (m2·a)·x_i·B_j + m1·x_i·B'_j
1062        // where B_j and B'_j are row evaluations of basis column j.
1063        //
1064        // Write C_B = m2·a (scalar coefficient multiplying B in the cross block)
1065        // and   C_B1 = m1  (scalar coefficient multiplying B' in the cross block).
1066        //
1067        // Product rule on C_B·B:
1068        //   d(C_B·B)/du = (dC_B/du)·B + C_B·B'·xi_u
1069        //   d²(C_B·B)/dudv = (d²C_B/dudv)·B + (dC_B/du)·B'·xi_v
1070        //                   + (dC_B/dv)·B'·xi_u + C_B·B''·xi_u·xi_v
1071        //
1072        // Product rule on C_B1·B':
1073        //   d²(C_B1·B')/dudv = (d²C_B1/dudv)·B' + (dC_B1/du)·B''·xi_v
1074        //                     + (dC_B1/dv)·B''·xi_u + C_B1·B'''·xi_u·xi_v
1075        //
1076        // Derivatives of the scalar coefficients:
1077        //   C_B  = m2·a
1078        //   dC_B/du  = m3·q_u·a + m2·a_u
1079        //   dC_B/dv  = m3·q_v·a + m2·a_v
1080        //   d²C_B/dudv = m4·q_u·q_v·a + m3·(q_uv·a + q_u·a_v + q_v·a_u) + m2·a_uv
1081        //
1082        //   C_B1 = m1
1083        //   dC_B1/du = m2·q_u
1084        //   dC_B1/dv = m2·q_v
1085        //   d²C_B1/dudv = m3·q_u·q_v + m2·q_uv
1086        //
1087        // Grouping by basis-matrix tier:
1088        //   B:   d²C_B/dudv
1089        //   B':  (dC_B/du)·xi_v + (dC_B/dv)·xi_u + d²C_B1/dudv
1090        //   B'': C_B·xi_u·xi_v + (dC_B1/du)·xi_v + (dC_B1/dv)·xi_u
1091        //   B''': C_B1·xi_u·xi_v
1092        let mut coeff_etaw_b = Array1::<f64>::zeros(n);
1093        let mut coeff_etaw_d1 = Array1::<f64>::zeros(n);
1094        let mut coeff_etaw_d2 = Array1::<f64>::zeros(n);
1095        let mut coeff_etaw_d3 = Array1::<f64>::zeros(n);
1096
1097        // Coefficients for the w-w block.
1098        //
1099        // The static w-w Hessian is:
1100        //   H(w_i, w_j) = m2·B_i·B_j
1101        //
1102        // Note: there is no m1·q_ij term because d²q/(d(beta_w_i) d(beta_w_j)) = 0
1103        // (the basis vectors B_i enter q linearly in beta_w).
1104        //
1105        // Product rule on m2·B_i·B_j, treating each factor as depending on beta:
1106        //   d²(m2·B_i·B_j)/dudv
1107        //     = (d²m2/dudv)·B_i·B_j                        → B'diag B  (symmetrised)
1108        //     + (dm2/du)·(B'_i·xi_v·B_j + B_i·B'_j·xi_v)  → dw_u terms
1109        //     + (dm2/dv)·(B'_i·xi_u·B_j + B_i·B'_j·xi_u)  → dw_v terms
1110        //     + m2·(B''_i·xi_u·xi_v·B_j + B'_i·xi_u·B'_j·xi_v
1111        //          + B'_i·xi_v·B'_j·xi_u + B_i·B''_j·xi_u·xi_v)
1112        //
1113        // where dm2/du = m3·q_u, dm2/dv = m3·q_v, d²m2/dudv = m4·q_u·q_v + m3·q_uv.
1114        //
1115        // Following the Gaussian LS wiggle pattern, we express this via:
1116        //   xt_diag_x_dense(B, dw_uv)                    — coeff: d²m2
1117        //   xt_diag_y_dense(basis_u, dw_v, B) + transpose — dB/du weighted by dm2/dv
1118        //   xt_diag_y_dense(basis_v, dw_u, B) + transpose — dB/dv weighted by dm2/du
1119        //   xt_diag_y_dense(basis_uv, w, B) + transpose   — d²B/dudv weighted by m2
1120        //   xt_diag_y_dense(basis_u, w, basis_v) + transpose — dB/du·dB/dv weighted by m2
1121        let mut dw = Array1::<f64>::zeros(n);
1122        let mut dw_u = Array1::<f64>::zeros(n);
1123        let mut dw_v = Array1::<f64>::zeros(n);
1124        let mut dw_uv = Array1::<f64>::zeros(n);
1125
1126        for row in 0..n {
1127            let q = eta[row] + etaw[row];
1128            let (m1, m2, m3) = self.neglog_q_derivatives(self.y[row], self.weights[row], q)?;
1129            let m4 = self.neglog_q_fourth_derivative(self.y[row], self.weights[row], q)?;
1130            let a = geom.dq_dq0[row];
1131            let b = geom.d2q_dq02[row];
1132            let c = geom.d3q_dq03[row];
1133            let d = geom.d4q_dq04[row];
1134
1135            // Chain-rule perturbations in direction u.
1136            let q_u = a * xi_u[row] + phi_u[row];
1137            let a_u = b * xi_u[row] + b1u[row];
1138            let b_u = c * xi_u[row] + b2u[row];
1139
1140            // Chain-rule perturbations in direction v.
1141            let q_v = a * xi_v[row] + phi_v[row];
1142            let a_v = b * xi_v[row] + b1v[row];
1143            let b_v = c * xi_v[row] + b2v[row];
1144
1145            // Mixed second-order perturbations.
1146            let q_uv = b * xi_u[row] * xi_v[row] + b1u[row] * xi_v[row] + b1v[row] * xi_u[row];
1147            let a_uv = c * xi_u[row] * xi_v[row] + b2u[row] * xi_v[row] + b2v[row] * xi_u[row];
1148            let b_uv = d * xi_u[row] * xi_v[row] + b3u[row] * xi_v[row] + b3v[row] * xi_u[row];
1149
1150            // ── eta-eta block ──
1151            // H(eta_i, eta_j) uses q_a = a, q_b = a, q_ab = b (absorbing x_eta
1152            // into the matrix product).  The perturbations of these geometric
1153            // quantities are: dq_a/du = a_u, dq_b/du = a_u (since q_a = q_b = a),
1154            // dq_ab/du = b_u (since q_ab = b), and analogously for v.
1155            coeff_eta[row] = second_directionalhessian_coeff_fromobjective_q_terms(
1156                m1, m2, m3, m4, q_u, q_v, q_uv, a, a, b, // q_a, q_b, q_ab
1157                a_u, a_v, // dq_a_u, dq_a_v
1158                a_u, a_v, // dq_b_u, dq_b_v  (q_b = a so same perturbation)
1159                a_uv, a_uv, // d2q_a_uv, d2q_b_uv
1160                b_u, b_v,  // dq_ab_u, dq_ab_v  (q_ab = b)
1161                b_uv, // d2q_ab_uv
1162            );
1163
1164            // ── eta-w block coefficients ──
1165            // See the derivation in the docstring above.  We group by which basis
1166            // matrix tier (B, B', B'', B''') the coefficient multiplies.
1167
1168            // d²(m2·a)/dudv
1169            let d2_c_b = m4 * q_u * q_v * a + m3 * (q_uv * a + q_u * a_v + q_v * a_u) + m2 * a_uv;
1170            // d(m2·a)/du and d(m2·a)/dv
1171            let dc_b_u = m3 * q_u * a + m2 * a_u;
1172            let dc_b_v = m3 * q_v * a + m2 * a_v;
1173            // m2·a (static coefficient for B in the cross block)
1174            let c_b_static = m2 * a;
1175            // d²(m1)/dudv
1176            let d2_c_b1 = m3 * q_u * q_v + m2 * q_uv;
1177            // d(m1)/du and d(m1)/dv
1178            let dc_b1_u = m2 * q_u;
1179            let dc_b1_v = m2 * q_v;
1180
1181            coeff_etaw_b[row] = d2_c_b;
1182            coeff_etaw_d1[row] = dc_b_u * xi_v[row] + dc_b_v * xi_u[row] + d2_c_b1;
1183            coeff_etaw_d2[row] =
1184                c_b_static * xi_u[row] * xi_v[row] + dc_b1_u * xi_v[row] + dc_b1_v * xi_u[row];
1185            coeff_etaw_d3[row] = m1 * xi_u[row] * xi_v[row];
1186
1187            // ── w-w block coefficients ──
1188            // The w-w static Hessian coefficient is m2 (for B'diag B).
1189            dw[row] = m2;
1190            dw_u[row] = m3 * q_u;
1191            dw_v[row] = m3 * q_v;
1192            dw_uv[row] = m4 * q_u * q_v + m3 * q_uv;
1193        }
1194
1195        // ── Assemble eta-eta block ──
1196        let d2_h_eta_eta = xt_diag_x_dense(&x_eta, &coeff_eta)?;
1197
1198        // ── Assemble eta-w block ──
1199        // The second-order directional derivative of the cross block H_eta_w is:
1200        //   d²H_eta_w[u,v] = X_eta' diag(coeff_etaw_b)  B
1201        //                   + X_eta' diag(coeff_etaw_d1) B'
1202        //                   + X_eta' diag(coeff_etaw_d2) B''
1203        //                   + X_eta' diag(coeff_etaw_d3) B'''
1204        let d2_h_eta_w = xt_diag_y_dense(&x_eta, &coeff_etaw_b, &geom.basis)?
1205            + &xt_diag_y_dense(&x_eta, &coeff_etaw_d1, &geom.basis_d1)?
1206            + &xt_diag_y_dense(&x_eta, &coeff_etaw_d2, &geom.basis_d2)?
1207            + &xt_diag_y_dense(&x_eta, &coeff_etaw_d3, &geom.basis_d3)?;
1208
1209        // ── Assemble w-w block ──
1210        // Following the Gaussian LS wiggle pattern (lines 6351-6363), the w-w
1211        // second directional derivative is assembled from scaled basis products:
1212        //
1213        //   d²(m2·B_i·B_j)/dudv decomposition:
1214        //     (d²m2)     · B_i·B_j        → xt_diag_x(B, dw_uv)
1215        //     (dm2/du)   · dB_j/dv · B_i  → xt_diag_y(basis_v, dw_u, B) + transpose
1216        //     (dm2/dv)   · dB_j/du · B_i  → xt_diag_y(basis_u, dw_v, B) + transpose
1217        //     m2 · d²B_j/dudv · B_i       → xt_diag_y(basis_uv, dw, B) + transpose
1218        //     m2 · dB_i/du · dB_j/dv      → xt_diag_y(basis_u, dw, basis_v) + transpose
1219        let a_ab = xt_diag_y_dense(&basis_uv, &dw, &geom.basis)?;
1220        let a_ij = xt_diag_y_dense(&basis_u, &dw, &basis_v)?;
1221        let a_iwj = xt_diag_y_dense(&basis_u, &dw_v, &geom.basis)?;
1222        let a_jwi = xt_diag_y_dense(&basis_v, &dw_u, &geom.basis)?;
1223        let d2_h_ww = &a_ab
1224            + &a_ab.t()
1225            + &a_ij
1226            + a_ij.t()
1227            + &a_iwj
1228            + a_iwj.t()
1229            + &a_jwi
1230            + a_jwi.t()
1231            + &xt_diag_x_dense(&geom.basis, &dw_uv)?;
1232
1233        Ok(Some(binomial_pack_mean_wiggle_joint_symmetrichessian(
1234            &d2_h_eta_eta,
1235            &d2_h_eta_w,
1236            &d2_h_ww,
1237        )))
1238    }
1239
1240    fn exact_newton_joint_psi_terms(
1241        &self,
1242        block_states: &[ParameterBlockState],
1243        specs: &[ParameterBlockSpec],
1244        derivative_blocks: &[Vec<CustomFamilyBlockPsiDerivative>],
1245        psi_index: usize,
1246    ) -> Result<Option<crate::custom_family::ExactNewtonJointPsiTerms>, String> {
1247        validate_block_count::<GamlssError>("BinomialMeanWiggleFamily", 2, block_states.len())?;
1248        if derivative_blocks.len() != 2 {
1249            return Err(GamlssError::DimensionMismatch { reason: format!(
1250                "BinomialMeanWiggleFamily joint psi terms expect 2 derivative block lists, got {}",
1251                derivative_blocks.len()
1252            ) }.into());
1253        }
1254        let x_eta = self.dense_eta_design_fromspecs(specs)?;
1255        let eta = &block_states[Self::BLOCK_ETA].eta;
1256        let etaw = &block_states[Self::BLOCK_WIGGLE].eta;
1257        let betaw = &block_states[Self::BLOCK_WIGGLE].beta;
1258        let n = self.y.len();
1259        if eta.len() != n || etaw.len() != n || self.weights.len() != n {
1260            return Err(GamlssError::DimensionMismatch {
1261                reason: "BinomialMeanWiggleFamily input size mismatch".to_string(),
1262            }
1263            .into());
1264        }
1265        let geom = self.wiggle_geometry(eta.view(), betaw.view())?;
1266        let p_eta = x_eta.ncols();
1267        let pw = geom.basis.ncols();
1268        let implicit_dir =
1269            self.exact_newton_joint_psi_action(block_states, derivative_blocks, psi_index, p_eta)?;
1270        let dense_dir = if implicit_dir.is_none() {
1271            self.exact_newton_joint_psi_direction(
1272                block_states,
1273                derivative_blocks,
1274                psi_index,
1275                &x_eta,
1276            )?
1277        } else {
1278            None
1279        };
1280        let z_eta_psi = if let Some((_, ref z_eta_psi)) = implicit_dir {
1281            z_eta_psi
1282        } else if let Some(ref dir_a) = dense_dir {
1283            &dir_a.z_eta_psi
1284        } else {
1285            return Ok(None);
1286        };
1287
1288        let mut objective_psi = 0.0;
1289        let mut score_eta_xa = Array1::<f64>::zeros(n);
1290        let mut score_eta_x = Array1::<f64>::zeros(n);
1291        let mut score_w_b = Array1::<f64>::zeros(n);
1292        let mut score_w_d1 = Array1::<f64>::zeros(n);
1293
1294        let mut coeff_eta_eta_xx = Array1::<f64>::zeros(n);
1295        let mut coeff_eta_eta_xa_x = Array1::<f64>::zeros(n);
1296        let mut coeff_eta_w_xa_b = Array1::<f64>::zeros(n);
1297        let mut coeff_eta_w_x_b = Array1::<f64>::zeros(n);
1298        let mut coeff_eta_w_x_d1 = Array1::<f64>::zeros(n);
1299        let mut coeff_eta_w_xa_d1 = Array1::<f64>::zeros(n);
1300        let mut coeff_eta_w_x_d2 = Array1::<f64>::zeros(n);
1301        let mut coeff_ww_bb = Array1::<f64>::zeros(n);
1302        let mut coeff_ww_db = Array1::<f64>::zeros(n);
1303
1304        for row in 0..n {
1305            let q = eta[row] + etaw[row];
1306            let (m1, m2, m3) = self.neglog_q_derivatives(self.y[row], self.weights[row], q)?;
1307            let z_a = z_eta_psi[row];
1308            let a = geom.dq_dq0[row];
1309            let b = geom.d2q_dq02[row];
1310            let c = geom.d3q_dq03[row];
1311            let q_a = a * z_a;
1312
1313            objective_psi += m1 * q_a;
1314
1315            score_eta_xa[row] = m1 * a;
1316            score_eta_x[row] = m2 * q_a * a + m1 * b * z_a;
1317            score_w_b[row] = m2 * q_a;
1318            score_w_d1[row] = m1 * z_a;
1319
1320            coeff_eta_eta_xx[row] =
1321                m3 * q_a * a * a + m2 * (2.0 * a * b * z_a + q_a * b) + m1 * c * z_a;
1322            coeff_eta_eta_xa_x[row] = m2 * a * a + m1 * b;
1323            coeff_eta_w_xa_b[row] = m2 * a;
1324            coeff_eta_w_x_b[row] = m3 * q_a * a + m2 * b * z_a;
1325            coeff_eta_w_x_d1[row] = m2 * (a * z_a + q_a);
1326            coeff_eta_w_xa_d1[row] = m1;
1327            coeff_eta_w_x_d2[row] = m1 * z_a;
1328            coeff_ww_bb[row] = m3 * q_a;
1329            coeff_ww_db[row] = m2 * z_a;
1330        }
1331
1332        let score_w = gam_linalg::faer_ndarray::fast_atv(&geom.basis, &score_w_b)
1333            + gam_linalg::faer_ndarray::fast_atv(&geom.basis_d1, &score_w_d1);
1334
1335        if let Some((action, _)) = implicit_dir {
1336            let score_eta = action.transpose_mul(score_eta_xa.view())
1337                + gam_linalg::faer_ndarray::fast_atv(x_eta.as_ref(), &score_eta_x);
1338            let score_psi = binomial_pack_mean_wiggle_joint_score(&score_eta, &score_w);
1339            let x_eta_arc = shared_dense_arc(x_eta.as_ref());
1340            let basis_arc = Arc::new(geom.basis.clone());
1341            let basis_d1_arc = Arc::new(geom.basis_d1.clone());
1342            let basis_d2_arc = Arc::new(geom.basis_d2.clone());
1343            let zeros = Array1::<f64>::zeros(n);
1344            let operator = CustomFamilyJointPsiOperator::new(
1345                p_eta + pw,
1346                vec![
1347                    CustomFamilyJointDesignChannel::new(
1348                        0..p_eta,
1349                        Arc::clone(&x_eta_arc),
1350                        Some(action),
1351                    ),
1352                    CustomFamilyJointDesignChannel::new(
1353                        p_eta..p_eta + pw,
1354                        Arc::clone(&basis_arc),
1355                        None,
1356                    ),
1357                    CustomFamilyJointDesignChannel::new(
1358                        p_eta..p_eta + pw,
1359                        Arc::clone(&basis_d1_arc),
1360                        None,
1361                    ),
1362                    CustomFamilyJointDesignChannel::new(
1363                        p_eta..p_eta + pw,
1364                        Arc::clone(&basis_d2_arc),
1365                        None,
1366                    ),
1367                ],
1368                vec![
1369                    CustomFamilyJointDesignPairContribution::new(
1370                        0,
1371                        0,
1372                        coeff_eta_eta_xa_x.clone(),
1373                        coeff_eta_eta_xx.clone(),
1374                    ),
1375                    CustomFamilyJointDesignPairContribution::new(
1376                        0,
1377                        1,
1378                        coeff_eta_w_xa_b.clone(),
1379                        coeff_eta_w_x_b.clone(),
1380                    ),
1381                    CustomFamilyJointDesignPairContribution::new(
1382                        1,
1383                        0,
1384                        coeff_eta_w_xa_b.clone(),
1385                        coeff_eta_w_x_b.clone(),
1386                    ),
1387                    CustomFamilyJointDesignPairContribution::new(
1388                        0,
1389                        2,
1390                        coeff_eta_w_xa_d1.clone(),
1391                        coeff_eta_w_x_d1.clone(),
1392                    ),
1393                    CustomFamilyJointDesignPairContribution::new(
1394                        2,
1395                        0,
1396                        coeff_eta_w_xa_d1.clone(),
1397                        coeff_eta_w_x_d1.clone(),
1398                    ),
1399                    CustomFamilyJointDesignPairContribution::new(
1400                        0,
1401                        3,
1402                        zeros.clone(),
1403                        coeff_eta_w_x_d2.clone(),
1404                    ),
1405                    CustomFamilyJointDesignPairContribution::new(
1406                        3,
1407                        0,
1408                        zeros.clone(),
1409                        coeff_eta_w_x_d2.clone(),
1410                    ),
1411                    CustomFamilyJointDesignPairContribution::new(
1412                        1,
1413                        1,
1414                        zeros.clone(),
1415                        coeff_ww_bb.clone(),
1416                    ),
1417                    CustomFamilyJointDesignPairContribution::new(
1418                        2,
1419                        1,
1420                        zeros.clone(),
1421                        coeff_ww_db.clone(),
1422                    ),
1423                    CustomFamilyJointDesignPairContribution::new(1, 2, zeros, coeff_ww_db.clone()),
1424                ],
1425            );
1426            return Ok(Some(crate::custom_family::ExactNewtonJointPsiTerms {
1427                objective_psi,
1428                score_psi,
1429                hessian_psi: Array2::zeros((0, 0)),
1430                hessian_psi_operator: Some(std::sync::Arc::new(operator)),
1431            }));
1432        }
1433
1434        let dir_a =
1435            dense_dir.expect("dense psi direction should exist when implicit direction is absent");
1436        let x_eta_psi = dir_a
1437            .x_eta_psi
1438            .as_ref()
1439            .expect("dense eta psi design should exist when implicit direction is absent");
1440        let score_psi = binomial_pack_mean_wiggle_joint_score(
1441            &(gam_linalg::faer_ndarray::fast_atv(x_eta_psi, &score_eta_xa)
1442                + gam_linalg::faer_ndarray::fast_atv(x_eta.as_ref(), &score_eta_x)),
1443            &score_w,
1444        );
1445        let a_eta_eta = xt_diag_y_dense(x_eta_psi, &coeff_eta_eta_xa_x, &x_eta)?;
1446        let h_eta_eta = &a_eta_eta + &a_eta_eta.t() + &xt_diag_x_dense(&x_eta, &coeff_eta_eta_xx)?;
1447        let h_eta_w = xt_diag_y_dense(x_eta_psi, &coeff_eta_w_xa_b, &geom.basis)?
1448            + &xt_diag_y_dense(&x_eta, &coeff_eta_w_x_b, &geom.basis)?
1449            + &xt_diag_y_dense(&x_eta, &coeff_eta_w_x_d1, &geom.basis_d1)?
1450            + &xt_diag_y_dense(x_eta_psi, &coeff_eta_w_xa_d1, &geom.basis_d1)?
1451            + &xt_diag_y_dense(&x_eta, &coeff_eta_w_x_d2, &geom.basis_d2)?;
1452        let a_ww = xt_diag_y_dense(&geom.basis_d1, &coeff_ww_db, &geom.basis)?;
1453        let h_ww = xt_diag_x_dense(&geom.basis, &coeff_ww_bb)? + &a_ww + a_ww.t();
1454
1455        Ok(Some(crate::custom_family::ExactNewtonJointPsiTerms {
1456            objective_psi,
1457            score_psi,
1458            hessian_psi: binomial_pack_mean_wiggle_joint_symmetrichessian(
1459                &h_eta_eta, &h_eta_w, &h_ww,
1460            ),
1461            hessian_psi_operator: None,
1462        }))
1463    }
1464}
1465
1466pub(crate) struct BinomialMeanWiggleHessianWorkspace {
1467    pub(crate) family: BinomialMeanWiggleFamily,
1468    pub(crate) block_states: Vec<ParameterBlockState>,
1469    pub(crate) x_eta: Arc<Array2<f64>>,
1470    pub(crate) hessian_operator: Arc<RowCoeffOperator>,
1471}
1472
1473impl BinomialMeanWiggleHessianWorkspace {
1474    pub(crate) fn new(
1475        family: BinomialMeanWiggleFamily,
1476        block_states: Vec<ParameterBlockState>,
1477        x_eta: Array2<f64>,
1478    ) -> Result<Self, String> {
1479        let x_eta = Arc::new(x_eta);
1480        let hessian_operator = family.bmw_static_hessian_operator(&block_states, x_eta.clone())?;
1481        Ok(Self {
1482            family,
1483            block_states,
1484            x_eta,
1485            hessian_operator,
1486        })
1487    }
1488}
1489
1490impl ExactNewtonJointHessianWorkspace for BinomialMeanWiggleHessianWorkspace {
1491    fn hessian_matvec_available(&self) -> bool {
1492        true
1493    }
1494
1495    fn hessian_matvec(&self, v: &Array1<f64>) -> Result<Option<Array1<f64>>, String> {
1496        Ok(Some(gam_problem::HyperOperator::mul_vec(
1497            self.hessian_operator.as_ref(),
1498            v,
1499        )))
1500    }
1501
1502    fn hessian_diagonal(&self) -> Result<Option<Array1<f64>>, String> {
1503        Ok(None)
1504    }
1505
1506    fn directional_derivative(
1507        &self,
1508        d_beta_flat: &Array1<f64>,
1509    ) -> Result<Option<Array2<f64>>, String> {
1510        Ok(self
1511            .directional_derivative_operator(d_beta_flat)?
1512            .map(|operator| operator.to_dense()))
1513    }
1514
1515    fn directional_derivative_operator(
1516        &self,
1517        d_beta_flat: &Array1<f64>,
1518    ) -> Result<Option<Arc<dyn gam_problem::HyperOperator>>, String> {
1519        self.family
1520            .bmw_directional_operator(&self.block_states, self.x_eta.clone(), d_beta_flat)
1521    }
1522
1523    fn second_directional_derivative(
1524        &self,
1525        d_beta_u_flat: &Array1<f64>,
1526        d_beta_v_flat: &Array1<f64>,
1527    ) -> Result<Option<Array2<f64>>, String> {
1528        Ok(self
1529            .second_directional_derivative_operator(d_beta_u_flat, d_beta_v_flat)?
1530            .map(|operator| operator.to_dense()))
1531    }
1532
1533    fn second_directional_derivative_operator(
1534        &self,
1535        d_beta_u: &Array1<f64>,
1536        d_beta_v: &Array1<f64>,
1537    ) -> Result<Option<Arc<dyn gam_problem::HyperOperator>>, String> {
1538        self.family.bmw_second_directional_operator(
1539            &self.block_states,
1540            self.x_eta.clone(),
1541            d_beta_u,
1542            d_beta_v,
1543        )
1544    }
1545}
1546
1547impl CustomFamilyGenerative for BinomialMeanWiggleFamily {
1548    fn generativespec(
1549        &self,
1550        block_states: &[ParameterBlockState],
1551    ) -> Result<GenerativeSpec, String> {
1552        validate_block_count::<GamlssError>("BinomialMeanWiggleFamily", 2, block_states.len())?;
1553        let eta = &block_states[Self::BLOCK_ETA].eta;
1554        let etaw = &block_states[Self::BLOCK_WIGGLE].eta;
1555        if eta.len() != self.y.len() || etaw.len() != self.y.len() {
1556            return Err(GamlssError::DimensionMismatch {
1557                reason: "BinomialMeanWiggleFamily generative size mismatch".to_string(),
1558            }
1559            .into());
1560        }
1561        let mean = gamlss_rowwise_map_result(self.y.len(), |i| {
1562            let jet = inverse_link_jet_for_inverse_link(&self.link_kind, eta[i] + etaw[i])
1563                .map_err(|e| format!("fixed-link wiggle inverse-link evaluation failed: {e}"))?;
1564            Ok(jet.mu)
1565        })?;
1566        Ok(GenerativeSpec {
1567            mean,
1568            noise: NoiseModel::Bernoulli,
1569        })
1570    }
1571}