1use super::*;
6
7pub struct GaussianLocationScaleFamily {
8 pub y: Array1<f64>,
9 pub weights: Array1<f64>,
10 pub mu_design: Option<DesignMatrix>,
11 pub log_sigma_design: Option<DesignMatrix>,
12 pub policy: gam_runtime::resource::ResourcePolicy,
17 pub cached_row_scalars:
27 std::sync::RwLock<Option<(Array1<f64>, Array1<f64>, Arc<GaussianJointRowScalars>)>>,
28}
29
30impl Clone for GaussianLocationScaleFamily {
31 fn clone(&self) -> Self {
32 Self {
33 y: self.y.clone(),
34 weights: self.weights.clone(),
35 mu_design: self.mu_design.clone(),
36 log_sigma_design: self.log_sigma_design.clone(),
37 policy: self.policy.clone(),
38 cached_row_scalars: std::sync::RwLock::new(
39 self.cached_row_scalars
40 .read()
41 .expect("lock poisoned")
42 .clone(),
43 ),
44 }
45 }
46}
47
48impl GaussianLocationScaleFamily {
49 pub const BLOCK_MU: usize = 0;
50 pub const BLOCK_LOG_SIGMA: usize = 1;
51
52 #[inline]
64 fn eta_keys_match(stored: &Array1<f64>, query: &Array1<f64>) -> bool {
65 stored.len() == query.len()
66 && stored
67 .iter()
68 .zip(query.iter())
69 .all(|(a, b)| a.to_bits() == b.to_bits())
70 }
71
72 pub(crate) fn get_or_compute_row_scalars(
73 &self,
74 etamu: &Array1<f64>,
75 eta_ls: &Array1<f64>,
76 ) -> Result<Arc<GaussianJointRowScalars>, String> {
77 if let Ok(guard) = self.cached_row_scalars.read() {
81 if let Some((cmu, cls, rows)) = guard.as_ref() {
82 if Self::eta_keys_match(cmu, etamu) && Self::eta_keys_match(cls, eta_ls) {
83 return Ok(Arc::clone(rows));
84 }
85 }
86 }
87 let rows = Arc::new(gaussian_jointrow_scalars(
92 &self.y,
93 etamu,
94 eta_ls,
95 &self.weights,
96 )?);
97 if let Ok(mut guard) = self.cached_row_scalars.write() {
98 *guard = Some((etamu.clone(), eta_ls.clone(), Arc::clone(&rows)));
99 }
100 Ok(rows)
101 }
102
103 pub fn parameternames() -> &'static [&'static str] {
104 &["mu", "log_sigma"]
105 }
106
107 pub fn parameter_links() -> &'static [ParameterLink] {
108 &[ParameterLink::Identity, ParameterLink::Log]
109 }
110
111 pub fn metadata() -> FamilyMetadata {
112 FamilyMetadata {
113 name: "gaussian_location_scale",
114 parameternames: Self::parameternames(),
115 parameter_links: Self::parameter_links(),
116 }
117 }
118
119 pub(crate) fn exact_joint_supported(&self) -> bool {
120 self.mu_design.is_some() && self.log_sigma_design.is_some()
121 }
122
123 pub(crate) fn exact_block_designs(
124 &self,
125 ) -> Result<(DenseOrOperator<'_>, DenseOrOperator<'_>), String> {
126 let mu_design = self.mu_design.as_ref().ok_or_else(|| {
127 "GaussianLocationScaleFamily exact path is missing mu design".to_string()
128 })?;
129 let log_sigma_design = self.log_sigma_design.as_ref().ok_or_else(|| {
130 "GaussianLocationScaleFamily exact path is missing log-sigma design".to_string()
131 })?;
132 let planned = dense_blocks_planned_budget(&[mu_design, log_sigma_design]);
133 let xmu = dense_block_or_operator(
134 mu_design,
135 mu_design.nrows(),
136 mu_design.ncols(),
137 planned[0],
138 &self.policy,
139 );
140 let x_ls = dense_block_or_operator(
141 log_sigma_design,
142 log_sigma_design.nrows(),
143 log_sigma_design.ncols(),
144 planned[1],
145 &self.policy,
146 );
147 Ok((xmu, x_ls))
148 }
149
150 pub(crate) fn exact_block_designs_fromspecs<'a>(
151 &self,
152 specs: &'a [ParameterBlockSpec],
153 ) -> Result<(DenseOrOperator<'a>, DenseOrOperator<'a>), String> {
154 if specs.len() != 2 {
155 return Err(GamlssError::DimensionMismatch {
156 reason: format!(
157 "GaussianLocationScaleFamily spec-aware exact path expects 2 specs, got {}",
158 specs.len()
159 ),
160 }
161 .into());
162 }
163 let mu_design = &specs[Self::BLOCK_MU].design;
164 let log_sigma_design = &specs[Self::BLOCK_LOG_SIGMA].design;
165 let planned = dense_blocks_planned_budget(&[mu_design, log_sigma_design]);
166 let xmu = dense_block_or_operator(
167 mu_design,
168 mu_design.nrows(),
169 mu_design.ncols(),
170 planned[0],
171 &self.policy,
172 );
173 let x_ls = dense_block_or_operator(
174 log_sigma_design,
175 log_sigma_design.nrows(),
176 log_sigma_design.ncols(),
177 planned[1],
178 &self.policy,
179 );
180 Ok((xmu, x_ls))
181 }
182
183 pub(crate) fn exact_joint_block_designs<'a>(
184 &'a self,
185 specs: Option<&'a [ParameterBlockSpec]>,
186 ) -> Result<Option<(DenseOrOperator<'a>, DenseOrOperator<'a>)>, String> {
187 if let Some(specs) = specs {
201 return self.exact_block_designs_fromspecs(specs).map(Some);
202 }
203 if self.exact_joint_supported() {
204 return self.exact_block_designs().map(Some);
205 }
206 Ok(None)
207 }
208
209 pub(crate) fn exact_joint_dense_block_designs<'a>(
210 &'a self,
211 specs: Option<&'a [ParameterBlockSpec]>,
212 ) -> Result<Option<(Cow<'a, Array2<f64>>, Cow<'a, Array2<f64>>)>, String> {
213 let Some((xmu, x_ls)) = self.exact_joint_block_designs(specs)? else {
214 return Ok(None);
215 };
216 let xmu = match xmu {
217 DenseOrOperator::Borrowed(dense) => Cow::Borrowed(dense),
218 DenseOrOperator::Owned(dense) => Cow::Owned(dense),
219 DenseOrOperator::Operator(_) => {
220 return Err(
221 "GaussianLocationScaleFamily exact psi path requires chunked operator support for oversized designs"
222 .to_string(),
223 );
224 }
225 };
226 let x_ls = match x_ls {
227 DenseOrOperator::Borrowed(dense) => Cow::Borrowed(dense),
228 DenseOrOperator::Owned(dense) => Cow::Owned(dense),
229 DenseOrOperator::Operator(_) => {
230 return Err(
231 "GaussianLocationScaleFamily exact psi path requires chunked operator support for oversized designs"
232 .to_string(),
233 );
234 }
235 };
236 Ok(Some((xmu, x_ls)))
237 }
238
239 pub(crate) fn exact_newton_joint_hessian_for_specs(
240 &self,
241 block_states: &[ParameterBlockState],
242 specs: Option<&[ParameterBlockSpec]>,
243 ) -> Result<Option<Array2<f64>>, String> {
244 let Some((xmu, x_ls)) = self.exact_joint_block_designs(specs)? else {
245 return Ok(None);
246 };
247 self.exact_newton_joint_hessian_from_designs(block_states, &xmu, &x_ls)
248 }
249
250 pub(crate) fn exact_newton_joint_gradient_for_specs(
255 &self,
256 block_states: &[ParameterBlockState],
257 specs: Option<&[ParameterBlockSpec]>,
258 ) -> Result<Option<ExactNewtonJointGradientEvaluation>, String> {
259 let Some((xmu, x_ls)) = self.exact_joint_dense_block_designs(specs)? else {
260 return Ok(None);
261 };
262 self.exact_newton_joint_gradient_from_designs(block_states, &xmu, &x_ls)
263 .map(Some)
264 }
265
266 pub(crate) fn exact_newton_joint_gradient_from_designs(
277 &self,
278 block_states: &[ParameterBlockState],
279 xmu: &Array2<f64>,
280 x_ls: &Array2<f64>,
281 ) -> Result<ExactNewtonJointGradientEvaluation, String> {
282 validate_block_count::<GamlssError>("GaussianLocationScaleFamily", 2, block_states.len())?;
283 let n = self.y.len();
284 let etamu = &block_states[Self::BLOCK_MU].eta;
285 let eta_ls = &block_states[Self::BLOCK_LOG_SIGMA].eta;
286 if etamu.len() != n
287 || eta_ls.len() != n
288 || self.weights.len() != n
289 || xmu.nrows() != n
290 || x_ls.nrows() != n
291 {
292 return Err(GamlssError::DimensionMismatch {
293 reason: "GaussianLocationScaleFamily joint gradient input size mismatch"
294 .to_string(),
295 }
296 .into());
297 }
298 let rows = self.get_or_compute_row_scalars(etamu, eta_ls)?;
299 let zero = Array1::<f64>::zeros(n);
300 let weights = gaussian_joint_psi_firstweights(&rows, &zero, &zero);
301 let grad_eta_mu = -&weights.scoremu;
302 let grad_eta_ls = -&weights.score_ls;
303 let grad_mu = fast_atv(xmu, &grad_eta_mu);
304 let grad_ls = fast_atv(x_ls, &grad_eta_ls);
305 let gradient = gaussian_pack_joint_score(&grad_mu, &grad_ls);
306 let log_likelihood = self.log_likelihood_only(block_states)?;
307 Ok(ExactNewtonJointGradientEvaluation {
308 log_likelihood,
309 gradient,
310 })
311 }
312
313 pub(crate) fn exact_newton_joint_hessian_directional_derivative_for_specs(
314 &self,
315 block_states: &[ParameterBlockState],
316 specs: Option<&[ParameterBlockSpec]>,
317 d_beta_flat: &Array1<f64>,
318 ) -> Result<Option<Array2<f64>>, String> {
319 let Some((xmu, x_ls)) = self.exact_joint_block_designs(specs)? else {
320 return Ok(None);
321 };
322 self.exact_newton_joint_hessian_directional_derivative_from_designs(
323 block_states,
324 &xmu,
325 &x_ls,
326 d_beta_flat,
327 )
328 }
329
330 pub(crate) fn exact_newton_joint_hessian_second_directional_derivative_for_specs(
331 &self,
332 block_states: &[ParameterBlockState],
333 specs: Option<&[ParameterBlockSpec]>,
334 d_beta_u_flat: &Array1<f64>,
335 d_betav_flat: &Array1<f64>,
336 ) -> Result<Option<Array2<f64>>, String> {
337 let Some((xmu, x_ls)) = self.exact_joint_block_designs(specs)? else {
338 return Ok(None);
339 };
340 self.exact_newton_joint_hessiansecond_directional_derivative_from_designs(
341 block_states,
342 &xmu,
343 &x_ls,
344 d_beta_u_flat,
345 d_betav_flat,
346 )
347 }
348
349 pub(crate) fn exact_newton_joint_psi_terms_for_specs(
350 &self,
351 block_states: &[ParameterBlockState],
352 specs: &[ParameterBlockSpec],
353 hyper_layout: &crate::custom_family::CustomFamilyHyperLayout,
354 psi_index: usize,
355 ) -> Result<Option<gam_problem::ExactNewtonJointPsiTerms>, String> {
356 if hyper_layout.family_axis_count() != 0 {
357 return Err("GaussianLocationScaleFamily does not declare family-owned hyper axes"
358 .to_string());
359 }
360 let Some((xmu, x_ls)) = self.exact_joint_dense_block_designs(Some(specs))? else {
361 return Ok(None);
362 };
363 self.exact_newton_joint_psi_terms_from_designs(
364 block_states,
365 specs,
366 hyper_layout.design_derivative_blocks(),
367 psi_index,
368 &xmu,
369 &x_ls,
370 )
371 }
372
373 pub(crate) fn exact_newton_joint_psisecond_order_terms_for_specs(
374 &self,
375 block_states: &[ParameterBlockState],
376 specs: &[ParameterBlockSpec],
377 hyper_layout: &crate::custom_family::CustomFamilyHyperLayout,
378 psi_i: usize,
379 psi_j: usize,
380 ) -> Result<Option<gam_problem::ExactNewtonJointPsiSecondOrderTerms>, String> {
381 if hyper_layout.family_axis_count() != 0 {
382 return Err("GaussianLocationScaleFamily does not declare family-owned hyper axes"
383 .to_string());
384 }
385 let Some((xmu, x_ls)) = self.exact_joint_dense_block_designs(Some(specs))? else {
386 return Ok(None);
387 };
388 self.exact_newton_joint_psisecond_order_terms_from_designs(
389 block_states,
390 hyper_layout.design_derivative_blocks(),
391 psi_i,
392 psi_j,
393 &xmu,
394 &x_ls,
395 )
396 }
397
398 pub(crate) fn exact_newton_joint_psihessian_directional_derivative_for_specs(
399 &self,
400 block_states: &[ParameterBlockState],
401 specs: &[ParameterBlockSpec],
402 hyper_layout: &crate::custom_family::CustomFamilyHyperLayout,
403 psi_index: usize,
404 d_beta_flat: &Array1<f64>,
405 ) -> Result<Option<Array2<f64>>, String> {
406 if hyper_layout.family_axis_count() != 0 {
407 return Err("GaussianLocationScaleFamily does not declare family-owned hyper axes"
408 .to_string());
409 }
410 let Some((xmu, x_ls)) = self.exact_joint_dense_block_designs(Some(specs))? else {
411 return Ok(None);
412 };
413 self.exact_newton_joint_psihessian_directional_derivative_from_designs(
414 block_states,
415 hyper_layout.design_derivative_blocks(),
416 psi_index,
417 d_beta_flat,
418 &xmu,
419 &x_ls,
420 )
421 }
422
423 pub(crate) fn exact_newton_joint_hessian_from_designs(
424 &self,
425 block_states: &[ParameterBlockState],
426 xmu: &DenseOrOperator<'_>,
427 x_ls: &DenseOrOperator<'_>,
428 ) -> Result<Option<Array2<f64>>, String> {
429 validate_block_count::<GamlssError>("GaussianLocationScaleFamily", 2, block_states.len())?;
430 let n = self.y.len();
431 let etamu = &block_states[Self::BLOCK_MU].eta;
432 let eta_ls = &block_states[Self::BLOCK_LOG_SIGMA].eta;
433 if etamu.len() != n || eta_ls.len() != n || self.weights.len() != n {
434 return Err(GamlssError::DimensionMismatch {
435 reason: "GaussianLocationScaleFamily input size mismatch".to_string(),
436 }
437 .into());
438 }
439
440 let rows = self.get_or_compute_row_scalars(etamu, eta_ls)?;
441 let (mm, cross, scale) = gaussian_locscale_observed_joint_row_coeffs(&rows);
446 Ok(Some(gaussian_joint_hessian_from_designs(
447 xmu, x_ls, &mm, &cross, &scale,
448 )?))
449 }
450
451 pub(crate) fn exact_newton_joint_hessian_directional_derivative_from_designs(
452 &self,
453 block_states: &[ParameterBlockState],
454 xmu: &DenseOrOperator<'_>,
455 x_ls: &DenseOrOperator<'_>,
456 d_beta_flat: &Array1<f64>,
457 ) -> Result<Option<Array2<f64>>, String> {
458 validate_block_count::<GamlssError>("GaussianLocationScaleFamily", 2, block_states.len())?;
459 let n = self.y.len();
460 let etamu = &block_states[Self::BLOCK_MU].eta;
461 let eta_ls = &block_states[Self::BLOCK_LOG_SIGMA].eta;
462 if etamu.len() != n || eta_ls.len() != n || self.weights.len() != n {
463 return Err(GamlssError::DimensionMismatch {
464 reason: "GaussianLocationScaleFamily input size mismatch".to_string(),
465 }
466 .into());
467 }
468
469 let pmu = xmu.ncols();
470 let p_ls = x_ls.ncols();
471 let total = pmu + p_ls;
472 if d_beta_flat.len() != total {
473 return Err(GamlssError::DimensionMismatch {
474 reason: format!(
475 "GaussianLocationScaleFamily joint d_beta length mismatch: got {}, expected {}",
476 d_beta_flat.len(),
477 total
478 ),
479 }
480 .into());
481 }
482 let ximu = xmu.dot(d_beta_flat.slice(s![0..pmu]));
483 let xi_ls = x_ls.dot(d_beta_flat.slice(s![pmu..pmu + p_ls]));
484 let rows = self.get_or_compute_row_scalars(etamu, eta_ls)?;
485 let directional = gaussian_joint_first_directionalweights(&rows, &ximu, &xi_ls);
486 let dhmumu = directional.0;
487 let dh_ls_ls = directional.2;
488 let dhmu_ls = directional.1;
496
497 Ok(Some(gaussian_joint_hessian_from_designs(
498 xmu, x_ls, &dhmumu, &dhmu_ls, &dh_ls_ls,
499 )?))
500 }
501
502 pub(crate) fn exact_newton_joint_hessiansecond_directional_derivative_from_designs(
503 &self,
504 block_states: &[ParameterBlockState],
505 xmu: &DenseOrOperator<'_>,
506 x_ls: &DenseOrOperator<'_>,
507 d_beta_u_flat: &Array1<f64>,
508 d_betav_flat: &Array1<f64>,
509 ) -> Result<Option<Array2<f64>>, String> {
510 validate_block_count::<GamlssError>("GaussianLocationScaleFamily", 2, block_states.len())?;
511 let n = self.y.len();
512 let etamu = &block_states[Self::BLOCK_MU].eta;
513 let eta_ls = &block_states[Self::BLOCK_LOG_SIGMA].eta;
514 if etamu.len() != n || eta_ls.len() != n || self.weights.len() != n {
515 return Err(GamlssError::DimensionMismatch {
516 reason: "GaussianLocationScaleFamily input size mismatch".to_string(),
517 }
518 .into());
519 }
520
521 let pmu = xmu.ncols();
522 let p_ls = x_ls.ncols();
523 let total = pmu + p_ls;
524 if d_beta_u_flat.len() != total || d_betav_flat.len() != total {
525 return Err(GamlssError::DimensionMismatch { reason: format!(
526 "GaussianLocationScaleFamily joint second directional derivative length mismatch: got {} and {}, expected {}",
527 d_beta_u_flat.len(),
528 d_betav_flat.len(),
529 total
530 ) }.into());
531 }
532 let ximu_u = xmu.dot(d_beta_u_flat.slice(s![0..pmu]));
533 let xi_ls_u = x_ls.dot(d_beta_u_flat.slice(s![pmu..pmu + p_ls]));
534 let ximuv = xmu.dot(d_betav_flat.slice(s![0..pmu]));
535 let xi_lsv = x_ls.dot(d_betav_flat.slice(s![pmu..pmu + p_ls]));
536 let rows = self.get_or_compute_row_scalars(etamu, eta_ls)?;
537 let second =
538 gaussian_jointsecond_directionalweights(&rows, &ximu_u, &xi_ls_u, &ximuv, &xi_lsv);
539 let d2hmumu = second.0;
540 let d2h_ls_ls = second.2;
541 let d2hmu_ls = second.1;
547
548 Ok(Some(gaussian_joint_hessian_from_designs(
549 xmu, x_ls, &d2hmumu, &d2hmu_ls, &d2h_ls_ls,
550 )?))
551 }
552
553 pub(crate) fn exact_newton_joint_psi_direction(
554 &self,
555 block_states: &[ParameterBlockState],
556 derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
557 psi_index: usize,
558 xmu: &Array2<f64>,
559 x_ls: &Array2<f64>,
560 policy: &gam_runtime::resource::ResourcePolicy,
561 ) -> Result<Option<LocationScaleJointPsiDirection>, String> {
562 let Some(parts) = locscale_joint_psi_direction_parts(
563 block_states,
564 derivative_blocks,
565 psi_index,
566 self.y.len(),
567 xmu.ncols(),
568 x_ls.ncols(),
569 Self::BLOCK_MU,
570 Self::BLOCK_LOG_SIGMA,
571 2,
572 "GaussianLocationScaleFamily",
573 "mu",
574 policy,
575 )?
576 else {
577 return Ok(None);
578 };
579 Ok(Some(LocationScaleJointPsiDirection {
580 block_idx: parts.block_idx,
581 local_idx: parts.local_idx,
582 z_primary_psi: parts.primary_z,
583 z_ls_psi: parts.log_sigma_z,
584 x_primary_psi: parts.primary_psi,
585 x_ls_psi: parts.log_sigma_psi,
586 }))
587 }
588
589 pub(crate) fn exact_newton_joint_psisecond_design_drifts(
590 &self,
591 block_states: &[ParameterBlockState],
592 derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
593 psi_a: &LocationScaleJointPsiDirection,
594 psi_b: &LocationScaleJointPsiDirection,
595 xmu: &Array2<f64>,
596 x_ls: &Array2<f64>,
597 ) -> Result<LocationScaleJointPsiSecondDrifts, String> {
598 locscale_joint_psisecond_design_drifts(
599 block_states,
600 derivative_blocks,
601 psi_a,
602 psi_b,
603 LocScalePsiDriftConfig {
604 n: self.y.len(),
605 p_primary: xmu.ncols(),
606 p_log_sigma: x_ls.ncols(),
607 primary_block_idx: Self::BLOCK_MU,
608 log_sigma_block_idx: Self::BLOCK_LOG_SIGMA,
609 family_name: "GaussianLocationScaleFamily",
610 primary_label: "mu",
611 policy: &self.policy,
612 },
613 )
614 }
615
616 pub(crate) fn exact_newton_joint_psi_terms_from_designs(
617 &self,
618 block_states: &[ParameterBlockState],
619 specs: &[ParameterBlockSpec],
620 derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
621 psi_index: usize,
622 xmu: &Array2<f64>,
623 x_ls: &Array2<f64>,
624 ) -> Result<Option<gam_problem::ExactNewtonJointPsiTerms>, String> {
625 validate_block_count::<GamlssError>("GaussianLocationScaleFamily", 2, block_states.len())?;
626 if specs.len() != 2 || derivative_blocks.len() != 2 {
627 return Err(GamlssError::DimensionMismatch { reason: format!(
628 "GaussianLocationScaleFamily joint psi terms expect 2 specs and 2 derivative blocks, got {} and {}",
629 specs.len(),
630 derivative_blocks.len()
631 ) }.into());
632 }
633 let Some(dir_a) = self.exact_newton_joint_psi_direction(
634 block_states,
635 derivative_blocks,
636 psi_index,
637 xmu,
638 x_ls,
639 &self.policy,
640 )?
641 else {
642 return Ok(None);
643 };
644 let etamu = &block_states[Self::BLOCK_MU].eta;
676 let eta_ls = &block_states[Self::BLOCK_LOG_SIGMA].eta;
677 let rows = self.get_or_compute_row_scalars(etamu, eta_ls)?;
678 let weights_a =
679 gaussian_joint_psi_firstweights(&rows, &dir_a.z_primary_psi, &dir_a.z_ls_psi);
680 let objective_psi = weights_a.objective_psirow.sum();
681 let xmu_map = dir_a.x_primary_psi.as_linear_map_ref();
682 let x_ls_map = dir_a.x_ls_psi.as_linear_map_ref();
683 let score_mu =
684 xmu_map.transpose_mul(weights_a.scoremu.view()) + fast_atv(xmu, &weights_a.dscoremu);
685 let score_ls = x_ls_map.transpose_mul(weights_a.score_ls.view())
686 + fast_atv(x_ls, &weights_a.dscore_ls);
687 let score_psi = gaussian_pack_joint_score(&score_mu, &score_ls);
688 let hessian_psi_operator = build_two_block_custom_family_joint_psi_operator_from_actions(
689 dir_a.x_primary_psi.cloned_first_action(),
690 dir_a.x_ls_psi.cloned_first_action(),
691 0..xmu.ncols(),
692 xmu.ncols()..xmu.ncols() + x_ls.ncols(),
693 xmu,
694 x_ls,
695 &weights_a.hmumu,
696 &weights_a.hmu_ls,
697 &weights_a.h_ls_ls,
698 &weights_a.dhmumu,
699 &weights_a.dhmu_ls,
700 &weights_a.dh_ls_ls,
701 )?;
702 let hessian_psi = if hessian_psi_operator.is_some() {
703 Array2::zeros((0, 0))
704 } else {
705 gaussian_joint_psihessian_fromweights(xmu, x_ls, xmu_map, x_ls_map, &weights_a)?
706 };
707
708 Ok(Some(gam_problem::ExactNewtonJointPsiTerms {
709 objective_psi,
710 score_psi,
711 hessian_psi,
712 hessian_psi_operator,
713 }))
714 }
715
716 pub(crate) fn exact_newton_joint_psisecond_order_terms_from_designs(
717 &self,
718 block_states: &[ParameterBlockState],
719 derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
720 psi_i: usize,
721 psi_j: usize,
722 xmu: &Array2<f64>,
723 x_ls: &Array2<f64>,
724 ) -> Result<Option<gam_problem::ExactNewtonJointPsiSecondOrderTerms>, String> {
725 let Some(dir_i) = self.exact_newton_joint_psi_direction(
726 block_states,
727 derivative_blocks,
728 psi_i,
729 xmu,
730 x_ls,
731 &self.policy,
732 )?
733 else {
734 return Ok(None);
735 };
736 let Some(dir_j) = self.exact_newton_joint_psi_direction(
737 block_states,
738 derivative_blocks,
739 psi_j,
740 xmu,
741 x_ls,
742 &self.policy,
743 )?
744 else {
745 return Ok(None);
746 };
747 Ok(Some(
748 self.exact_newton_joint_psisecond_order_terms_from_parts(
749 block_states,
750 derivative_blocks,
751 &dir_i,
752 &dir_j,
753 xmu,
754 x_ls,
755 None,
756 )?,
757 ))
758 }
759
760 pub(crate) fn exact_newton_joint_psisecond_order_terms_from_parts(
761 &self,
762 block_states: &[ParameterBlockState],
763 derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
764 dir_i: &LocationScaleJointPsiDirection,
765 dir_j: &LocationScaleJointPsiDirection,
766 xmu: &Array2<f64>,
767 x_ls: &Array2<f64>,
768 subsample: Option<&[crate::outer_subsample::WeightedOuterRow]>,
769 ) -> Result<gam_problem::ExactNewtonJointPsiSecondOrderTerms, String> {
770 let second_drifts = self.exact_newton_joint_psisecond_design_drifts(
771 block_states,
772 derivative_blocks,
773 dir_i,
774 dir_j,
775 xmu,
776 x_ls,
777 )?;
778 let n = self.y.len();
779 let xmu_i_map = dir_i.x_primary_psi.as_linear_map_ref();
780 let x_ls_i_map = dir_i.x_ls_psi.as_linear_map_ref();
781 let xmu_j_map = dir_j.x_primary_psi.as_linear_map_ref();
782 let x_ls_j_map = dir_j.x_ls_psi.as_linear_map_ref();
783 let xmu_ab_map = second_psi_linear_map(
784 second_drifts.x_primary_ab_action.as_ref(),
785 second_drifts.x_primary_ab.as_ref(),
786 n,
787 xmu.ncols(),
788 );
789 let x_ls_ab_map = second_psi_linear_map(
790 second_drifts.x_ls_ab_action.as_ref(),
791 second_drifts.x_ls_ab.as_ref(),
792 n,
793 x_ls.ncols(),
794 );
795 let etamu = &block_states[Self::BLOCK_MU].eta;
819 let eta_ls = &block_states[Self::BLOCK_LOG_SIGMA].eta;
820 let rows = self.get_or_compute_row_scalars(etamu, eta_ls)?;
821 let mut weights_i =
822 gaussian_joint_psi_firstweights(&rows, &dir_i.z_primary_psi, &dir_i.z_ls_psi);
823 let mut weights_j =
824 gaussian_joint_psi_firstweights(&rows, &dir_j.z_primary_psi, &dir_j.z_ls_psi);
825 let mut secondweights = gaussian_joint_psisecondweights(
826 &rows,
827 &dir_i.z_primary_psi,
828 &dir_i.z_ls_psi,
829 &dir_j.z_primary_psi,
830 &dir_j.z_ls_psi,
831 &second_drifts.z_primary_ab,
832 &second_drifts.z_ls_ab,
833 );
834 if let Some(sub_rows) = subsample {
835 apply_ht_mask_first(&mut weights_i, sub_rows);
841 apply_ht_mask_first(&mut weights_j, sub_rows);
842 apply_ht_mask_second(&mut secondweights, sub_rows);
843 }
844 let objective_psi_psi = secondweights.objective_psi_psirow.sum();
845
846 let score_psi_psi = gaussian_pack_joint_score(
847 &(xmu_ab_map.transpose_mul(weights_i.scoremu.view())
848 + xmu_i_map.transpose_mul(weights_j.dscoremu.view())
849 + xmu_j_map.transpose_mul(weights_i.dscoremu.view())
850 + fast_atv(xmu, &secondweights.d2scoremu)),
851 &(x_ls_ab_map.transpose_mul(weights_i.score_ls.view())
852 + x_ls_i_map.transpose_mul(weights_j.dscore_ls.view())
853 + x_ls_j_map.transpose_mul(weights_i.dscore_ls.view())
854 + fast_atv(x_ls, &secondweights.d2score_ls)),
855 );
856 let hessian_psi_psi = gaussian_joint_psisecondhessian_fromweights(
857 xmu,
858 x_ls,
859 xmu_i_map,
860 x_ls_i_map,
861 xmu_j_map,
862 x_ls_j_map,
863 xmu_ab_map,
864 x_ls_ab_map,
865 &weights_i,
866 &weights_j,
867 &secondweights,
868 )?;
869
870 Ok(gam_problem::ExactNewtonJointPsiSecondOrderTerms {
871 objective_psi_psi,
872 score_psi_psi,
873 hessian_psi_psi,
874 hessian_psi_psi_operator: None,
875 })
876 }
877
878 pub(crate) fn exact_newton_joint_psihessian_directional_derivative_from_designs(
879 &self,
880 block_states: &[ParameterBlockState],
881 derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
882 psi_index: usize,
883 d_beta_flat: &Array1<f64>,
884 xmu: &Array2<f64>,
885 x_ls: &Array2<f64>,
886 ) -> Result<Option<Array2<f64>>, String> {
887 let Some(dir_a) = self.exact_newton_joint_psi_direction(
888 block_states,
889 derivative_blocks,
890 psi_index,
891 xmu,
892 x_ls,
893 &self.policy,
894 )?
895 else {
896 return Ok(None);
897 };
898 Ok(Some(
899 self.exact_newton_joint_psihessian_directional_derivative_from_parts(
900 block_states,
901 &dir_a,
902 d_beta_flat,
903 xmu,
904 x_ls,
905 None,
906 )?,
907 ))
908 }
909
910 pub(crate) fn exact_newton_joint_psihessian_directional_derivative_from_parts(
911 &self,
912 block_states: &[ParameterBlockState],
913 dir_a: &LocationScaleJointPsiDirection,
914 d_beta_flat: &Array1<f64>,
915 xmu: &Array2<f64>,
916 x_ls: &Array2<f64>,
917 subsample: Option<&[crate::outer_subsample::WeightedOuterRow]>,
918 ) -> Result<Array2<f64>, String> {
919 let etamu = &block_states[Self::BLOCK_MU].eta;
920 let eta_ls = &block_states[Self::BLOCK_LOG_SIGMA].eta;
921 let pmu = xmu.ncols();
922 let p_ls = x_ls.ncols();
923 let xmu_map = dir_a.x_primary_psi.as_linear_map_ref();
924 let x_ls_map = dir_a.x_ls_psi.as_linear_map_ref();
925 let total = pmu + p_ls;
926 if d_beta_flat.len() != total {
927 return Err(GamlssError::DimensionMismatch { reason: format!(
928 "GaussianLocationScaleFamily joint psi hessian directional derivative length mismatch: got {}, expected {}",
929 d_beta_flat.len(),
930 total
931 ) }.into());
932 }
933 let u_mu = d_beta_flat.slice(s![0..pmu]);
938 let u_ls = d_beta_flat.slice(s![pmu..pmu + p_ls]);
939 let xi_mu = fast_av(xmu, &u_mu);
940 let xi_ls = fast_av(x_ls, &u_ls);
941 let uza_mu = xmu_map.forward_mul(u_mu);
942 let uza_ls = x_ls_map.forward_mul(u_ls);
943 let rows = self.get_or_compute_row_scalars(etamu, eta_ls)?;
985 let mut mixedweights = gaussian_joint_psi_mixed_driftweights(
986 &rows,
987 &xi_mu,
988 &xi_ls,
989 &dir_a.z_primary_psi,
990 &dir_a.z_ls_psi,
991 &uza_mu,
992 &uza_ls,
993 );
994 if let Some(sub_rows) = subsample {
995 apply_ht_mask_mixed(&mut mixedweights, sub_rows);
1000 }
1001
1002 gaussian_joint_psi_mixedhessian_drift_fromweights(
1003 xmu,
1004 x_ls,
1005 xmu_map,
1006 x_ls_map,
1007 &mixedweights,
1008 )
1009 }
1010
1011 pub fn block_effective_jacobian(
1018 specs: &[ParameterBlockSpec],
1019 block_idx: usize,
1020 ) -> Result<Box<dyn BlockEffectiveJacobian>, String> {
1021 crate::block_layout::block_jacobian::AdditiveWiggleBlockLayout {
1022 family: "GaussianLocationScaleFamily",
1023 n_outputs: 2,
1024 additive_blocks: &[Self::BLOCK_MU, Self::BLOCK_LOG_SIGMA],
1025 wiggle_block: None,
1026 }
1027 .block_effective_jacobian(specs, block_idx)
1028 }
1029}
1030
1031impl CustomFamily for GaussianLocationScaleFamily {
1032 fn exact_newton_joint_hessian_beta_dependent(&self) -> bool {
1044 true
1045 }
1046
1047 fn outer_seed_config(&self, n_params: usize) -> crate::seeding::SeedConfig {
1070 if n_params == 0 {
1071 return crate::seeding::SeedConfig::default();
1072 }
1073 let mut config = crate::seeding::SeedConfig::default();
1074 config.risk_profile = crate::seeding::SeedRiskProfile::GaussianLocationScale;
1075 config.max_seeds = 4;
1076 config.seed_budget = 2;
1077 config
1078 }
1079
1080 fn output_channel_assignment(&self, specs: &[ParameterBlockSpec]) -> Option<Vec<usize>> {
1087 Some(
1090 (0..specs.len())
1091 .map(|i| usize::from(i == Self::BLOCK_LOG_SIGMA))
1092 .collect(),
1093 )
1094 }
1095
1096 fn coefficient_hessian_cost(&self, specs: &[ParameterBlockSpec]) -> u64 {
1097 crate::location_scale_engine::location_scale_coefficient_hessian_cost(
1105 self.y.len() as u64,
1106 specs,
1107 )
1108 }
1109
1110 fn evaluate(&self, block_states: &[ParameterBlockState]) -> Result<FamilyEvaluation, String> {
1111 validate_block_count::<GamlssError>("GaussianLocationScaleFamily", 2, block_states.len())?;
1112 let n = self.y.len();
1113 let etamu = &block_states[Self::BLOCK_MU].eta;
1114 let eta_log_sigma = &block_states[Self::BLOCK_LOG_SIGMA].eta;
1115 if etamu.len() != n || eta_log_sigma.len() != n || self.weights.len() != n {
1116 return Err(GamlssError::DimensionMismatch {
1117 reason: "GaussianLocationScaleFamily input size mismatch".to_string(),
1118 }
1119 .into());
1120 }
1121
1122 let ln2pi = (2.0 * std::f64::consts::PI).ln();
1134 let certified: Vec<Result<GaussianDiagonalRowKernel, String>> = (0..n)
1135 .into_par_iter()
1136 .map(|i| {
1137 gaussian_diagonal_row_kernel(
1138 i,
1139 self.y[i],
1140 etamu[i],
1141 eta_log_sigma[i],
1142 self.weights[i],
1143 ln2pi,
1144 )
1145 })
1146 .collect();
1147 let mut rows = Vec::with_capacity(n);
1148 for row in certified {
1149 rows.push(row?);
1150 }
1151 let mut ll = 0.0;
1152 for (i, row) in rows.iter().enumerate() {
1153 ll += row.log_likelihood;
1154 if !ll.is_finite() {
1155 return Err(GamlssError::RowGeometryUnrepresentable {
1156 row: i,
1157 quantity: "Gaussian cumulative log likelihood",
1158 eta: eta_log_sigma[i],
1159 value: ll,
1160 }
1161 .into());
1162 }
1163 }
1164 let zmu = self.y.clone();
1165 let wmu = Array1::from_iter(rows.iter().map(|row| row.location_working_weight));
1166 let z_ls = Array1::from_iter(rows.iter().map(|row| row.log_sigma_working_response));
1167 let w_ls = Array1::from_iter(rows.iter().map(|row| row.log_sigma_working_weight));
1168
1169 Ok(FamilyEvaluation {
1170 log_likelihood: ll,
1171 blockworking_sets: vec![
1172 BlockWorkingSet::diagonal_checked(zmu, wmu)?,
1173 BlockWorkingSet::diagonal_checked(z_ls, w_ls)?,
1174 ],
1175 })
1176 }
1177
1178 fn log_likelihood_only(&self, block_states: &[ParameterBlockState]) -> Result<f64, String> {
1179 validate_block_count::<GamlssError>("GaussianLocationScaleFamily", 2, block_states.len())?;
1180 let n = self.y.len();
1181 let etamu = &block_states[Self::BLOCK_MU].eta;
1182 let eta_log_sigma = &block_states[Self::BLOCK_LOG_SIGMA].eta;
1183 if etamu.len() != n || eta_log_sigma.len() != n || self.weights.len() != n {
1184 return Err(GamlssError::DimensionMismatch {
1185 reason: "GaussianLocationScaleFamily input size mismatch".to_string(),
1186 }
1187 .into());
1188 }
1189 let ln2pi = (2.0 * std::f64::consts::PI).ln();
1190 let mut ll = 0.0;
1191 for i in 0..n {
1192 ll += gaussian_diagonal_row_kernel(
1193 i,
1194 self.y[i],
1195 etamu[i],
1196 eta_log_sigma[i],
1197 self.weights[i],
1198 ln2pi,
1199 )?
1200 .log_likelihood;
1201 if !ll.is_finite() {
1202 return Err(GamlssError::RowGeometryUnrepresentable {
1203 row: i,
1204 quantity: "Gaussian cumulative log likelihood",
1205 eta: eta_log_sigma[i],
1206 value: ll,
1207 }
1208 .into());
1209 }
1210 }
1211 Ok(ll)
1212 }
1213
1214 fn log_likelihood_only_with_options(
1225 &self,
1226 block_states: &[ParameterBlockState],
1227 options: &BlockwiseFitOptions,
1228 ) -> Result<f64, String> {
1229 let Some(subsample) = options.outer_score_subsample.as_ref() else {
1230 return self.log_likelihood_only(block_states);
1231 };
1232 validate_block_count::<GamlssError>("GaussianLocationScaleFamily", 2, block_states.len())?;
1233 let n = self.y.len();
1234 let etamu = &block_states[Self::BLOCK_MU].eta;
1235 let eta_log_sigma = &block_states[Self::BLOCK_LOG_SIGMA].eta;
1236 if etamu.len() != n || eta_log_sigma.len() != n || self.weights.len() != n {
1237 return Err(GamlssError::DimensionMismatch {
1238 reason: "GaussianLocationScaleFamily input size mismatch".to_string(),
1239 }
1240 .into());
1241 }
1242 let ln2pi = (2.0 * std::f64::consts::PI).ln();
1243 let mut ll = 0.0;
1244 for sampled in subsample.rows.iter() {
1245 let i = sampled.index;
1246 let row_ll = gaussian_diagonal_row_kernel(
1247 i,
1248 self.y[i],
1249 etamu[i],
1250 eta_log_sigma[i],
1251 self.weights[i],
1252 ln2pi,
1253 )?
1254 .log_likelihood;
1255 let contribution = scaled_signed_product3(sampled.weight, row_ll, 1.0);
1256 ll += contribution;
1257 if !contribution.is_finite() || !ll.is_finite() {
1258 return Err(GamlssError::RowGeometryUnrepresentable {
1259 row: i,
1260 quantity: "Gaussian subsampled log likelihood",
1261 eta: eta_log_sigma[i],
1262 value: if contribution.is_finite() {
1263 ll
1264 } else {
1265 contribution
1266 },
1267 }
1268 .into());
1269 }
1270 }
1271 Ok(ll)
1272 }
1273
1274 fn exact_newton_joint_hessian(
1275 &self,
1276 block_states: &[ParameterBlockState],
1277 ) -> Result<Option<Array2<f64>>, String> {
1278 self.exact_newton_joint_hessian_for_specs(block_states, None)
1279 }
1280
1281 fn exact_newton_joint_gradient_evaluation(
1282 &self,
1283 block_states: &[ParameterBlockState],
1284 specs: &[ParameterBlockSpec],
1285 ) -> Result<Option<ExactNewtonJointGradientEvaluation>, String> {
1286 self.exact_newton_joint_gradient_for_specs(block_states, Some(specs))
1287 }
1288
1289 fn has_explicit_joint_hessian(&self) -> bool {
1290 true
1291 }
1292
1293 fn joint_jeffreys_term_required(&self) -> bool {
1309 false
1310 }
1311
1312 fn exact_newton_joint_hessian_directional_derivative(
1313 &self,
1314 block_states: &[ParameterBlockState],
1315 d_beta_flat: &Array1<f64>,
1316 ) -> Result<Option<Array2<f64>>, String> {
1317 self.exact_newton_joint_hessian_directional_derivative_for_specs(
1318 block_states,
1319 None,
1320 d_beta_flat,
1321 )
1322 }
1323
1324 fn exact_newton_joint_hessiansecond_directional_derivative(
1325 &self,
1326 block_states: &[ParameterBlockState],
1327 d_beta_u_flat: &Array1<f64>,
1328 d_betav_flat: &Array1<f64>,
1329 ) -> Result<Option<Array2<f64>>, String> {
1330 self.exact_newton_joint_hessian_second_directional_derivative_for_specs(
1331 block_states,
1332 None,
1333 d_beta_u_flat,
1334 d_betav_flat,
1335 )
1336 }
1337
1338 fn diagonalworking_weights_directional_derivative(
1339 &self,
1340 block_states: &[ParameterBlockState],
1341 block_idx: usize,
1342 d_eta: &Array1<f64>,
1343 ) -> Result<Option<Array1<f64>>, String> {
1344 validate_block_count::<GamlssError>("GaussianLocationScaleFamily", 2, block_states.len())?;
1345 let n = self.y.len();
1346 let eta_t = &block_states[Self::BLOCK_MU].eta;
1347 let eta_ls = &block_states[Self::BLOCK_LOG_SIGMA].eta;
1348 if eta_t.len() != n || eta_ls.len() != n || self.weights.len() != n || d_eta.len() != n {
1349 return Err(GamlssError::DimensionMismatch {
1350 reason: "GaussianLocationScaleFamily input size mismatch".to_string(),
1351 }
1352 .into());
1353 }
1354
1355 let sigma = eta_ls.mapv(logb_sigma_from_eta_scalar);
1356 let mut dw = Array1::<f64>::zeros(n);
1357 match block_idx {
1358 Self::BLOCK_MU => {
1359 Ok(Some(dw))
1367 }
1368 Self::BLOCK_LOG_SIGMA => {
1369 use rayon::iter::{IntoParallelIterator, ParallelIterator};
1384 let dw_vec: Vec<Result<f64, String>> = (0..n)
1385 .into_par_iter()
1386 .map(|i| {
1387 let d1 = crate::sigma_link::logb_sigma_jet1_scalar(eta_ls[i]).d1;
1388 gaussian_log_sigma_irlsinfo_directional_derivative(
1389 i,
1390 eta_ls[i],
1391 self.weights[i],
1392 sigma[i],
1393 d1,
1394 d_eta[i],
1395 )
1396 })
1397 .collect();
1398 for (i, v) in dw_vec.into_iter().enumerate() {
1399 dw[i] = v?;
1400 }
1401 Ok(Some(dw))
1402 }
1403 _ => Ok(None),
1404 }
1405 }
1406
1407 fn exact_newton_joint_hessian_with_specs(
1408 &self,
1409 block_states: &[ParameterBlockState],
1410 specs: &[ParameterBlockSpec],
1411 ) -> Result<Option<Array2<f64>>, String> {
1412 self.exact_newton_joint_hessian_for_specs(block_states, Some(specs))
1413 }
1414
1415 fn exact_newton_joint_hessian_directional_derivative_with_specs(
1416 &self,
1417 block_states: &[ParameterBlockState],
1418 specs: &[ParameterBlockSpec],
1419 d_beta_flat: &Array1<f64>,
1420 ) -> Result<Option<Array2<f64>>, String> {
1421 self.exact_newton_joint_hessian_directional_derivative_for_specs(
1422 block_states,
1423 Some(specs),
1424 d_beta_flat,
1425 )
1426 }
1427
1428 fn exact_newton_joint_hessian_second_directional_derivative_with_specs(
1429 &self,
1430 block_states: &[ParameterBlockState],
1431 specs: &[ParameterBlockSpec],
1432 d_beta_u_flat: &Array1<f64>,
1433 d_betav_flat: &Array1<f64>,
1434 ) -> Result<Option<Array2<f64>>, String> {
1435 self.exact_newton_joint_hessian_second_directional_derivative_for_specs(
1436 block_states,
1437 Some(specs),
1438 d_beta_u_flat,
1439 d_betav_flat,
1440 )
1441 }
1442
1443 fn exact_newton_joint_psi_terms(
1444 &self,
1445 block_states: &[ParameterBlockState],
1446 specs: &[ParameterBlockSpec],
1447 hyper_layout: &crate::custom_family::CustomFamilyHyperLayout,
1448 psi_index: usize,
1449 ) -> Result<Option<gam_problem::ExactNewtonJointPsiTerms>, String> {
1450 if hyper_layout.family_axis_count() != 0 {
1451 return Err("GaussianLocationScaleFamily does not declare family-owned hyper axes"
1452 .to_string());
1453 }
1454 self.exact_newton_joint_psi_terms_for_specs(
1455 block_states,
1456 specs,
1457 hyper_layout,
1458 psi_index,
1459 )
1460 }
1461
1462 fn exact_newton_joint_psisecond_order_terms(
1463 &self,
1464 block_states: &[ParameterBlockState],
1465 specs: &[ParameterBlockSpec],
1466 hyper_layout: &crate::custom_family::CustomFamilyHyperLayout,
1467 psi_i: usize,
1468 psi_j: usize,
1469 ) -> Result<Option<gam_problem::ExactNewtonJointPsiSecondOrderTerms>, String> {
1470 if hyper_layout.family_axis_count() != 0 {
1471 return Err("GaussianLocationScaleFamily does not declare family-owned hyper axes"
1472 .to_string());
1473 }
1474 self.exact_newton_joint_psisecond_order_terms_for_specs(
1475 block_states,
1476 specs,
1477 hyper_layout,
1478 psi_i,
1479 psi_j,
1480 )
1481 }
1482
1483 fn exact_newton_joint_psihessian_directional_derivative(
1484 &self,
1485 block_states: &[ParameterBlockState],
1486 specs: &[ParameterBlockSpec],
1487 hyper_layout: &crate::custom_family::CustomFamilyHyperLayout,
1488 psi_index: usize,
1489 d_beta_flat: &Array1<f64>,
1490 ) -> Result<Option<Array2<f64>>, String> {
1491 if hyper_layout.family_axis_count() != 0 {
1492 return Err("GaussianLocationScaleFamily does not declare family-owned hyper axes"
1493 .to_string());
1494 }
1495 self.exact_newton_joint_psihessian_directional_derivative_for_specs(
1496 block_states,
1497 specs,
1498 hyper_layout,
1499 psi_index,
1500 d_beta_flat,
1501 )
1502 }
1503
1504 fn exact_newton_joint_psi_workspace(
1505 &self,
1506 block_states: &[ParameterBlockState],
1507 specs: &[ParameterBlockSpec],
1508 hyper_layout: &crate::custom_family::CustomFamilyHyperLayout,
1509 ) -> Result<Option<Arc<dyn ExactNewtonJointPsiWorkspace>>, String> {
1510 if hyper_layout.family_axis_count() != 0 {
1511 return Err("GaussianLocationScaleFamily does not declare family-owned hyper axes"
1512 .to_string());
1513 }
1514 let derivative_blocks = hyper_layout.design_derivative_blocks();
1515 validate_block_count::<GamlssError>("GaussianLocationScaleFamily", 2, block_states.len())?;
1516 if specs.len() != 2 || derivative_blocks.len() != 2 {
1517 return Err(GamlssError::DimensionMismatch { reason: format!(
1518 "GaussianLocationScaleFamily joint psi workspace expects 2 specs and 2 derivative block lists, got {} / {}",
1519 specs.len(),
1520 derivative_blocks.len()
1521 ) }.into());
1522 }
1523 Ok(Some(Arc::new(
1524 GaussianLocationScaleExactNewtonJointPsiWorkspace::new(
1525 self.clone(),
1526 block_states.to_vec(),
1527 specs,
1528 derivative_blocks.to_vec(),
1529 )?,
1530 )))
1531 }
1532
1533 fn exact_newton_joint_psi_workspace_with_options(
1551 &self,
1552 block_states: &[ParameterBlockState],
1553 specs: &[ParameterBlockSpec],
1554 hyper_layout: &crate::custom_family::CustomFamilyHyperLayout,
1555 options: &BlockwiseFitOptions,
1556 ) -> Result<Option<Arc<dyn ExactNewtonJointPsiWorkspace>>, String> {
1557 if hyper_layout.family_axis_count() != 0 {
1558 return Err("GaussianLocationScaleFamily does not declare family-owned hyper axes"
1559 .to_string());
1560 }
1561 let derivative_blocks = hyper_layout.design_derivative_blocks();
1562 validate_block_count::<GamlssError>("GaussianLocationScaleFamily", 2, block_states.len())?;
1563 if specs.len() != 2 || derivative_blocks.len() != 2 {
1564 return Err(GamlssError::DimensionMismatch { reason: format!(
1565 "GaussianLocationScaleFamily joint psi workspace expects 2 specs and 2 derivative block lists, got {} / {}",
1566 specs.len(),
1567 derivative_blocks.len()
1568 ) }.into());
1569 }
1570 Ok(Some(Arc::new(
1571 GaussianLocationScaleExactNewtonJointPsiWorkspace::new_with_subsample(
1572 self.clone(),
1573 block_states.to_vec(),
1574 specs,
1575 derivative_blocks.to_vec(),
1576 options.outer_score_subsample.clone(),
1577 )?,
1578 )))
1579 }
1580
1581 fn exact_newton_joint_hessian_workspace(
1582 &self,
1583 block_states: &[ParameterBlockState],
1584 specs: &[ParameterBlockSpec],
1585 ) -> Result<Option<Arc<dyn ExactNewtonJointHessianWorkspace>>, String> {
1586 let Some((xmu, x_ls)) = self.exact_joint_dense_block_designs(Some(specs))? else {
1587 return Ok(None);
1588 };
1589 let workspace = GaussianLocationScaleHessianWorkspace::new(
1590 self.clone(),
1591 block_states.to_vec(),
1592 xmu.into_owned(),
1593 x_ls.into_owned(),
1594 )?;
1595 Ok(Some(Arc::new(workspace)))
1596 }
1597
1598 fn exact_newton_joint_hessian_workspace_with_options(
1612 &self,
1613 block_states: &[ParameterBlockState],
1614 specs: &[ParameterBlockSpec],
1615 options: &BlockwiseFitOptions,
1616 ) -> Result<Option<Arc<dyn ExactNewtonJointHessianWorkspace>>, String> {
1617 let Some((xmu, x_ls)) = self.exact_joint_dense_block_designs(Some(specs))? else {
1618 return Ok(None);
1619 };
1620 let mut workspace = GaussianLocationScaleHessianWorkspace::new(
1621 self.clone(),
1622 block_states.to_vec(),
1623 xmu.into_owned(),
1624 x_ls.into_owned(),
1625 )?;
1626 if let Some(subsample) = options.outer_score_subsample.as_ref() {
1627 workspace.apply_outer_subsample(subsample.rows.as_ref());
1628 }
1629 Ok(Some(Arc::new(workspace)))
1630 }
1631
1632 fn inner_coefficient_hessian_hvp_available(&self, specs: &[ParameterBlockSpec]) -> bool {
1633 self.exact_joint_supported()
1639 && matches!(
1640 self.exact_joint_dense_block_designs(Some(specs)),
1641 Ok(Some(_))
1642 )
1643 }
1644
1645 fn outer_derivative_subsample_capable(&self) -> bool {
1667 true
1668 }
1669}
1670
1671impl CustomFamilyGenerative for GaussianLocationScaleFamily {
1672 fn generativespec(
1673 &self,
1674 block_states: &[ParameterBlockState],
1675 ) -> Result<GenerativeSpec, String> {
1676 validate_block_count::<GamlssError>("GaussianLocationScaleFamily", 2, block_states.len())?;
1677 let mu = block_states[Self::BLOCK_MU].eta.clone();
1678 let eta_log_sigma = &block_states[Self::BLOCK_LOG_SIGMA].eta;
1679 let sigma = gamlss_rowwise_map(eta_log_sigma.len(), |i| {
1680 logb_sigma_from_eta_scalar(eta_log_sigma[i])
1681 });
1682 Ok(GenerativeSpec {
1683 mean: mu,
1684 noise: NoiseModel::Gaussian { sigma },
1685 })
1686 }
1687}