Skip to main content

gam_models/gamlss/binomial/
wiggle_workspace.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/// Matrix-free joint-Hessian operator for the 3-block binomial
8/// location-scale wiggle family. See `BinomialWiggleOrder2Rows`
9/// for the per-row weight structure.
10pub(crate) struct BinomialLocationScaleWiggleHessianWorkspace {
11    pub(crate) family: BinomialLocationScaleWiggleFamily,
12    pub(crate) block_states: Vec<ParameterBlockState>,
13    pub(crate) x_t: Arc<Array2<f64>>,
14    pub(crate) x_ls: Arc<Array2<f64>>,
15    pub(crate) pieces: BinomialWiggleOrder2Rows,
16}
17
18impl BinomialLocationScaleWiggleHessianWorkspace {
19    pub(crate) fn new(
20        family: BinomialLocationScaleWiggleFamily,
21        block_states: Vec<ParameterBlockState>,
22        x_t: Array2<f64>,
23        x_ls: Array2<f64>,
24    ) -> Result<Self, String> {
25        let pieces = family.wiggle_order2_rows(&block_states)?;
26        Ok(Self {
27            family,
28            block_states,
29            x_t: Arc::new(x_t),
30            x_ls: Arc::new(x_ls),
31            pieces,
32        })
33    }
34
35    /// Apply a Horvitz–Thompson outer-row subsample mask to the precomputed
36    /// per-row coefficient arrays in place.
37    ///
38    /// Each sampled row's `coeff_*[i]` is multiplied by its
39    /// `WeightedOuterRow.weight` (the HT inverse-inclusion factor 1/π_i —
40    /// uniform or stratified sampling both supported). All non-sampled rows
41    /// are zeroed. Because every downstream assembly (`hessian_dense`,
42    /// `hessian_matvec`, `hessian_diagonal`) is row-linear in these arrays
43    /// via `Xᵀ diag(W) Y`, the resulting joint-Hessian is an unbiased
44    /// estimator of the full-data joint Hessian. The `b0`/`d0` basis matrices
45    /// are independent of the per-row weights and remain unchanged.
46    pub(crate) fn apply_outer_subsample(
47        &mut self,
48        rows: &[crate::outer_subsample::WeightedOuterRow],
49    ) {
50        let n = self.pieces.coeff_tt.len();
51        let mut mask_tt = Array1::<f64>::zeros(n);
52        let mut mask_tl = Array1::<f64>::zeros(n);
53        let mut mask_ll = Array1::<f64>::zeros(n);
54        let mut mask_tw_b = Array1::<f64>::zeros(n);
55        let mut mask_tw_d = Array1::<f64>::zeros(n);
56        let mut mask_lw_b = Array1::<f64>::zeros(n);
57        let mut mask_lw_d = Array1::<f64>::zeros(n);
58        let mut mask_ww = Array1::<f64>::zeros(n);
59        for r in rows {
60            let i = r.index;
61            let w = r.weight;
62            mask_tt[i] = self.pieces.coeff_tt[i] * w;
63            mask_tl[i] = self.pieces.coeff_tl[i] * w;
64            mask_ll[i] = self.pieces.coeff_ll[i] * w;
65            mask_tw_b[i] = self.pieces.coeff_tw_b[i] * w;
66            mask_tw_d[i] = self.pieces.coeff_tw_d[i] * w;
67            mask_lw_b[i] = self.pieces.coeff_lw_b[i] * w;
68            mask_lw_d[i] = self.pieces.coeff_lw_d[i] * w;
69            mask_ww[i] = self.pieces.coeff_ww[i] * w;
70        }
71        self.pieces.coeff_tt = mask_tt;
72        self.pieces.coeff_tl = mask_tl;
73        self.pieces.coeff_ll = mask_ll;
74        self.pieces.coeff_tw_b = mask_tw_b;
75        self.pieces.coeff_tw_d = mask_tw_d;
76        self.pieces.coeff_lw_b = mask_lw_b;
77        self.pieces.coeff_lw_d = mask_lw_d;
78        self.pieces.coeff_ww = mask_ww;
79    }
80}
81
82impl ExactNewtonJointHessianWorkspace for BinomialLocationScaleWiggleHessianWorkspace {
83    fn warm_up_outer_caches_for_mode(
84        &self,
85        eval_mode: gam_problem::EvalMode,
86    ) -> Result<(), String> {
87        match eval_mode {
88            gam_problem::EvalMode::ValueOnly
89            | gam_problem::EvalMode::ValueAndGradient
90            | gam_problem::EvalMode::ValueGradientHessian => Ok(()),
91        }
92    }
93
94    fn hessian_dense(&self) -> Result<Option<Array2<f64>>, String> {
95        // Same Hv structure as `hessian_matvec`, but routed through the
96        // already-existing `assemble_dense` row-pieces helper (eight GEMMs
97        // covering h_tt, h_tl, h_ll, h_tw_b, h_tw_d, h_lw_b, h_lw_d, h_ww).
98        // Avoids `total` canonical-basis HVPs in
99        // `MatrixFreeSpdOperator::materialize_dense_operator`, which at
100        // large scale (n≈320k, p_total≈82) costs ~568s per κ-iter versus
101        // ~1s for the dense build.
102        let dense = self
103            .pieces
104            .assemble_dense(self.x_t.as_ref(), self.x_ls.as_ref())?;
105        Ok(Some(dense))
106    }
107
108    fn hessian_matvec_available(&self) -> bool {
109        true
110    }
111
112    fn hessian_matvec(&self, v: &Array1<f64>) -> Result<Option<Array1<f64>>, String> {
113        let pt = self.x_t.ncols();
114        let pls = self.x_ls.ncols();
115        let pw = self.pieces.b0.ncols();
116        let total = pt + pls + pw;
117        if v.len() != total {
118            return Err(GamlssError::DimensionMismatch {
119                reason: format!(
120                    "BinomialLocationScaleWiggle matvec dimension mismatch: got {}, expected {}",
121                    v.len(),
122                    total
123                ),
124            }
125            .into());
126        }
127        let v_t = v.slice(s![0..pt]);
128        let v_ls = v.slice(s![pt..pt + pls]);
129        let v_w = v.slice(s![pt + pls..total]);
130
131        let u_t = self.x_t.dot(&v_t);
132        let u_ls = self.x_ls.dot(&v_ls);
133        let u_b = self.pieces.b0.dot(&v_w);
134        let u_d = self.pieces.d0.dot(&v_w);
135
136        let r_t = &self.pieces.coeff_tt * &u_t
137            + &self.pieces.coeff_tl * &u_ls
138            + &self.pieces.coeff_tw_b * &u_b
139            + &self.pieces.coeff_tw_d * &u_d;
140        let r_ls = &self.pieces.coeff_tl * &u_t
141            + &self.pieces.coeff_ll * &u_ls
142            + &self.pieces.coeff_lw_b * &u_b
143            + &self.pieces.coeff_lw_d * &u_d;
144        let r_b = &self.pieces.coeff_tw_b * &u_t
145            + &self.pieces.coeff_lw_b * &u_ls
146            + &self.pieces.coeff_ww * &u_b;
147        let r_d = &self.pieces.coeff_tw_d * &u_t + &self.pieces.coeff_lw_d * &u_ls;
148
149        let out_t = fast_atv(self.x_t.as_ref(), &r_t);
150        let out_ls = fast_atv(self.x_ls.as_ref(), &r_ls);
151        let out_w = fast_atv(&self.pieces.b0, &r_b) + &fast_atv(&self.pieces.d0, &r_d);
152
153        let mut out = Array1::<f64>::zeros(total);
154        out.slice_mut(s![0..pt]).assign(&out_t);
155        out.slice_mut(s![pt..pt + pls]).assign(&out_ls);
156        out.slice_mut(s![pt + pls..total]).assign(&out_w);
157        Ok(Some(out))
158    }
159
160    fn hessian_diagonal(&self) -> Result<Option<Array1<f64>>, String> {
161        let pt = self.x_t.ncols();
162        let pls = self.x_ls.ncols();
163        let pw = self.pieces.b0.ncols();
164        let total = pt + pls + pw;
165        let mut diag = Array1::<f64>::zeros(total);
166        let n = self.pieces.coeff_tt.len();
167        for j in 0..pt {
168            let col = self.x_t.column(j);
169            let mut acc = 0.0;
170            for i in 0..n {
171                let v = col[i];
172                acc += self.pieces.coeff_tt[i] * v * v;
173            }
174            diag[j] = acc;
175        }
176        for j in 0..pls {
177            let col = self.x_ls.column(j);
178            let mut acc = 0.0;
179            for i in 0..n {
180                let v = col[i];
181                acc += self.pieces.coeff_ll[i] * v * v;
182            }
183            diag[pt + j] = acc;
184        }
185        for j in 0..pw {
186            let col = self.pieces.b0.column(j);
187            let mut acc = 0.0;
188            for i in 0..n {
189                let v = col[i];
190                acc += self.pieces.coeff_ww[i] * v * v;
191            }
192            diag[pt + pls + j] = acc;
193        }
194        Ok(Some(diag))
195    }
196
197    fn directional_derivative(
198        &self,
199        d_beta_flat: &Array1<f64>,
200    ) -> Result<Option<Array2<f64>>, String> {
201        self.family
202            .exact_newton_joint_hessian_directional_derivative(&self.block_states, d_beta_flat)
203    }
204
205    fn directional_derivative_operator(
206        &self,
207        d_beta_flat: &Array1<f64>,
208    ) -> Result<Option<Arc<dyn gam_problem::HyperOperator>>, String> {
209        self.family.bls_wiggle_directional_operator(
210            &self.block_states,
211            self.x_t.clone(),
212            self.x_ls.clone(),
213            d_beta_flat,
214        )
215    }
216
217    fn second_directional_derivative(
218        &self,
219        d_beta_u_flat: &Array1<f64>,
220        d_beta_v_flat: &Array1<f64>,
221    ) -> Result<Option<Array2<f64>>, String> {
222        self.family
223            .exact_newton_joint_hessiansecond_directional_derivative(
224                &self.block_states,
225                d_beta_u_flat,
226                d_beta_v_flat,
227            )
228    }
229
230    fn second_directional_derivative_operator(
231        &self,
232        d_beta_u: &Array1<f64>,
233        d_beta_v: &Array1<f64>,
234    ) -> Result<Option<Arc<dyn gam_problem::HyperOperator>>, String> {
235        self.family.bls_wiggle_second_directional_operator(
236            &self.block_states,
237            self.x_t.clone(),
238            self.x_ls.clone(),
239            d_beta_u,
240            d_beta_v,
241        )
242    }
243}
244
245impl CustomFamilyGenerative for BinomialLocationScaleWiggleFamily {
246    fn generativespec(
247        &self,
248        block_states: &[ParameterBlockState],
249    ) -> Result<GenerativeSpec, String> {
250        validate_block_count::<GamlssError>(
251            "BinomialLocationScaleWiggleFamily",
252            3,
253            block_states.len(),
254        )?;
255        let eta_t = &block_states[Self::BLOCK_T].eta;
256        let eta_ls = &block_states[Self::BLOCK_LOG_SIGMA].eta;
257        let etaw = &block_states[Self::BLOCK_WIGGLE].eta;
258        if eta_t.len() != self.y.len() || eta_ls.len() != self.y.len() || etaw.len() != self.y.len()
259        {
260            return Err(GamlssError::DimensionMismatch {
261                reason: "BinomialLocationScaleWiggleFamily generative size mismatch".to_string(),
262            }
263            .into());
264        }
265        let mean = gamlss_rowwise_map_result(self.y.len(), |i| {
266            let sigma = exp_sigma_from_eta_scalar(eta_ls[i]);
267            let q0 = binomial_location_scale_q0(eta_t[i], sigma);
268            let jet = inverse_link_jet_for_inverse_link(&self.link_kind, q0 + etaw[i])
269                .map_err(|e| format!("location-scale inverse-link evaluation failed: {e}"))?;
270            Ok(jet.mu)
271        })?;
272        Ok(GenerativeSpec {
273            mean,
274            noise: NoiseModel::Bernoulli,
275        })
276    }
277}