1use 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 pub policy: gam_runtime::resource::ResourcePolicy,
19 pub frozen_warp_design: Option<Arc<Array2<f64>>>,
47}
48
49pub(crate) struct BinomialMeanWiggleGeometry {
50 pub(crate) basis: Array2<f64>,
51 pub(crate) basis_d1: Array2<f64>,
52 pub(crate) basis_d2: Array2<f64>,
53 pub(crate) basis_d3: Array2<f64>,
54 pub(crate) dq_dq0: Array1<f64>,
55 pub(crate) d2q_dq02: Array1<f64>,
56 pub(crate) d3q_dq03: Array1<f64>,
57 pub(crate) d4q_dq04: Array1<f64>,
58}
59
60pub(crate) struct BinomialMeanWiggleJointPsiDirection {
61 pub(crate) x_eta_psi: Option<Array2<f64>>,
62 pub(crate) z_eta_psi: Array1<f64>,
63}
64
65impl BinomialMeanWiggleFamily {
66 fn check_supplied_block_states(
76 &self,
77 block_states: &[ParameterBlockState],
78 ) -> Result<(), String> {
79 if block_states.is_empty() {
80 return Ok(());
81 }
82 validate_block_count::<String>("BinomialMeanWiggleFamily", 2, block_states.len())
83 }
84
85 pub const BLOCK_ETA: usize = 0;
86 pub const BLOCK_WIGGLE: usize = 1;
87
88 pub(crate) fn wiggle_basiswith_options(
89 &self,
90 q0: ArrayView1<'_, f64>,
91 options: BasisOptions,
92 ) -> Result<Array2<f64>, String> {
93 monotone_wiggle_basis_with_derivative_order(
94 q0,
95 &self.wiggle_knots,
96 self.wiggle_degree,
97 options.derivative_order,
98 )
99 }
100
101 pub(crate) fn wiggle_design(&self, q0: ArrayView1<'_, f64>) -> Result<Array2<f64>, String> {
102 self.wiggle_basiswith_options(q0, BasisOptions::value())
103 }
104
105 pub(crate) fn wiggle_dq_dq0(
106 &self,
107 q0: ArrayView1<'_, f64>,
108 beta_link_wiggle: ArrayView1<'_, f64>,
109 ) -> Result<Array1<f64>, String> {
110 let d_constrained = self.wiggle_basiswith_options(q0, BasisOptions::first_derivative())?;
111 if d_constrained.ncols() != beta_link_wiggle.len() {
112 return Err(GamlssError::DimensionMismatch { reason: format!(
113 "wiggle derivative/beta mismatch: basis has {} columns but beta_link_wiggle has {} coefficients",
114 d_constrained.ncols(),
115 beta_link_wiggle.len()
116 ) }.into());
117 }
118 Ok(d_constrained.dot(&beta_link_wiggle) + 1.0)
119 }
120
121 pub(crate) fn wiggle_d2q_dq02(
122 &self,
123 q0: ArrayView1<'_, f64>,
124 beta_link_wiggle: ArrayView1<'_, f64>,
125 ) -> Result<Array1<f64>, String> {
126 let d2 = self.wiggle_basiswith_options(q0, BasisOptions::second_derivative())?;
127 if d2.ncols() != beta_link_wiggle.len() {
128 return Err(GamlssError::DimensionMismatch { reason: format!(
129 "wiggle second-derivative/beta mismatch: basis has {} columns but beta_link_wiggle has {} coefficients",
130 d2.ncols(),
131 beta_link_wiggle.len()
132 ) }.into());
133 }
134 Ok(d2.dot(&beta_link_wiggle))
135 }
136
137 pub(crate) fn wiggle_d3basis_constrained(
138 &self,
139 q0: ArrayView1<'_, f64>,
140 ) -> Result<Array2<f64>, String> {
141 monotone_wiggle_basis_with_derivative_order(q0, &self.wiggle_knots, self.wiggle_degree, 3)
142 }
143
144 pub(crate) fn wiggle_d3q_dq03(
145 &self,
146 q0: ArrayView1<'_, f64>,
147 beta_link_wiggle: ArrayView1<'_, f64>,
148 ) -> Result<Array1<f64>, String> {
149 let d3 = self.wiggle_d3basis_constrained(q0)?;
150 if d3.ncols() != beta_link_wiggle.len() {
151 return Err(GamlssError::DimensionMismatch { reason: format!(
152 "wiggle third-derivative/beta mismatch: basis has {} columns but beta_link_wiggle has {} coefficients",
153 d3.ncols(),
154 beta_link_wiggle.len()
155 ) }.into());
156 }
157 Ok(d3.dot(&beta_link_wiggle))
158 }
159
160 pub(crate) fn wiggle_d4q_dq04(
161 &self,
162 q0: ArrayView1<'_, f64>,
163 beta_link_wiggle: ArrayView1<'_, f64>,
164 ) -> Result<Array1<f64>, String> {
165 let d4 = monotone_wiggle_basis_with_derivative_order(
166 q0,
167 &self.wiggle_knots,
168 self.wiggle_degree,
169 4,
170 )?;
171 if d4.ncols() != beta_link_wiggle.len() {
172 return Err(GamlssError::DimensionMismatch { reason: format!(
173 "wiggle fourth-derivative/beta mismatch: basis has {} columns but beta_link_wiggle has {} coefficients",
174 d4.ncols(),
175 beta_link_wiggle.len()
176 ) }.into());
177 }
178 Ok(d4.dot(&beta_link_wiggle))
179 }
180
181 pub(crate) fn wiggle_geometry(
182 &self,
183 q0: ArrayView1<'_, f64>,
184 beta_link_wiggle: ArrayView1<'_, f64>,
185 ) -> Result<BinomialMeanWiggleGeometry, String> {
186 if let Some(frozen) = self.frozen_warp_design.as_ref() {
194 let n = frozen.nrows();
195 let pw = frozen.ncols();
196 return Ok(BinomialMeanWiggleGeometry {
197 basis: frozen.as_ref().clone(),
198 basis_d1: Array2::zeros((n, pw)),
199 basis_d2: Array2::zeros((n, pw)),
200 basis_d3: Array2::zeros((n, pw)),
201 dq_dq0: Array1::ones(n),
202 d2q_dq02: Array1::zeros(n),
203 d3q_dq03: Array1::zeros(n),
204 d4q_dq04: Array1::zeros(n),
205 });
206 }
207 let basis = self.wiggle_design(q0)?;
208 let basis_d1 = self.wiggle_basiswith_options(q0, BasisOptions::first_derivative())?;
209 let basis_d2 = self.wiggle_basiswith_options(q0, BasisOptions::second_derivative())?;
210 let basis_d3 = self.wiggle_d3basis_constrained(q0)?;
211 let dq_dq0 = self.wiggle_dq_dq0(q0, beta_link_wiggle)?;
212 let d2q_dq02 = self.wiggle_d2q_dq02(q0, beta_link_wiggle)?;
213 let d3q_dq03 = self.wiggle_d3q_dq03(q0, beta_link_wiggle)?;
214 let d4q_dq04 = self.wiggle_d4q_dq04(q0, beta_link_wiggle)?;
215 Ok(BinomialMeanWiggleGeometry {
216 basis,
217 basis_d1,
218 basis_d2,
219 basis_d3,
220 dq_dq0,
221 d2q_dq02,
222 d3q_dq03,
223 d4q_dq04,
224 })
225 }
226
227 pub(crate) fn neglog_q_derivatives(
228 &self,
229 y: f64,
230 weight: f64,
231 q: f64,
232 ) -> Result<(f64, f64, f64), String> {
233 let jet = inverse_link_jet_for_inverse_link(&self.link_kind, q)
234 .map_err(|e| format!("fixed-link wiggle inverse-link evaluation failed: {e}"))?;
235 Ok(binomial_neglog_q_derivatives_dispatch(
239 y,
240 weight,
241 q,
242 jet.mu,
243 jet.d1,
244 jet.d2,
245 jet.d3,
246 &self.link_kind,
247 ))
248 }
249
250 pub(crate) fn neglog_q_fourth_derivative(
251 &self,
252 y: f64,
253 weight: f64,
254 q: f64,
255 ) -> Result<f64, String> {
256 let jet = inverse_link_jet_for_inverse_link(&self.link_kind, q)
257 .map_err(|e| format!("fixed-link wiggle inverse-link evaluation failed: {e}"))?;
258 binomial_neglog_q_fourth_derivative_dispatch(
260 y,
261 weight,
262 q,
263 jet.mu,
264 jet.d1,
265 jet.d2,
266 jet.d3,
267 &self.link_kind,
268 )
269 }
270
271 pub(crate) fn dense_eta_design_fromspecs<'a>(
272 &self,
273 specs: &'a [ParameterBlockSpec],
274 ) -> Result<Cow<'a, Array2<f64>>, String> {
275 if specs.len() != 2 {
276 return Err(GamlssError::DimensionMismatch {
277 reason: format!(
278 "BinomialMeanWiggleFamily expects 2 specs, got {}",
279 specs.len()
280 ),
281 }
282 .into());
283 }
284 Ok(match specs[Self::BLOCK_ETA].design.as_dense_ref() {
285 Some(d) => Cow::Borrowed(d),
286 None => Cow::Owned(
287 specs[Self::BLOCK_ETA]
288 .design
289 .try_to_dense_with_policy(
290 &self.policy.material_policy(),
291 "BinomialMeanWiggle dense_eta_design_fromspecs eta",
292 )
293 .map_err(|e| e.to_string())?
294 .as_ref()
295 .clone(),
296 ),
297 })
298 }
299
300 pub(crate) fn exact_newton_joint_psi_direction(
301 &self,
302 block_states: &[ParameterBlockState],
303 derivative_blocks: &[Vec<CustomFamilyBlockPsiDerivative>],
304 psi_index: usize,
305 x_eta: &Array2<f64>,
306 ) -> Result<Option<BinomialMeanWiggleJointPsiDirection>, String> {
307 validate_block_count::<GamlssError>("BinomialMeanWiggleFamily", 2, block_states.len())?;
308 if derivative_blocks.len() != 2 {
309 return Err(GamlssError::DimensionMismatch { reason: format!(
310 "BinomialMeanWiggleFamily joint psi direction expects 2 derivative block lists, got {}",
311 derivative_blocks.len()
312 ) }.into());
313 }
314 let n = self.y.len();
315 let p_eta = x_eta.ncols();
316 let beta_eta = &block_states[Self::BLOCK_ETA].beta;
317 let mut global = 0usize;
318 for (block_idx, block_derivs) in derivative_blocks.iter().enumerate() {
319 for deriv in block_derivs {
320 if global == psi_index {
321 if block_idx != Self::BLOCK_ETA {
322 return Ok(None);
323 }
324 let x_eta_psi_map = resolve_custom_family_x_psi_map(
325 deriv,
326 n,
327 p_eta,
328 0..n,
329 "BinomialMeanWiggleFamily eta",
330 &self.policy,
331 ).map_err(|error| error.to_string())?;
332 let x_eta_psi = x_eta_psi_map.row_chunk(0..n).map_err(|error| error.to_string())?;
333 let z_eta_psi = x_eta_psi.dot(beta_eta);
334 return Ok(Some(BinomialMeanWiggleJointPsiDirection {
335 x_eta_psi: Some(x_eta_psi),
336 z_eta_psi,
337 }));
338 }
339 global += 1;
340 }
341 }
342 Ok(None)
343 }
344
345 pub(crate) fn exact_newton_joint_psi_action(
346 &self,
347 block_states: &[ParameterBlockState],
348 derivative_blocks: &[Vec<CustomFamilyBlockPsiDerivative>],
349 psi_index: usize,
350 p_eta: usize,
351 ) -> Result<Option<(CustomFamilyPsiDesignAction, Array1<f64>)>, String> {
352 validate_block_count::<GamlssError>("BinomialMeanWiggleFamily", 2, block_states.len())?;
353 if derivative_blocks.len() != 2 {
354 return Err(GamlssError::DimensionMismatch { reason: format!(
355 "BinomialMeanWiggleFamily joint psi action expects 2 derivative block lists, got {}",
356 derivative_blocks.len()
357 ) }.into());
358 }
359 let n = self.y.len();
360 let beta_eta = &block_states[Self::BLOCK_ETA].beta;
361 let mut global = 0usize;
362 for (block_idx, block_derivs) in derivative_blocks.iter().enumerate() {
363 for deriv in block_derivs {
364 if global == psi_index {
365 if block_idx != Self::BLOCK_ETA {
366 return Ok(None);
367 }
368 let action = match CustomFamilyPsiDesignAction::from_first_derivative(
369 deriv,
370 n,
371 p_eta,
372 0..n,
373 "BinomialMeanWiggleFamily eta",
374 ) {
375 Ok(action) => action,
376 Err(_) => return Ok(None),
377 };
378 let z_eta_psi = action.forward_mul(beta_eta.view());
379 return Ok(Some((action, z_eta_psi)));
380 }
381 global += 1;
382 }
383 }
384 Ok(None)
385 }
386
387 pub(crate) fn bmw_static_hessian_operator(
388 &self,
389 block_states: &[ParameterBlockState],
390 x_eta_arc: Arc<Array2<f64>>,
391 ) -> Result<Arc<RowCoeffOperator>, String> {
392 validate_block_count::<GamlssError>("BinomialMeanWiggleFamily", 2, block_states.len())?;
393 let eta = &block_states[Self::BLOCK_ETA].eta;
394 let etaw = &block_states[Self::BLOCK_WIGGLE].eta;
395 let betaw = &block_states[Self::BLOCK_WIGGLE].beta;
396 let n = self.y.len();
397 if eta.len() != n || etaw.len() != n || self.weights.len() != n {
398 return Err(GamlssError::DimensionMismatch {
399 reason: "BinomialMeanWiggleFamily input size mismatch".to_string(),
400 }
401 .into());
402 }
403 let geom = self.wiggle_geometry(eta.view(), betaw.view())?;
404 let p_eta = x_eta_arc.ncols();
405 let pw = geom.basis.ncols();
406 let mut coeff_eta = Array1::<f64>::zeros(n);
407 let mut coeff_etaw_b = Array1::<f64>::zeros(n);
408 let mut coeff_etaw_d1 = Array1::<f64>::zeros(n);
409 let mut coeff_ww = Array1::<f64>::zeros(n);
410 for row in 0..n {
411 let q = eta[row] + etaw[row];
412 let (m1, m2, _) = self.neglog_q_derivatives(self.y[row], self.weights[row], q)?;
413 let a = geom.dq_dq0[row];
414 let b = geom.d2q_dq02[row];
415 coeff_eta[row] = hessian_coeff_fromobjective_q_terms(m1, m2, a, a, b);
416 coeff_etaw_b[row] = m2 * a;
417 coeff_etaw_d1[row] = m1;
418 coeff_ww[row] = m2;
419 }
420 Ok(Arc::new(RowCoeffOperator::from_directions(
421 vec![p_eta, pw],
422 vec![
423 (0, x_eta_arc),
424 (1, Arc::new(geom.basis)),
425 (1, Arc::new(geom.basis_d1)),
426 ],
427 vec![
428 (0, 0, coeff_eta),
429 (0, 1, coeff_etaw_b),
430 (0, 2, coeff_etaw_d1),
431 (1, 1, coeff_ww),
432 ],
433 n,
434 )))
435 }
436
437 pub(crate) fn bmw_directional_operator(
438 &self,
439 block_states: &[ParameterBlockState],
440 x_eta_arc: Arc<Array2<f64>>,
441 d_beta_flat: &Array1<f64>,
442 ) -> Result<Option<Arc<dyn gam_problem::HyperOperator>>, String> {
443 validate_block_count::<GamlssError>("BinomialMeanWiggleFamily", 2, block_states.len())?;
444 let eta = &block_states[Self::BLOCK_ETA].eta;
445 let etaw = &block_states[Self::BLOCK_WIGGLE].eta;
446 let betaw = &block_states[Self::BLOCK_WIGGLE].beta;
447 let n = self.y.len();
448 if eta.len() != n || etaw.len() != n || self.weights.len() != n {
449 return Err(GamlssError::DimensionMismatch {
450 reason: "BinomialMeanWiggleFamily input size mismatch".to_string(),
451 }
452 .into());
453 }
454 let geom = self.wiggle_geometry(eta.view(), betaw.view())?;
455 let p_eta = x_eta_arc.ncols();
456 let pw = geom.basis.ncols();
457 let total = p_eta + pw;
458 if d_beta_flat.len() != total {
459 return Err(GamlssError::DimensionMismatch {
460 reason: format!(
461 "BinomialMeanWiggleFamily joint d_beta length mismatch: got {}, expected {}",
462 d_beta_flat.len(),
463 total
464 ),
465 }
466 .into());
467 }
468 let u_eta = d_beta_flat.slice(s![0..p_eta]).to_owned();
469 let uw = d_beta_flat.slice(s![p_eta..total]).to_owned();
470 let xi = fast_av(x_eta_arc.as_ref(), &u_eta);
471 let phi = fast_av(&geom.basis, &uw);
472 let basis1_u = fast_av(&geom.basis_d1, &uw);
473 let basis2_u = fast_av(&geom.basis_d2, &uw);
474
475 let mut coeff_eta = Array1::<f64>::zeros(n);
476 let mut coeff_etaw_b = Array1::<f64>::zeros(n);
477 let mut coeff_etaw_d1 = Array1::<f64>::zeros(n);
478 let mut coeff_etaw_d2 = Array1::<f64>::zeros(n);
479 let mut coeff_ww_bb = Array1::<f64>::zeros(n);
480 let mut coeff_ww_db = Array1::<f64>::zeros(n);
481 for row in 0..n {
482 let q = eta[row] + etaw[row];
483 let (m1, m2, m3) = self.neglog_q_derivatives(self.y[row], self.weights[row], q)?;
484 let a = geom.dq_dq0[row];
485 let b = geom.d2q_dq02[row];
486 let c = geom.d3q_dq03[row];
487 let q_u = a * xi[row] + phi[row];
488 let a_u = b * xi[row] + basis1_u[row];
489 let b_u = c * xi[row] + basis2_u[row];
490 let [eta_eta, eta_w_b, eta_w_d1, eta_w_d2, ww_bb, ww_db] =
491 mean_wiggle_directional_coefficients(
492 [m1, m2, m3],
493 q_u,
494 a,
495 b,
496 a_u,
497 b_u,
498 xi[row],
499 );
500 coeff_eta[row] = eta_eta;
501 coeff_etaw_b[row] = eta_w_b;
502 coeff_etaw_d1[row] = eta_w_d1;
503 coeff_etaw_d2[row] = eta_w_d2;
504 coeff_ww_bb[row] = ww_bb;
505 coeff_ww_db[row] = ww_db;
506 }
507 Ok(Some(Arc::new(RowCoeffOperator::from_directions(
508 vec![p_eta, pw],
509 vec![
510 (0, x_eta_arc),
511 (1, Arc::new(geom.basis)),
512 (1, Arc::new(geom.basis_d1)),
513 (1, Arc::new(geom.basis_d2)),
514 ],
515 vec![
516 (0, 0, coeff_eta),
517 (0, 1, coeff_etaw_b),
518 (0, 2, coeff_etaw_d1),
519 (0, 3, coeff_etaw_d2),
520 (1, 1, coeff_ww_bb),
521 (1, 2, coeff_ww_db),
522 ],
523 n,
524 ))))
525 }
526
527 pub(crate) fn bmw_second_directional_operator(
528 &self,
529 block_states: &[ParameterBlockState],
530 x_eta_arc: Arc<Array2<f64>>,
531 d_beta_u_flat: &Array1<f64>,
532 d_beta_v_flat: &Array1<f64>,
533 ) -> Result<Option<Arc<dyn gam_problem::HyperOperator>>, String> {
534 validate_block_count::<GamlssError>("BinomialMeanWiggleFamily", 2, block_states.len())?;
535 let eta = &block_states[Self::BLOCK_ETA].eta;
536 let etaw = &block_states[Self::BLOCK_WIGGLE].eta;
537 let betaw = &block_states[Self::BLOCK_WIGGLE].beta;
538 let n = self.y.len();
539 if eta.len() != n || etaw.len() != n || self.weights.len() != n {
540 return Err(GamlssError::DimensionMismatch {
541 reason: "BinomialMeanWiggleFamily input size mismatch".to_string(),
542 }
543 .into());
544 }
545 let geom = self.wiggle_geometry(eta.view(), betaw.view())?;
546 let p_eta = x_eta_arc.ncols();
547 let pw = geom.basis.ncols();
548 let total = p_eta + pw;
549 if d_beta_u_flat.len() != total || d_beta_v_flat.len() != total {
550 return Err(GamlssError::DimensionMismatch { reason: format!(
551 "BinomialMeanWiggleFamily joint second d_beta length mismatch: got {} and {}, expected {}",
552 d_beta_u_flat.len(),
553 d_beta_v_flat.len(),
554 total
555 ) }.into());
556 }
557 let u_eta = d_beta_u_flat.slice(s![0..p_eta]).to_owned();
558 let v_eta = d_beta_v_flat.slice(s![0..p_eta]).to_owned();
559 let uw = d_beta_u_flat.slice(s![p_eta..total]).to_owned();
560 let vw = d_beta_v_flat.slice(s![p_eta..total]).to_owned();
561
562 let xi_u = fast_av(x_eta_arc.as_ref(), &u_eta);
563 let xi_v = fast_av(x_eta_arc.as_ref(), &v_eta);
564 let phi_u = fast_av(&geom.basis, &uw);
565 let phi_v = fast_av(&geom.basis, &vw);
566 let b1u = fast_av(&geom.basis_d1, &uw);
567 let b1v = fast_av(&geom.basis_d1, &vw);
568 let b2u = fast_av(&geom.basis_d2, &uw);
569 let b2v = fast_av(&geom.basis_d2, &vw);
570 let b3u = fast_av(&geom.basis_d3, &uw);
571 let b3v = fast_av(&geom.basis_d3, &vw);
572
573 let mut coeff_eta = Array1::<f64>::zeros(n);
574 let mut coeff_etaw_b = Array1::<f64>::zeros(n);
575 let mut coeff_etaw_d1 = Array1::<f64>::zeros(n);
576 let mut coeff_etaw_d2 = Array1::<f64>::zeros(n);
577 let mut coeff_etaw_d3 = Array1::<f64>::zeros(n);
578 let mut coeff_ww_bb = Array1::<f64>::zeros(n);
579 let mut coeff_ww_db = Array1::<f64>::zeros(n);
580 let mut coeff_ww_ddb = Array1::<f64>::zeros(n);
581 let mut coeff_ww_dd = Array1::<f64>::zeros(n);
582
583 for row in 0..n {
584 let q = eta[row] + etaw[row];
585 let (m1, m2, m3) = self.neglog_q_derivatives(self.y[row], self.weights[row], q)?;
586 let m4 = self.neglog_q_fourth_derivative(self.y[row], self.weights[row], q)?;
587 let a = geom.dq_dq0[row];
588 let b = geom.d2q_dq02[row];
589 let c = geom.d3q_dq03[row];
590 let d = geom.d4q_dq04[row];
591
592 let q_u = a * xi_u[row] + phi_u[row];
593 let a_u = b * xi_u[row] + b1u[row];
594 let b_u = c * xi_u[row] + b2u[row];
595 let q_v = a * xi_v[row] + phi_v[row];
596 let a_v = b * xi_v[row] + b1v[row];
597 let b_v = c * xi_v[row] + b2v[row];
598 let q_uv = b * xi_u[row] * xi_v[row] + b1u[row] * xi_v[row] + b1v[row] * xi_u[row];
599 let a_uv = c * xi_u[row] * xi_v[row] + b2u[row] * xi_v[row] + b2v[row] * xi_u[row];
600 let b_uv = d * xi_u[row] * xi_v[row] + b3u[row] * xi_v[row] + b3v[row] * xi_u[row];
601
602 let [
603 eta_eta,
604 eta_w_b,
605 eta_w_d1,
606 eta_w_d2,
607 eta_w_d3,
608 ww_bb,
609 ww_db,
610 ww_ddb,
611 ww_dd,
612 ] = mean_wiggle_second_directional_coefficients(
613 [m1, m2, m3, m4],
614 [q_u, q_v, q_uv],
615 [a, a_u, a_v, a_uv],
616 [b, b_u, b_v, b_uv],
617 [xi_u[row], xi_v[row]],
618 );
619 coeff_eta[row] = eta_eta;
620 coeff_etaw_b[row] = eta_w_b;
621 coeff_etaw_d1[row] = eta_w_d1;
622 coeff_etaw_d2[row] = eta_w_d2;
623 coeff_etaw_d3[row] = eta_w_d3;
624 coeff_ww_bb[row] = ww_bb;
625 coeff_ww_db[row] = ww_db;
626 coeff_ww_ddb[row] = ww_ddb;
627 coeff_ww_dd[row] = ww_dd;
628 }
629
630 Ok(Some(Arc::new(RowCoeffOperator::from_directions(
631 vec![p_eta, pw],
632 vec![
633 (0, x_eta_arc),
634 (1, Arc::new(geom.basis)),
635 (1, Arc::new(geom.basis_d1)),
636 (1, Arc::new(geom.basis_d2)),
637 (1, Arc::new(geom.basis_d3)),
638 ],
639 vec![
640 (0, 0, coeff_eta),
641 (0, 1, coeff_etaw_b),
642 (0, 2, coeff_etaw_d1),
643 (0, 3, coeff_etaw_d2),
644 (0, 4, coeff_etaw_d3),
645 (1, 1, coeff_ww_bb),
646 (1, 2, coeff_ww_db),
647 (1, 3, coeff_ww_ddb),
648 (2, 2, coeff_ww_dd),
649 ],
650 n,
651 ))))
652 }
653
654 pub fn block_effective_jacobian(
660 specs: &[ParameterBlockSpec],
661 block_idx: usize,
662 ) -> Result<Box<dyn BlockEffectiveJacobian>, String> {
663 crate::block_layout::block_jacobian::AdditiveWiggleBlockLayout {
664 family: "BinomialMeanWiggleFamily",
665 n_outputs: 1,
666 additive_blocks: &[Self::BLOCK_ETA],
667 wiggle_block: Some(Self::BLOCK_WIGGLE),
668 }
669 .block_effective_jacobian(specs, block_idx)
670 }
671}
672
673impl CustomFamily for BinomialMeanWiggleFamily {
674 fn exact_newton_joint_hessian_beta_dependent(&self) -> bool {
675 true
676 }
677
678 fn joint_jeffreys_term_required(&self) -> bool {
716 false
717 }
718
719 fn coefficient_hessian_cost(&self, specs: &[ParameterBlockSpec]) -> u64 {
720 let p_total = specs
724 .iter()
725 .map(|s| s.design.ncols() as u64)
726 .fold(0u64, |acc, p| acc.saturating_add(p));
727 (self.y.len() as u64).saturating_mul(p_total.max(1))
728 }
729
730 fn block_linear_constraints(
731 &self,
732 block_states: &[ParameterBlockState],
733 block_idx: usize,
734 spec: &ParameterBlockSpec,
735 ) -> Result<Option<ConstraintSet>, String> {
736 self.check_supplied_block_states(block_states)?;
737 if block_idx != Self::BLOCK_WIGGLE {
738 return Ok(None);
739 }
740 Ok(monotone_wiggle_nonnegative_constraints(spec.design.ncols()))
745 }
746
747 fn post_update_block_beta(
748 &self,
749 block_states: &[ParameterBlockState],
750 block_idx: usize,
751 block_spec: &ParameterBlockSpec,
752 beta: Array1<f64>,
753 ) -> Result<Array1<f64>, String> {
754 self.check_supplied_block_states(block_states)?;
755 assert!(!block_spec.name.is_empty());
756 if block_idx != Self::BLOCK_WIGGLE {
757 return Ok(beta);
758 }
759 let beta = project_monotone_wiggle_beta_nonnegative(beta);
760 validate_monotone_wiggle_beta_nonnegative(&beta, "BinomialMeanWiggleFamily post-update")?;
761 Ok(beta)
762 }
763
764 fn evaluate(&self, block_states: &[ParameterBlockState]) -> Result<FamilyEvaluation, String> {
765 validate_block_count::<GamlssError>("BinomialMeanWiggleFamily", 2, block_states.len())?;
766 let eta = &block_states[Self::BLOCK_ETA].eta;
767 let etaw = &block_states[Self::BLOCK_WIGGLE].eta;
768 let betaw = &block_states[Self::BLOCK_WIGGLE].beta;
769 let n = self.y.len();
770 if eta.len() != n || etaw.len() != n || self.weights.len() != n {
771 return Err(GamlssError::DimensionMismatch {
772 reason: "BinomialMeanWiggleFamily input size mismatch".to_string(),
773 }
774 .into());
775 }
776 let dq_dq0 = if self.frozen_warp_design.is_some() {
780 Array1::<f64>::ones(n)
781 } else {
782 self.wiggle_dq_dq0(eta.view(), betaw.view())?
783 };
784 if dq_dq0.len() != n {
785 return Err(GamlssError::DimensionMismatch {
786 reason: format!(
787 "BinomialMeanWiggleFamily dq/dq0 length mismatch: got {}, expected {}",
788 dq_dq0.len(),
789 n
790 ),
791 }
792 .into());
793 }
794
795 let mut rows = Vec::with_capacity(n);
800 for i in 0..n {
801 let q = eta[i] + etaw[i];
802 if !eta[i].is_finite() || !etaw[i].is_finite() || !q.is_finite() {
803 return Err(GamlssError::RowGeometryUnrepresentable {
804 row: i,
805 quantity: "binomial mean-wiggle predictor q",
806 eta: eta[i],
807 value: q,
808 }
809 .into());
810 }
811 let yi = self.y[i];
812 let wi = self.weights[i];
813 if !yi.is_finite() || !(0.0..=1.0).contains(&yi) {
814 return Err(GamlssError::InvalidInput {
815 reason: format!(
816 "BinomialMeanWiggleFamily requires y in [0, 1]; found y[{i}]={yi}"
817 ),
818 }
819 .into());
820 }
821 if !wi.is_finite() || wi < 0.0 {
822 return Err(GamlssError::InvalidInput {
823 reason: format!(
824 "BinomialMeanWiggleFamily requires finite non-negative weights; found weight[{i}]={wi}"
825 ),
826 }
827 .into());
828 }
829 let slope = dq_dq0[i];
830 if !slope.is_finite() {
831 return Err(GamlssError::RowGeometryUnrepresentable {
832 row: i,
833 quantity: "binomial mean-wiggle warp slope",
834 eta: eta[i],
835 value: slope,
836 }
837 .into());
838 }
839 if wi == 0.0 {
840 rows.push((0.0, eta[i], 0.0, etaw[i], 0.0));
841 continue;
842 }
843 let jet = inverse_link_jet_for_inverse_link(&self.link_kind, q)
844 .map_err(|e| format!("fixed-link wiggle inverse-link evaluation failed: {e}"))?;
845 let row_ll =
846 binomial_location_scale_log_likelihood(yi, wi, q, &self.link_kind, jet.mu)?;
847 let (m1, _, _) = binomial_neglog_q_derivatives_dispatch(
868 yi,
869 wi,
870 q,
871 jet.mu,
872 jet.d1,
873 jet.d2,
874 jet.d3,
875 &self.link_kind,
876 );
877 let (fisher, _, _) =
878 binomial_expected_q_information_derivatives(wi, jet.mu, jet.d1, jet.d2, jet.d3);
879 for (quantity, value, nonnegative) in [
880 ("binomial mean-wiggle row log likelihood", row_ll, false),
881 ("binomial mean-wiggle q score", m1, false),
882 ("binomial mean-wiggle expected q information", fisher, true),
883 ] {
884 if !value.is_finite() || (nonnegative && value < 0.0) {
885 return Err(GamlssError::RowGeometryUnrepresentable {
886 row: i,
887 quantity,
888 eta: q,
889 value,
890 }
891 .into());
892 }
893 }
894 let (z_eta_i, w_eta_i) = if slope == 0.0 || fisher == 0.0 {
897 (eta[i], 0.0)
898 } else {
899 let weight = fisher * slope * slope;
900 if !weight.is_finite() || weight <= 0.0 {
901 return Err(GamlssError::RowGeometryUnrepresentable {
902 row: i,
903 quantity: "binomial mean-wiggle eta working weight",
904 eta: eta[i],
905 value: weight,
906 }
907 .into());
908 }
909 let response = eta[i] - m1 / (fisher * slope);
910 if !response.is_finite() {
911 return Err(GamlssError::RowGeometryUnrepresentable {
912 row: i,
913 quantity: "binomial mean-wiggle eta working response",
914 eta: eta[i],
915 value: response,
916 }
917 .into());
918 }
919 (response, weight)
920 };
921 let (z_wiggle_i, w_wiggle_i) = if fisher == 0.0 {
924 (etaw[i], 0.0)
925 } else {
926 let z_wiggle_i = etaw[i] - m1 / fisher;
927 if !z_wiggle_i.is_finite() {
928 return Err(GamlssError::RowGeometryUnrepresentable {
929 row: i,
930 quantity: "binomial mean-wiggle wiggle working response",
931 eta: etaw[i],
932 value: z_wiggle_i,
933 }
934 .into());
935 }
936 (z_wiggle_i, fisher)
937 };
938 rows.push((row_ll, z_eta_i, w_eta_i, z_wiggle_i, w_wiggle_i));
939 }
940
941 let mut ll = 0.0;
942 for (i, row) in rows.iter().enumerate() {
943 ll += row.0;
944 if !ll.is_finite() {
945 return Err(GamlssError::RowGeometryUnrepresentable {
946 row: i,
947 quantity: "binomial mean-wiggle cumulative log likelihood",
948 eta: eta[i],
949 value: ll,
950 }
951 .into());
952 }
953 }
954 let z_eta = Array1::from_iter(rows.iter().map(|row| row.1));
955 let w_eta = Array1::from_iter(rows.iter().map(|row| row.2));
956 let z_wiggle = Array1::from_iter(rows.iter().map(|row| row.3));
957 let w_wiggle = Array1::from_iter(rows.iter().map(|row| row.4));
958
959 Ok(FamilyEvaluation {
960 log_likelihood: ll,
961 blockworking_sets: vec![
962 BlockWorkingSet::diagonal_checked(z_eta, w_eta)?,
963 BlockWorkingSet::diagonal_checked(z_wiggle, w_wiggle)?,
964 ],
965 })
966 }
967
968 fn exact_newton_joint_gradient_evaluation(
969 &self,
970 block_states: &[ParameterBlockState],
971 specs: &[ParameterBlockSpec],
972 ) -> Result<Option<ExactNewtonJointGradientEvaluation>, String> {
973 let eval = self.evaluate(block_states)?;
978 gamlss_joint_gradient_from_working_sets(&eval, specs, block_states).map(Some)
979 }
980
981 fn block_geometry(
982 &self,
983 block_states: &[ParameterBlockState],
984 spec: &ParameterBlockSpec,
985 ) -> Result<(DesignMatrix, Array1<f64>), String> {
986 if spec.name != "wiggle" {
987 return Ok((spec.design.clone(), spec.offset.clone()));
988 }
989 if block_states.is_empty() {
990 return Err(GamlssError::UnsupportedConfiguration {
991 reason: "wiggle geometry requires eta block".to_string(),
992 }
993 .into());
994 }
995 let eta = &block_states[Self::BLOCK_ETA].eta;
996 if eta.len() != self.y.len() {
997 return Err(GamlssError::DimensionMismatch {
998 reason: "BinomialMeanWiggleFamily eta size mismatch".to_string(),
999 }
1000 .into());
1001 }
1002 let x = match self.frozen_warp_design.as_ref() {
1008 Some(frozen) => frozen.as_ref().clone(),
1009 None => self.wiggle_design(eta.view())?,
1010 };
1011 if x.ncols() != spec.design.ncols() {
1012 return Err(GamlssError::DimensionMismatch {
1013 reason: format!(
1014 "dynamic wiggle design col mismatch: got {}, expected {}",
1015 x.ncols(),
1016 spec.design.ncols()
1017 ),
1018 }
1019 .into());
1020 }
1021 let nrows = x.nrows();
1022 Ok((
1023 DesignMatrix::Dense(gam_linalg::matrix::DenseDesignMatrix::from(x)),
1024 Array1::zeros(nrows),
1025 ))
1026 }
1027
1028 fn block_geometry_is_dynamic(&self) -> bool {
1029 true
1030 }
1031
1032 fn exact_newton_joint_hessian_workspace(
1033 &self,
1034 block_states: &[ParameterBlockState],
1035 specs: &[ParameterBlockSpec],
1036 ) -> Result<Option<Arc<dyn ExactNewtonJointHessianWorkspace>>, String> {
1037 let x_eta = self.dense_eta_design_fromspecs(specs)?.into_owned();
1038 let workspace = BinomialMeanWiggleHessianWorkspace::new(
1039 self.clone(),
1040 block_states.to_vec(),
1041 specs.to_vec(),
1042 x_eta,
1043 )?;
1044 Ok(Some(Arc::new(workspace)))
1045 }
1046
1047 fn inner_coefficient_hessian_hvp_available(&self, specs: &[ParameterBlockSpec]) -> bool {
1048 self.dense_eta_design_fromspecs(specs).is_ok()
1049 }
1050
1051 fn exact_newton_joint_hessian_with_specs(
1052 &self,
1053 block_states: &[ParameterBlockState],
1054 specs: &[ParameterBlockSpec],
1055 ) -> Result<Option<Array2<f64>>, String> {
1056 validate_block_count::<GamlssError>("BinomialMeanWiggleFamily", 2, block_states.len())?;
1057 let x_eta = self.dense_eta_design_fromspecs(specs)?;
1058 let eta = &block_states[Self::BLOCK_ETA].eta;
1059 let etaw = &block_states[Self::BLOCK_WIGGLE].eta;
1060 let betaw = &block_states[Self::BLOCK_WIGGLE].beta;
1061 let n = self.y.len();
1062 if eta.len() != n || etaw.len() != n || self.weights.len() != n {
1063 return Err(GamlssError::DimensionMismatch {
1064 reason: "BinomialMeanWiggleFamily input size mismatch".to_string(),
1065 }
1066 .into());
1067 }
1068 let geom = self.wiggle_geometry(eta.view(), betaw.view())?;
1069 let p_eta = x_eta.ncols();
1070 let pw = geom.basis.ncols();
1071 let mut coeff_eta = Array1::<f64>::zeros(n);
1072 let mut coeff_etaw_b = Array1::<f64>::zeros(n);
1073 let mut coeff_etaw_d1 = Array1::<f64>::zeros(n);
1074 let mut coeff_ww = Array1::<f64>::zeros(n);
1075 for row in 0..n {
1076 let q = eta[row] + etaw[row];
1077 let (m1, m2, _) = self.neglog_q_derivatives(self.y[row], self.weights[row], q)?;
1078 let a = geom.dq_dq0[row];
1079 let b = geom.d2q_dq02[row];
1080 coeff_eta[row] = hessian_coeff_fromobjective_q_terms(m1, m2, a, a, b);
1081 coeff_etaw_b[row] = m2 * a;
1082 coeff_etaw_d1[row] = m1;
1083 coeff_ww[row] = m2;
1084 }
1085 let h_eta_eta = xt_diag_x_dense(&x_eta, &coeff_eta)?;
1086 let h_eta_w = xt_diag_y_dense(&x_eta, &coeff_etaw_b, &geom.basis)?
1087 + &xt_diag_y_dense(&x_eta, &coeff_etaw_d1, &geom.basis_d1)?;
1088 let h_ww = xt_diag_x_dense(&geom.basis, &coeff_ww)?;
1089 assert_eq!(h_eta_eta.nrows(), p_eta);
1090 assert_eq!(h_ww.nrows(), pw);
1091 Ok(Some(binomial_pack_mean_wiggle_joint_symmetrichessian(
1092 &h_eta_eta, &h_eta_w, &h_ww,
1093 )))
1094 }
1095
1096 fn exact_newton_joint_hessian_directional_derivative_with_specs(
1097 &self,
1098 block_states: &[ParameterBlockState],
1099 specs: &[ParameterBlockSpec],
1100 d_beta_flat: &Array1<f64>,
1101 ) -> Result<Option<Array2<f64>>, String> {
1102 validate_block_count::<GamlssError>("BinomialMeanWiggleFamily", 2, block_states.len())?;
1103 let x_eta = self.dense_eta_design_fromspecs(specs)?;
1104 let eta = &block_states[Self::BLOCK_ETA].eta;
1105 let etaw = &block_states[Self::BLOCK_WIGGLE].eta;
1106 let betaw = &block_states[Self::BLOCK_WIGGLE].beta;
1107 let n = self.y.len();
1108 if eta.len() != n || etaw.len() != n || self.weights.len() != n {
1109 return Err(GamlssError::DimensionMismatch {
1110 reason: "BinomialMeanWiggleFamily input size mismatch".to_string(),
1111 }
1112 .into());
1113 }
1114 let geom = self.wiggle_geometry(eta.view(), betaw.view())?;
1115 let p_eta = x_eta.ncols();
1116 let pw = geom.basis.ncols();
1117 if d_beta_flat.len() != p_eta + pw {
1118 return Err(GamlssError::DimensionMismatch {
1119 reason: format!(
1120 "BinomialMeanWiggleFamily joint d_beta length mismatch: got {}, expected {}",
1121 d_beta_flat.len(),
1122 p_eta + pw
1123 ),
1124 }
1125 .into());
1126 }
1127 let u_eta = d_beta_flat.slice(s![0..p_eta]).to_owned();
1128 let uw = d_beta_flat.slice(s![p_eta..p_eta + pw]).to_owned();
1129 let xi = x_eta.dot(&u_eta);
1130 let phi = geom.basis.dot(&uw);
1131 let basis1_u = geom.basis_d1.dot(&uw);
1132 let basis2_u = geom.basis_d2.dot(&uw);
1133
1134 let mut coeff_eta = Array1::<f64>::zeros(n);
1135 let mut coeff_etaw_b = Array1::<f64>::zeros(n);
1136 let mut coeff_etaw_d1 = Array1::<f64>::zeros(n);
1137 let mut coeff_etaw_d2 = Array1::<f64>::zeros(n);
1138 let mut coeff_ww_bb = Array1::<f64>::zeros(n);
1139 let mut coeff_ww_db = Array1::<f64>::zeros(n);
1140 for row in 0..n {
1141 let q = eta[row] + etaw[row];
1142 let (m1, m2, m3) = self.neglog_q_derivatives(self.y[row], self.weights[row], q)?;
1143 let a = geom.dq_dq0[row];
1144 let b = geom.d2q_dq02[row];
1145 let c = geom.d3q_dq03[row];
1146 let q_u = a * xi[row] + phi[row];
1147 let a_u = b * xi[row] + basis1_u[row];
1148 let b_u = c * xi[row] + basis2_u[row];
1149 let [eta_eta, eta_w_b, eta_w_d1, eta_w_d2, ww_bb, ww_db] =
1150 mean_wiggle_directional_coefficients(
1151 [m1, m2, m3],
1152 q_u,
1153 a,
1154 b,
1155 a_u,
1156 b_u,
1157 xi[row],
1158 );
1159 coeff_eta[row] = eta_eta;
1160 coeff_etaw_b[row] = eta_w_b;
1161 coeff_etaw_d1[row] = eta_w_d1;
1162 coeff_etaw_d2[row] = eta_w_d2;
1163 coeff_ww_bb[row] = ww_bb;
1164 coeff_ww_db[row] = ww_db;
1165 }
1166
1167 let d_h_eta_eta = xt_diag_x_dense(&x_eta, &coeff_eta)?;
1168 let d_h_eta_w = xt_diag_y_dense(&x_eta, &coeff_etaw_b, &geom.basis)?
1169 + &xt_diag_y_dense(&x_eta, &coeff_etaw_d1, &geom.basis_d1)?
1170 + &xt_diag_y_dense(&x_eta, &coeff_etaw_d2, &geom.basis_d2)?;
1171 let a_ww = xt_diag_y_dense(&geom.basis_d1, &coeff_ww_db, &geom.basis)?;
1172 let d_h_ww = xt_diag_x_dense(&geom.basis, &coeff_ww_bb)? + &a_ww + a_ww.t();
1173 Ok(Some(binomial_pack_mean_wiggle_joint_symmetrichessian(
1174 &d_h_eta_eta,
1175 &d_h_eta_w,
1176 &d_h_ww,
1177 )))
1178 }
1179
1180 fn exact_newton_joint_hessian_second_directional_derivative_with_specs(
1239 &self,
1240 block_states: &[ParameterBlockState],
1241 specs: &[ParameterBlockSpec],
1242 d_beta_u_flat: &Array1<f64>,
1243 d_beta_v_flat: &Array1<f64>,
1244 ) -> Result<Option<Array2<f64>>, String> {
1245 validate_block_count::<GamlssError>("BinomialMeanWiggleFamily", 2, block_states.len())?;
1246 let x_eta = self.dense_eta_design_fromspecs(specs)?;
1247 let eta = &block_states[Self::BLOCK_ETA].eta;
1248 let etaw = &block_states[Self::BLOCK_WIGGLE].eta;
1249 let betaw = &block_states[Self::BLOCK_WIGGLE].beta;
1250 let n = self.y.len();
1251 if eta.len() != n || etaw.len() != n || self.weights.len() != n {
1252 return Err(GamlssError::DimensionMismatch {
1253 reason: "BinomialMeanWiggleFamily input size mismatch".to_string(),
1254 }
1255 .into());
1256 }
1257 let geom = self.wiggle_geometry(eta.view(), betaw.view())?;
1258 let p_eta = x_eta.ncols();
1259 let pw = geom.basis.ncols();
1260 let total = p_eta + pw;
1261 if d_beta_u_flat.len() != total || d_beta_v_flat.len() != total {
1262 return Err(GamlssError::DimensionMismatch { reason: format!(
1263 "BinomialMeanWiggleFamily joint second d_beta length mismatch: got {} and {}, expected {}",
1264 d_beta_u_flat.len(),
1265 d_beta_v_flat.len(),
1266 total
1267 ) }.into());
1268 }
1269
1270 let u_eta = d_beta_u_flat.slice(s![0..p_eta]).to_owned();
1272 let v_eta = d_beta_v_flat.slice(s![0..p_eta]).to_owned();
1273 let uw = d_beta_u_flat.slice(s![p_eta..total]).to_owned();
1274 let vw = d_beta_v_flat.slice(s![p_eta..total]).to_owned();
1275
1276 let xi_u = x_eta.dot(&u_eta); let xi_v = x_eta.dot(&v_eta); let phi_u = geom.basis.dot(&uw); let phi_v = geom.basis.dot(&vw); let b1u = geom.basis_d1.dot(&uw); let b1v = geom.basis_d1.dot(&vw);
1283 let b2u = geom.basis_d2.dot(&uw); let b2v = geom.basis_d2.dot(&vw);
1285 let b3u = geom.basis_d3.dot(&uw); let b3v = geom.basis_d3.dot(&vw);
1287
1288 let basis_u = scale_matrix_rows(&geom.basis_d1, &xi_u)?; let basis_v = scale_matrix_rows(&geom.basis_d1, &xi_v)?; let basis_uv = scale_matrix_rows(&geom.basis_d2, &(&xi_u * &xi_v))?; let mut coeff_eta = Array1::<f64>::zeros(n);
1308
1309 let mut coeff_etaw_b = Array1::<f64>::zeros(n);
1344 let mut coeff_etaw_d1 = Array1::<f64>::zeros(n);
1345 let mut coeff_etaw_d2 = Array1::<f64>::zeros(n);
1346 let mut coeff_etaw_d3 = Array1::<f64>::zeros(n);
1347
1348 let mut dw = Array1::<f64>::zeros(n);
1373 let mut dw_u = Array1::<f64>::zeros(n);
1374 let mut dw_v = Array1::<f64>::zeros(n);
1375 let mut dw_uv = Array1::<f64>::zeros(n);
1376
1377 for row in 0..n {
1378 let q = eta[row] + etaw[row];
1379 let (m1, m2, m3) = self.neglog_q_derivatives(self.y[row], self.weights[row], q)?;
1380 let m4 = self.neglog_q_fourth_derivative(self.y[row], self.weights[row], q)?;
1381 let a = geom.dq_dq0[row];
1382 let b = geom.d2q_dq02[row];
1383 let c = geom.d3q_dq03[row];
1384 let d = geom.d4q_dq04[row];
1385
1386 let q_u = a * xi_u[row] + phi_u[row];
1388 let a_u = b * xi_u[row] + b1u[row];
1389 let b_u = c * xi_u[row] + b2u[row];
1390
1391 let q_v = a * xi_v[row] + phi_v[row];
1393 let a_v = b * xi_v[row] + b1v[row];
1394 let b_v = c * xi_v[row] + b2v[row];
1395
1396 let q_uv = b * xi_u[row] * xi_v[row] + b1u[row] * xi_v[row] + b1v[row] * xi_u[row];
1398 let a_uv = c * xi_u[row] * xi_v[row] + b2u[row] * xi_v[row] + b2v[row] * xi_u[row];
1399 let b_uv = d * xi_u[row] * xi_v[row] + b3u[row] * xi_v[row] + b3v[row] * xi_u[row];
1400
1401 let [eta_eta, eta_w_b, eta_w_d1, eta_w_d2, eta_w_d3, _, _, _, _] =
1402 mean_wiggle_second_directional_coefficients(
1403 [m1, m2, m3, m4],
1404 [q_u, q_v, q_uv],
1405 [a, a_u, a_v, a_uv],
1406 [b, b_u, b_v, b_uv],
1407 [xi_u[row], xi_v[row]],
1408 );
1409 coeff_eta[row] = eta_eta;
1410 coeff_etaw_b[row] = eta_w_b;
1411 coeff_etaw_d1[row] = eta_w_d1;
1412 coeff_etaw_d2[row] = eta_w_d2;
1413 coeff_etaw_d3[row] = eta_w_d3;
1414
1415 dw[row] = m2;
1418 dw_u[row] = m3 * q_u;
1419 dw_v[row] = m3 * q_v;
1420 dw_uv[row] = m4 * q_u * q_v + m3 * q_uv;
1421 }
1422
1423 let d2_h_eta_eta = xt_diag_x_dense(&x_eta, &coeff_eta)?;
1425
1426 let d2_h_eta_w = xt_diag_y_dense(&x_eta, &coeff_etaw_b, &geom.basis)?
1433 + &xt_diag_y_dense(&x_eta, &coeff_etaw_d1, &geom.basis_d1)?
1434 + &xt_diag_y_dense(&x_eta, &coeff_etaw_d2, &geom.basis_d2)?
1435 + &xt_diag_y_dense(&x_eta, &coeff_etaw_d3, &geom.basis_d3)?;
1436
1437 let a_ab = xt_diag_y_dense(&basis_uv, &dw, &geom.basis)?;
1448 let a_ij = xt_diag_y_dense(&basis_u, &dw, &basis_v)?;
1449 let a_iwj = xt_diag_y_dense(&basis_u, &dw_v, &geom.basis)?;
1450 let a_jwi = xt_diag_y_dense(&basis_v, &dw_u, &geom.basis)?;
1451 let d2_h_ww = &a_ab
1452 + &a_ab.t()
1453 + &a_ij
1454 + a_ij.t()
1455 + &a_iwj
1456 + a_iwj.t()
1457 + &a_jwi
1458 + a_jwi.t()
1459 + &xt_diag_x_dense(&geom.basis, &dw_uv)?;
1460
1461 Ok(Some(binomial_pack_mean_wiggle_joint_symmetrichessian(
1462 &d2_h_eta_eta,
1463 &d2_h_eta_w,
1464 &d2_h_ww,
1465 )))
1466 }
1467
1468 fn exact_newton_joint_psi_terms(
1469 &self,
1470 block_states: &[ParameterBlockState],
1471 specs: &[ParameterBlockSpec],
1472 hyper_layout: &crate::custom_family::CustomFamilyHyperLayout,
1473 psi_index: usize,
1474 ) -> Result<Option<gam_problem::ExactNewtonJointPsiTerms>, String> {
1475 if hyper_layout.family_axis_count() != 0 {
1476 return Err("BinomialMeanWiggleFamily does not declare family-owned hyper axes"
1477 .to_string());
1478 }
1479 let derivative_blocks = hyper_layout.design_derivative_blocks();
1480 validate_block_count::<GamlssError>("BinomialMeanWiggleFamily", 2, block_states.len())?;
1481 if derivative_blocks.len() != 2 {
1482 return Err(GamlssError::DimensionMismatch { reason: format!(
1483 "BinomialMeanWiggleFamily joint psi terms expect 2 derivative block lists, got {}",
1484 derivative_blocks.len()
1485 ) }.into());
1486 }
1487 let x_eta = self.dense_eta_design_fromspecs(specs)?;
1488 let eta = &block_states[Self::BLOCK_ETA].eta;
1489 let etaw = &block_states[Self::BLOCK_WIGGLE].eta;
1490 let betaw = &block_states[Self::BLOCK_WIGGLE].beta;
1491 let n = self.y.len();
1492 if eta.len() != n || etaw.len() != n || self.weights.len() != n {
1493 return Err(GamlssError::DimensionMismatch {
1494 reason: "BinomialMeanWiggleFamily input size mismatch".to_string(),
1495 }
1496 .into());
1497 }
1498 let geom = self.wiggle_geometry(eta.view(), betaw.view())?;
1499 let p_eta = x_eta.ncols();
1500 let pw = geom.basis.ncols();
1501 let implicit_dir =
1502 self.exact_newton_joint_psi_action(block_states, derivative_blocks, psi_index, p_eta)?;
1503 let dense_dir = if implicit_dir.is_none() {
1504 self.exact_newton_joint_psi_direction(
1505 block_states,
1506 derivative_blocks,
1507 psi_index,
1508 &x_eta,
1509 )?
1510 } else {
1511 None
1512 };
1513 let z_eta_psi = if let Some((_, ref z_eta_psi)) = implicit_dir {
1514 z_eta_psi
1515 } else if let Some(ref dir_a) = dense_dir {
1516 &dir_a.z_eta_psi
1517 } else {
1518 return Ok(None);
1519 };
1520
1521 let mut objective_psi = 0.0;
1522 let mut score_eta_xa = Array1::<f64>::zeros(n);
1523 let mut score_eta_x = Array1::<f64>::zeros(n);
1524 let mut score_w_b = Array1::<f64>::zeros(n);
1525 let mut score_w_d1 = Array1::<f64>::zeros(n);
1526
1527 let mut coeff_eta_eta_xx = Array1::<f64>::zeros(n);
1528 let mut coeff_eta_eta_xa_x = Array1::<f64>::zeros(n);
1529 let mut coeff_eta_w_xa_b = Array1::<f64>::zeros(n);
1530 let mut coeff_eta_w_x_b = Array1::<f64>::zeros(n);
1531 let mut coeff_eta_w_x_d1 = Array1::<f64>::zeros(n);
1532 let mut coeff_eta_w_xa_d1 = Array1::<f64>::zeros(n);
1533 let mut coeff_eta_w_x_d2 = Array1::<f64>::zeros(n);
1534 let mut coeff_ww_bb = Array1::<f64>::zeros(n);
1535 let mut coeff_ww_db = Array1::<f64>::zeros(n);
1536
1537 for row in 0..n {
1538 let q = eta[row] + etaw[row];
1539 let (m1, m2, m3) = self.neglog_q_derivatives(self.y[row], self.weights[row], q)?;
1540 let z_a = z_eta_psi[row];
1541 let a = geom.dq_dq0[row];
1542 let b = geom.d2q_dq02[row];
1543 let c = geom.d3q_dq03[row];
1544 let q_a = a * z_a;
1545
1546 objective_psi += m1 * q_a;
1547
1548 score_eta_xa[row] = m1 * a;
1549 score_eta_x[row] = m2 * q_a * a + m1 * b * z_a;
1550 score_w_b[row] = m2 * q_a;
1551 score_w_d1[row] = m1 * z_a;
1552
1553 coeff_eta_eta_xx[row] =
1554 m3 * q_a * a * a + m2 * (2.0 * a * b * z_a + q_a * b) + m1 * c * z_a;
1555 coeff_eta_eta_xa_x[row] = m2 * a * a + m1 * b;
1556 coeff_eta_w_xa_b[row] = m2 * a;
1557 coeff_eta_w_x_b[row] = m3 * q_a * a + m2 * b * z_a;
1558 coeff_eta_w_x_d1[row] = m2 * (a * z_a + q_a);
1559 coeff_eta_w_xa_d1[row] = m1;
1560 coeff_eta_w_x_d2[row] = m1 * z_a;
1561 coeff_ww_bb[row] = m3 * q_a;
1562 coeff_ww_db[row] = m2 * z_a;
1563 }
1564
1565 let score_w = gam_linalg::faer_ndarray::fast_atv(&geom.basis, &score_w_b)
1566 + gam_linalg::faer_ndarray::fast_atv(&geom.basis_d1, &score_w_d1);
1567
1568 if let Some((action, _)) = implicit_dir {
1569 let score_eta = action.transpose_mul(score_eta_xa.view())
1570 + gam_linalg::faer_ndarray::fast_atv(x_eta.as_ref(), &score_eta_x);
1571 let score_psi = binomial_pack_mean_wiggle_joint_score(&score_eta, &score_w);
1572 let x_eta_arc = shared_dense_arc(x_eta.as_ref());
1573 let basis_arc = Arc::new(geom.basis.clone());
1574 let basis_d1_arc = Arc::new(geom.basis_d1.clone());
1575 let basis_d2_arc = Arc::new(geom.basis_d2.clone());
1576 let zeros = Array1::<f64>::zeros(n);
1577 let operator = CustomFamilyJointPsiOperator::new(
1578 p_eta + pw,
1579 vec![
1580 CustomFamilyJointDesignChannel::new(
1581 0..p_eta,
1582 Arc::clone(&x_eta_arc),
1583 Some(action),
1584 ),
1585 CustomFamilyJointDesignChannel::new(
1586 p_eta..p_eta + pw,
1587 Arc::clone(&basis_arc),
1588 None,
1589 ),
1590 CustomFamilyJointDesignChannel::new(
1591 p_eta..p_eta + pw,
1592 Arc::clone(&basis_d1_arc),
1593 None,
1594 ),
1595 CustomFamilyJointDesignChannel::new(
1596 p_eta..p_eta + pw,
1597 Arc::clone(&basis_d2_arc),
1598 None,
1599 ),
1600 ],
1601 vec![
1602 CustomFamilyJointDesignPairContribution::new(
1603 0,
1604 0,
1605 coeff_eta_eta_xa_x.clone(),
1606 coeff_eta_eta_xx.clone(),
1607 ),
1608 CustomFamilyJointDesignPairContribution::new(
1609 0,
1610 1,
1611 coeff_eta_w_xa_b.clone(),
1612 coeff_eta_w_x_b.clone(),
1613 ),
1614 CustomFamilyJointDesignPairContribution::new(
1615 1,
1616 0,
1617 coeff_eta_w_xa_b.clone(),
1618 coeff_eta_w_x_b.clone(),
1619 ),
1620 CustomFamilyJointDesignPairContribution::new(
1621 0,
1622 2,
1623 coeff_eta_w_xa_d1.clone(),
1624 coeff_eta_w_x_d1.clone(),
1625 ),
1626 CustomFamilyJointDesignPairContribution::new(
1627 2,
1628 0,
1629 coeff_eta_w_xa_d1.clone(),
1630 coeff_eta_w_x_d1.clone(),
1631 ),
1632 CustomFamilyJointDesignPairContribution::new(
1633 0,
1634 3,
1635 zeros.clone(),
1636 coeff_eta_w_x_d2.clone(),
1637 ),
1638 CustomFamilyJointDesignPairContribution::new(
1639 3,
1640 0,
1641 zeros.clone(),
1642 coeff_eta_w_x_d2.clone(),
1643 ),
1644 CustomFamilyJointDesignPairContribution::new(
1645 1,
1646 1,
1647 zeros.clone(),
1648 coeff_ww_bb.clone(),
1649 ),
1650 CustomFamilyJointDesignPairContribution::new(
1651 2,
1652 1,
1653 zeros.clone(),
1654 coeff_ww_db.clone(),
1655 ),
1656 CustomFamilyJointDesignPairContribution::new(1, 2, zeros, coeff_ww_db.clone()),
1657 ],
1658 );
1659 return Ok(Some(gam_problem::ExactNewtonJointPsiTerms {
1660 objective_psi,
1661 score_psi,
1662 hessian_psi: Array2::zeros((0, 0)),
1663 hessian_psi_operator: Some(std::sync::Arc::new(operator)),
1664 }));
1665 }
1666
1667 let dir_a =
1668 dense_dir.expect("dense psi direction should exist when implicit direction is absent");
1669 let x_eta_psi = dir_a
1670 .x_eta_psi
1671 .as_ref()
1672 .expect("dense eta psi design should exist when implicit direction is absent");
1673 let score_psi = binomial_pack_mean_wiggle_joint_score(
1674 &(gam_linalg::faer_ndarray::fast_atv(x_eta_psi, &score_eta_xa)
1675 + gam_linalg::faer_ndarray::fast_atv(x_eta.as_ref(), &score_eta_x)),
1676 &score_w,
1677 );
1678 let a_eta_eta = xt_diag_y_dense(x_eta_psi, &coeff_eta_eta_xa_x, &x_eta)?;
1679 let h_eta_eta = &a_eta_eta + &a_eta_eta.t() + &xt_diag_x_dense(&x_eta, &coeff_eta_eta_xx)?;
1680 let h_eta_w = xt_diag_y_dense(x_eta_psi, &coeff_eta_w_xa_b, &geom.basis)?
1681 + &xt_diag_y_dense(&x_eta, &coeff_eta_w_x_b, &geom.basis)?
1682 + &xt_diag_y_dense(&x_eta, &coeff_eta_w_x_d1, &geom.basis_d1)?
1683 + &xt_diag_y_dense(x_eta_psi, &coeff_eta_w_xa_d1, &geom.basis_d1)?
1684 + &xt_diag_y_dense(&x_eta, &coeff_eta_w_x_d2, &geom.basis_d2)?;
1685 let a_ww = xt_diag_y_dense(&geom.basis_d1, &coeff_ww_db, &geom.basis)?;
1686 let h_ww = xt_diag_x_dense(&geom.basis, &coeff_ww_bb)? + &a_ww + a_ww.t();
1687
1688 Ok(Some(gam_problem::ExactNewtonJointPsiTerms {
1689 objective_psi,
1690 score_psi,
1691 hessian_psi: binomial_pack_mean_wiggle_joint_symmetrichessian(
1692 &h_eta_eta, &h_eta_w, &h_ww,
1693 ),
1694 hessian_psi_operator: None,
1695 }))
1696 }
1697}
1698
1699pub(crate) struct BinomialMeanWiggleHessianWorkspace {
1700 pub(crate) family: BinomialMeanWiggleFamily,
1701 pub(crate) block_states: Vec<ParameterBlockState>,
1702 pub(crate) specs: Vec<ParameterBlockSpec>,
1706 pub(crate) x_eta: Arc<Array2<f64>>,
1707 pub(crate) hessian_operator: Arc<RowCoeffOperator>,
1708}
1709
1710impl BinomialMeanWiggleHessianWorkspace {
1711 pub(crate) fn new(
1712 family: BinomialMeanWiggleFamily,
1713 block_states: Vec<ParameterBlockState>,
1714 specs: Vec<ParameterBlockSpec>,
1715 x_eta: Array2<f64>,
1716 ) -> Result<Self, String> {
1717 let x_eta = Arc::new(x_eta);
1718 let hessian_operator = family.bmw_static_hessian_operator(&block_states, x_eta.clone())?;
1719 Ok(Self {
1720 family,
1721 block_states,
1722 specs,
1723 x_eta,
1724 hessian_operator,
1725 })
1726 }
1727}
1728
1729impl ExactNewtonJointHessianWorkspace for BinomialMeanWiggleHessianWorkspace {
1730 fn joint_gradient_evaluation(
1745 &self,
1746 ) -> Result<Option<ExactNewtonJointGradientEvaluation>, String> {
1747 self.family
1748 .exact_newton_joint_gradient_evaluation(&self.block_states, &self.specs)
1749 }
1750
1751 fn warm_up_outer_caches_for_mode(
1752 &self,
1753 eval_mode: gam_problem::EvalMode,
1754 ) -> Result<(), String> {
1755 match eval_mode {
1756 gam_problem::EvalMode::ValueOnly
1757 | gam_problem::EvalMode::ValueAndGradient
1758 | gam_problem::EvalMode::ValueGradientHessian => Ok(()),
1759 }
1760 }
1761
1762 fn hessian_matvec_available(&self) -> bool {
1763 true
1764 }
1765
1766 fn hessian_matvec(&self, v: &Array1<f64>) -> Result<Option<Array1<f64>>, String> {
1767 Ok(Some(gam_problem::HyperOperator::mul_vec(
1768 self.hessian_operator.as_ref(),
1769 v,
1770 )))
1771 }
1772
1773 fn hessian_diagonal(&self) -> Result<Option<Array1<f64>>, String> {
1774 Ok(Some(self.hessian_operator.diagonal()))
1780 }
1781
1782 fn directional_derivative(
1783 &self,
1784 d_beta_flat: &Array1<f64>,
1785 ) -> Result<Option<Array2<f64>>, String> {
1786 Ok(self
1787 .directional_derivative_operator(d_beta_flat)?
1788 .map(|operator| operator.to_dense()))
1789 }
1790
1791 fn directional_derivative_operator(
1792 &self,
1793 d_beta_flat: &Array1<f64>,
1794 ) -> Result<Option<Arc<dyn gam_problem::HyperOperator>>, String> {
1795 self.family
1796 .bmw_directional_operator(&self.block_states, self.x_eta.clone(), d_beta_flat)
1797 }
1798
1799 fn second_directional_derivative(
1800 &self,
1801 d_beta_u_flat: &Array1<f64>,
1802 d_beta_v_flat: &Array1<f64>,
1803 ) -> Result<Option<Array2<f64>>, String> {
1804 Ok(self
1805 .second_directional_derivative_operator(d_beta_u_flat, d_beta_v_flat)?
1806 .map(|operator| operator.to_dense()))
1807 }
1808
1809 fn second_directional_derivative_operator(
1810 &self,
1811 d_beta_u: &Array1<f64>,
1812 d_beta_v: &Array1<f64>,
1813 ) -> Result<Option<Arc<dyn gam_problem::HyperOperator>>, String> {
1814 self.family.bmw_second_directional_operator(
1815 &self.block_states,
1816 self.x_eta.clone(),
1817 d_beta_u,
1818 d_beta_v,
1819 )
1820 }
1821}
1822
1823impl CustomFamilyGenerative for BinomialMeanWiggleFamily {
1824 fn generativespec(
1825 &self,
1826 block_states: &[ParameterBlockState],
1827 ) -> Result<GenerativeSpec, String> {
1828 validate_block_count::<GamlssError>("BinomialMeanWiggleFamily", 2, block_states.len())?;
1829 let eta = &block_states[Self::BLOCK_ETA].eta;
1830 let etaw = &block_states[Self::BLOCK_WIGGLE].eta;
1831 if eta.len() != self.y.len() || etaw.len() != self.y.len() {
1832 return Err(GamlssError::DimensionMismatch {
1833 reason: "BinomialMeanWiggleFamily generative size mismatch".to_string(),
1834 }
1835 .into());
1836 }
1837 let mean = gamlss_rowwise_map_result(self.y.len(), |i| {
1838 let jet = inverse_link_jet_for_inverse_link(&self.link_kind, eta[i] + etaw[i])
1839 .map_err(|e| format!("fixed-link wiggle inverse-link evaluation failed: {e}"))?;
1840 Ok(jet.mu)
1841 })?;
1842 Ok(GenerativeSpec {
1843 mean,
1844 noise: NoiseModel::Bernoulli,
1845 })
1846 }
1847}
1848
1849#[cfg(test)]
1850mod exact_frozen_monotonicity_tests {
1851 use super::*;
1852
1853 fn frozen_family_and_wiggle_spec() -> (BinomialMeanWiggleFamily, ParameterBlockSpec) {
1854 let n = 4;
1855 let p = 3;
1856 let frozen = Array2::<f64>::zeros((n, p));
1857 let family = BinomialMeanWiggleFamily {
1858 y: Array1::zeros(n),
1859 weights: Array1::ones(n),
1860 link_kind: InverseLink::Standard(StandardLink::Logit),
1861 wiggle_knots: Array1::linspace(-1.0, 1.0, 8),
1862 wiggle_degree: 3,
1863 policy: gam_runtime::resource::ResourcePolicy::default_library(),
1864 frozen_warp_design: Some(Arc::new(frozen.clone())),
1865 };
1866 let spec = ParameterBlockSpec {
1867 name: "wiggle".to_string(),
1868 design: DesignMatrix::Dense(gam_linalg::matrix::DenseDesignMatrix::from(frozen)),
1869 offset: Array1::zeros(n),
1870 penalties: vec![],
1871 nullspace_dims: vec![],
1872 initial_log_lambdas: Array1::zeros(0),
1873 initial_beta: None,
1874 gauge_priority: 100,
1875 jacobian_callback: None,
1876 stacked_design: None,
1877 stacked_offset: None,
1878 };
1879 (family, spec)
1880 }
1881
1882 #[test]
1883 fn frozen_warp_keeps_exact_nonnegative_i_spline_cone() {
1884 let (family, spec) = frozen_family_and_wiggle_spec();
1885 let constraints = family
1886 .block_linear_constraints(&[], BinomialMeanWiggleFamily::BLOCK_WIGGLE, &spec)
1887 .expect("frozen constraint construction")
1888 .expect("wiggle block must be constrained")
1889 .to_dense()
1890 .expect("wiggle cone is a small dense system");
1891 assert_eq!(constraints.a, Array2::<f64>::eye(3));
1892 assert_eq!(constraints.b, Array1::<f64>::zeros(3));
1893
1894 let solver_slop = Array1::from_vec(vec![
1895 -0.5 * crate::wiggle::MONOTONE_WIGGLE_ACTIVE_SET_TOL,
1896 0.2,
1897 0.0,
1898 ]);
1899 let projected = family
1900 .post_update_block_beta(
1901 &[],
1902 BinomialMeanWiggleFamily::BLOCK_WIGGLE,
1903 &spec,
1904 solver_slop,
1905 )
1906 .expect("active-set slop projects onto the exact cone");
1907 assert_eq!(projected[0], 0.0);
1908
1909 let material_violation = Array1::from_vec(vec![
1910 -2.0 * crate::wiggle::MONOTONE_WIGGLE_ACTIVE_SET_TOL,
1911 0.2,
1912 0.0,
1913 ]);
1914 assert!(
1915 family
1916 .post_update_block_beta(
1917 &[],
1918 BinomialMeanWiggleFamily::BLOCK_WIGGLE,
1919 &spec,
1920 material_violation,
1921 )
1922 .is_err(),
1923 "a material negative I-spline coefficient must be rejected"
1924 );
1925 }
1926
1927 #[test]
1956 fn the_frozen_arm_is_convex_and_psi_independent_and_the_dynamic_arm_is_neither_2748() {
1957 let n = 64;
1958 let knots = Array1::linspace(-3.0, 3.0, 10);
1959 let degree = 3;
1960 let dynamic = BinomialMeanWiggleFamily {
1961 y: Array1::from_shape_fn(n, |i| if i % 2 == 0 { 1.0 } else { 0.0 }),
1962 weights: Array1::ones(n),
1963 link_kind: InverseLink::Standard(StandardLink::Logit),
1964 wiggle_knots: knots.clone(),
1965 wiggle_degree: degree,
1966 policy: gam_runtime::resource::ResourcePolicy::default_library(),
1967 frozen_warp_design: None,
1968 };
1969 let q0 = Array1::linspace(-2.5, 2.5, n);
1974 let basis_width = dynamic
1975 .wiggle_design(q0.view())
1976 .expect("value basis at the probe index")
1977 .ncols();
1978 let beta_w = Array1::from_elem(basis_width, 1.5);
1979
1980 let geom = dynamic
1981 .wiggle_geometry(q0.view(), beta_w.view())
1982 .expect("dynamic geometry");
1983 assert!(
1984 geom.dq_dq0.iter().cloned().fold(f64::MIN, f64::max) > 1.0,
1985 "NON-VACUITY: the fixture must be in the supercritical warp regime, else the \
1986 dynamic arm has nothing to be non-convex about (max dq/dq0 = {})",
1987 geom.dq_dq0.iter().cloned().fold(f64::MIN, f64::max),
1988 );
1989 let dynamic_min_coeff = (0..n)
1990 .map(|row| {
1991 let q = q0[row] + 0.0;
1992 let (m1, m2, _) = dynamic
1993 .neglog_q_derivatives(dynamic.y[row], dynamic.weights[row], q)
1994 .expect("row derivatives");
1995 let a = geom.dq_dq0[row];
1996 let b = geom.d2q_dq02[row];
1997 crate::gamlss::binomial_q_coeffs::hessian_coeff_fromobjective_q_terms(
1998 m1, m2, a, a, b,
1999 )
2000 })
2001 .fold(f64::MAX, f64::min);
2002 assert!(
2003 dynamic_min_coeff < 0.0,
2004 "the dynamic arm's own eta row coefficient must go negative -- that IS the \
2005 indefiniteness the outer search could not solve through (min = {dynamic_min_coeff:.6e})"
2006 );
2007
2008 let frozen_basis = dynamic
2010 .wiggle_design(q0.view())
2011 .expect("frozen basis is the value basis at the frozen index");
2012 let frozen = BinomialMeanWiggleFamily {
2013 frozen_warp_design: Some(Arc::new(frozen_basis.clone())),
2014 ..dynamic.clone()
2015 };
2016 let frozen_geom = frozen
2017 .wiggle_geometry(q0.view(), beta_w.view())
2018 .expect("frozen geometry");
2019 assert!(
2020 frozen_geom.dq_dq0.iter().all(|value| *value == 1.0)
2021 && frozen_geom.d2q_dq02.iter().all(|value| *value == 0.0),
2022 "the freeze is exactly `dq/dq0 = 1`, `d2q/dq0^2 = 0`"
2023 );
2024 let frozen_min_coeff = (0..n)
2025 .map(|row| {
2026 let (m1, m2, _) = frozen
2027 .neglog_q_derivatives(frozen.y[row], frozen.weights[row], q0[row])
2028 .expect("row derivatives");
2029 crate::gamlss::binomial_q_coeffs::hessian_coeff_fromobjective_q_terms(
2030 m1, m2, 1.0, 1.0, 0.0,
2031 )
2032 })
2033 .fold(f64::MAX, f64::min);
2034 assert!(
2035 frozen_min_coeff >= 0.0,
2036 "frozen, every row coefficient is `m2 = w*mu*(1-mu) >= 0`, so the joint Hessian \
2037 is a Gram plus penalties (min = {frozen_min_coeff:.6e})"
2038 );
2039
2040 let spec = ParameterBlockSpec {
2043 name: "wiggle".to_string(),
2044 design: DesignMatrix::Dense(gam_linalg::matrix::DenseDesignMatrix::from(
2045 frozen_basis.clone(),
2046 )),
2047 offset: Array1::zeros(n),
2048 penalties: vec![],
2049 nullspace_dims: vec![],
2050 initial_log_lambdas: Array1::zeros(0),
2051 initial_beta: None,
2052 gauge_priority: 100,
2053 jacobian_callback: None,
2054 stacked_design: None,
2055 stacked_offset: None,
2056 };
2057 let states_at = |eta: Array1<f64>| {
2058 vec![
2059 ParameterBlockState {
2060 beta: Array1::zeros(1),
2061 eta,
2062 },
2063 ParameterBlockState {
2064 beta: beta_w.clone(),
2065 eta: Array1::zeros(n),
2066 },
2067 ]
2068 };
2069 let shifted = &q0 + 0.75;
2070 let geometry_of = |fam: &BinomialMeanWiggleFamily, eta: Array1<f64>| {
2071 fam.block_geometry(&states_at(eta), &spec)
2072 .expect("wiggle block geometry")
2073 .0
2074 .to_dense()
2075 };
2076 let frozen_here = geometry_of(&frozen, q0.clone());
2077 let frozen_there = geometry_of(&frozen, shifted.clone());
2078 assert_eq!(
2079 frozen_here, frozen_there,
2080 "frozen, the wiggle geometry does not move with the index, which is what the \
2081 driver's `vec![eta_derivs, Vec::new()]` psi layout asserts"
2082 );
2083 let dynamic_here = geometry_of(&dynamic, q0.clone());
2084 let dynamic_there = geometry_of(&dynamic, shifted);
2085 assert_ne!(
2086 dynamic_here, dynamic_there,
2087 "NON-VACUITY: dynamic, it MUST move -- otherwise the psi-layout claim would have \
2088 been true on that arm too and there would be nothing to repair"
2089 );
2090 }
2091
2092 #[test]
2104 fn the_warp_coordinate_is_structural_and_the_mean_coordinate_is_not_2748() {
2105 let (family, wiggle_spec) = frozen_family_and_wiggle_spec();
2106 let n = family.y.len();
2107 let p_eta = 2;
2108 let mut eta_spec = wiggle_spec.clone();
2109 eta_spec.name = "eta".to_string();
2110 eta_spec.design = DesignMatrix::Dense(gam_linalg::matrix::DenseDesignMatrix::from(
2111 Array2::<f64>::zeros((n, p_eta)),
2112 ));
2113 let specs = [eta_spec, wiggle_spec];
2114 let states: Vec<ParameterBlockState> = specs
2115 .iter()
2116 .map(|spec| ParameterBlockState {
2117 beta: Array1::zeros(spec.design.ncols()),
2118 eta: Array1::zeros(n),
2119 })
2120 .collect();
2121
2122 for (index, spec) in specs.iter().enumerate() {
2123 let cone = family
2124 .block_linear_constraints(&states, index, spec)
2125 .expect("constraint query at the probe state");
2126 let coordinate = family.block_coefficient_coordinate(&states, index, spec);
2127 assert_eq!(
2128 coordinate.is_structural(),
2129 cone.is_some(),
2130 "block '{}' declares coordinate {coordinate:?} while its cone is {}: the \
2131 declaration and the constraint it is about must be one statement",
2132 spec.name,
2133 if cone.is_some() { "present" } else { "absent" },
2134 );
2135 }
2136
2137 assert!(
2140 family
2141 .block_coefficient_coordinate(&states, BinomialMeanWiggleFamily::BLOCK_WIGGLE, &specs[1])
2142 .is_structural(),
2143 "the monotone warp coordinate carries the beta_w >= 0 cone",
2144 );
2145 assert!(
2146 family
2147 .block_coefficient_coordinate(&states, BinomialMeanWiggleFamily::BLOCK_ETA, &specs[0])
2148 .is_spanning(),
2149 "the mean block is an ordinary basis and must stay reparameterisable",
2150 );
2151 }
2152}