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_hessian_from_designs(
399 &self,
400 block_states: &[ParameterBlockState],
401 xmu: &DenseOrOperator<'_>,
402 x_ls: &DenseOrOperator<'_>,
403 ) -> Result<Option<Array2<f64>>, String> {
404 validate_block_count::<GamlssError>("GaussianLocationScaleFamily", 2, block_states.len())?;
405 let n = self.y.len();
406 let etamu = &block_states[Self::BLOCK_MU].eta;
407 let eta_ls = &block_states[Self::BLOCK_LOG_SIGMA].eta;
408 if etamu.len() != n || eta_ls.len() != n || self.weights.len() != n {
409 return Err(GamlssError::DimensionMismatch {
410 reason: "GaussianLocationScaleFamily input size mismatch".to_string(),
411 }
412 .into());
413 }
414
415 let rows = self.get_or_compute_row_scalars(etamu, eta_ls)?;
416 let (mm, cross, scale) = gaussian_locscale_observed_joint_row_coeffs(&rows);
421 Ok(Some(gaussian_joint_hessian_from_designs(
422 xmu, x_ls, &mm, &cross, &scale,
423 )?))
424 }
425
426 pub(crate) fn exact_newton_joint_hessian_directional_derivative_from_designs(
427 &self,
428 block_states: &[ParameterBlockState],
429 xmu: &DenseOrOperator<'_>,
430 x_ls: &DenseOrOperator<'_>,
431 d_beta_flat: &Array1<f64>,
432 ) -> Result<Option<Array2<f64>>, String> {
433 validate_block_count::<GamlssError>("GaussianLocationScaleFamily", 2, block_states.len())?;
434 let n = self.y.len();
435 let etamu = &block_states[Self::BLOCK_MU].eta;
436 let eta_ls = &block_states[Self::BLOCK_LOG_SIGMA].eta;
437 if etamu.len() != n || eta_ls.len() != n || self.weights.len() != n {
438 return Err(GamlssError::DimensionMismatch {
439 reason: "GaussianLocationScaleFamily input size mismatch".to_string(),
440 }
441 .into());
442 }
443
444 let pmu = xmu.ncols();
445 let p_ls = x_ls.ncols();
446 let total = pmu + p_ls;
447 if d_beta_flat.len() != total {
448 return Err(GamlssError::DimensionMismatch {
449 reason: format!(
450 "GaussianLocationScaleFamily joint d_beta length mismatch: got {}, expected {}",
451 d_beta_flat.len(),
452 total
453 ),
454 }
455 .into());
456 }
457 let ximu = xmu.dot(d_beta_flat.slice(s![0..pmu]));
458 let xi_ls = x_ls.dot(d_beta_flat.slice(s![pmu..pmu + p_ls]));
459 let rows = self.get_or_compute_row_scalars(etamu, eta_ls)?;
460 let directional = gaussian_joint_first_directionalweights(&rows, &ximu, &xi_ls);
461 let dhmumu = directional.0;
462 let dh_ls_ls = directional.2;
463 let dhmu_ls = directional.1;
471
472 Ok(Some(gaussian_joint_hessian_from_designs(
473 xmu, x_ls, &dhmumu, &dhmu_ls, &dh_ls_ls,
474 )?))
475 }
476
477 pub(crate) fn exact_newton_joint_hessiansecond_directional_derivative_from_designs(
478 &self,
479 block_states: &[ParameterBlockState],
480 xmu: &DenseOrOperator<'_>,
481 x_ls: &DenseOrOperator<'_>,
482 d_beta_u_flat: &Array1<f64>,
483 d_betav_flat: &Array1<f64>,
484 ) -> Result<Option<Array2<f64>>, String> {
485 validate_block_count::<GamlssError>("GaussianLocationScaleFamily", 2, block_states.len())?;
486 let n = self.y.len();
487 let etamu = &block_states[Self::BLOCK_MU].eta;
488 let eta_ls = &block_states[Self::BLOCK_LOG_SIGMA].eta;
489 if etamu.len() != n || eta_ls.len() != n || self.weights.len() != n {
490 return Err(GamlssError::DimensionMismatch {
491 reason: "GaussianLocationScaleFamily input size mismatch".to_string(),
492 }
493 .into());
494 }
495
496 let pmu = xmu.ncols();
497 let p_ls = x_ls.ncols();
498 let total = pmu + p_ls;
499 if d_beta_u_flat.len() != total || d_betav_flat.len() != total {
500 return Err(GamlssError::DimensionMismatch { reason: format!(
501 "GaussianLocationScaleFamily joint second directional derivative length mismatch: got {} and {}, expected {}",
502 d_beta_u_flat.len(),
503 d_betav_flat.len(),
504 total
505 ) }.into());
506 }
507 let ximu_u = xmu.dot(d_beta_u_flat.slice(s![0..pmu]));
508 let xi_ls_u = x_ls.dot(d_beta_u_flat.slice(s![pmu..pmu + p_ls]));
509 let ximuv = xmu.dot(d_betav_flat.slice(s![0..pmu]));
510 let xi_lsv = x_ls.dot(d_betav_flat.slice(s![pmu..pmu + p_ls]));
511 let rows = self.get_or_compute_row_scalars(etamu, eta_ls)?;
512 let second =
513 gaussian_jointsecond_directionalweights(&rows, &ximu_u, &xi_ls_u, &ximuv, &xi_lsv);
514 let d2hmumu = second.0;
515 let d2h_ls_ls = second.2;
516 let d2hmu_ls = second.1;
522
523 Ok(Some(gaussian_joint_hessian_from_designs(
524 xmu, x_ls, &d2hmumu, &d2hmu_ls, &d2h_ls_ls,
525 )?))
526 }
527
528 pub(crate) fn exact_newton_joint_psi_terms_from_designs(
529 &self,
530 block_states: &[ParameterBlockState],
531 specs: &[ParameterBlockSpec],
532 derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
533 psi_index: usize,
534 xmu: &Array2<f64>,
535 x_ls: &Array2<f64>,
536 ) -> Result<Option<gam_problem::ExactNewtonJointPsiTerms>, String> {
537 validate_block_count::<GamlssError>("GaussianLocationScaleFamily", 2, block_states.len())?;
538 if specs.len() != 2 || derivative_blocks.len() != 2 {
539 return Err(GamlssError::DimensionMismatch { reason: format!(
540 "GaussianLocationScaleFamily joint psi terms expect 2 specs and 2 derivative blocks, got {} and {}",
541 specs.len(),
542 derivative_blocks.len()
543 ) }.into());
544 }
545 let Some(dir_a) = self.exact_newton_joint_psi_direction(
546 block_states,
547 derivative_blocks,
548 psi_index,
549 xmu,
550 x_ls,
551 &self.policy,
552 )?
553 else {
554 return Ok(None);
555 };
556 let etamu = &block_states[Self::BLOCK_MU].eta;
588 let eta_ls = &block_states[Self::BLOCK_LOG_SIGMA].eta;
589 let rows = self.get_or_compute_row_scalars(etamu, eta_ls)?;
590 let weights_a =
591 gaussian_joint_psi_firstweights(&rows, &dir_a.z_primary_psi, &dir_a.z_ls_psi);
592 let objective_psi = weights_a.objective_psirow.sum();
593 let xmu_map = dir_a.x_primary_psi.as_linear_map_ref();
594 let x_ls_map = dir_a.x_ls_psi.as_linear_map_ref();
595 let score_mu =
596 xmu_map.transpose_mul(weights_a.scoremu.view()) + fast_atv(xmu, &weights_a.dscoremu);
597 let score_ls = x_ls_map.transpose_mul(weights_a.score_ls.view())
598 + fast_atv(x_ls, &weights_a.dscore_ls);
599 let score_psi = gaussian_pack_joint_score(&score_mu, &score_ls);
600 let hessian_psi_operator = build_two_block_custom_family_joint_psi_operator_from_actions(
601 dir_a.x_primary_psi.cloned_first_action(),
602 dir_a.x_ls_psi.cloned_first_action(),
603 0..xmu.ncols(),
604 xmu.ncols()..xmu.ncols() + x_ls.ncols(),
605 xmu,
606 x_ls,
607 &weights_a.hmumu,
608 &weights_a.hmu_ls,
609 &weights_a.h_ls_ls,
610 &weights_a.dhmumu,
611 &weights_a.dhmu_ls,
612 &weights_a.dh_ls_ls,
613 )?;
614 let hessian_psi = if hessian_psi_operator.is_some() {
615 Array2::zeros((0, 0))
616 } else {
617 gaussian_joint_psihessian_fromweights(xmu, x_ls, xmu_map, x_ls_map, &weights_a)?
618 };
619
620 Ok(Some(gam_problem::ExactNewtonJointPsiTerms {
621 objective_psi,
622 score_psi,
623 hessian_psi,
624 hessian_psi_operator,
625 }))
626 }
627
628 pub(crate) fn exact_newton_joint_psisecond_order_terms_from_designs(
629 &self,
630 block_states: &[ParameterBlockState],
631 derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
632 psi_i: usize,
633 psi_j: usize,
634 xmu: &Array2<f64>,
635 x_ls: &Array2<f64>,
636 ) -> Result<Option<gam_problem::ExactNewtonJointPsiSecondOrderTerms>, String> {
637 let Some(dir_i) = self.exact_newton_joint_psi_direction(
638 block_states,
639 derivative_blocks,
640 psi_i,
641 xmu,
642 x_ls,
643 &self.policy,
644 )?
645 else {
646 return Ok(None);
647 };
648 let Some(dir_j) = self.exact_newton_joint_psi_direction(
649 block_states,
650 derivative_blocks,
651 psi_j,
652 xmu,
653 x_ls,
654 &self.policy,
655 )?
656 else {
657 return Ok(None);
658 };
659 Ok(Some(
660 self.exact_newton_joint_psisecond_order_terms_from_parts(
661 block_states,
662 derivative_blocks,
663 &dir_i,
664 &dir_j,
665 xmu,
666 x_ls,
667 None,
668 )?,
669 ))
670 }
671
672 pub(crate) fn exact_newton_joint_psisecond_order_terms_from_parts(
673 &self,
674 block_states: &[ParameterBlockState],
675 derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
676 dir_i: &LocationScaleJointPsiDirection,
677 dir_j: &LocationScaleJointPsiDirection,
678 xmu: &Array2<f64>,
679 x_ls: &Array2<f64>,
680 subsample: Option<&[crate::outer_subsample::WeightedOuterRow]>,
681 ) -> Result<gam_problem::ExactNewtonJointPsiSecondOrderTerms, String> {
682 let second_drifts = self.exact_newton_joint_psisecond_design_drifts(
683 block_states,
684 derivative_blocks,
685 dir_i,
686 dir_j,
687 xmu,
688 x_ls,
689 )?;
690 let n = self.y.len();
691 let xmu_i_map = dir_i.x_primary_psi.as_linear_map_ref();
692 let x_ls_i_map = dir_i.x_ls_psi.as_linear_map_ref();
693 let xmu_j_map = dir_j.x_primary_psi.as_linear_map_ref();
694 let x_ls_j_map = dir_j.x_ls_psi.as_linear_map_ref();
695 let xmu_ab_map = second_psi_linear_map(
696 second_drifts.x_primary_ab_action.as_ref(),
697 second_drifts.x_primary_ab.as_ref(),
698 n,
699 xmu.ncols(),
700 );
701 let x_ls_ab_map = second_psi_linear_map(
702 second_drifts.x_ls_ab_action.as_ref(),
703 second_drifts.x_ls_ab.as_ref(),
704 n,
705 x_ls.ncols(),
706 );
707 let etamu = &block_states[Self::BLOCK_MU].eta;
731 let eta_ls = &block_states[Self::BLOCK_LOG_SIGMA].eta;
732 let rows = self.get_or_compute_row_scalars(etamu, eta_ls)?;
733 let mut weights_i =
734 gaussian_joint_psi_firstweights(&rows, &dir_i.z_primary_psi, &dir_i.z_ls_psi);
735 let mut weights_j =
736 gaussian_joint_psi_firstweights(&rows, &dir_j.z_primary_psi, &dir_j.z_ls_psi);
737 let mut secondweights = gaussian_joint_psisecondweights(
738 &rows,
739 &dir_i.z_primary_psi,
740 &dir_i.z_ls_psi,
741 &dir_j.z_primary_psi,
742 &dir_j.z_ls_psi,
743 &second_drifts.z_primary_ab,
744 &second_drifts.z_ls_ab,
745 );
746 if let Some(sub_rows) = subsample {
747 apply_ht_mask_first(&mut weights_i, sub_rows);
753 apply_ht_mask_first(&mut weights_j, sub_rows);
754 apply_ht_mask_second(&mut secondweights, sub_rows);
755 }
756 let objective_psi_psi = secondweights.objective_psi_psirow.sum();
757
758 let score_psi_psi = gaussian_pack_joint_score(
759 &(xmu_ab_map.transpose_mul(weights_i.scoremu.view())
760 + xmu_i_map.transpose_mul(weights_j.dscoremu.view())
761 + xmu_j_map.transpose_mul(weights_i.dscoremu.view())
762 + fast_atv(xmu, &secondweights.d2scoremu)),
763 &(x_ls_ab_map.transpose_mul(weights_i.score_ls.view())
764 + x_ls_i_map.transpose_mul(weights_j.dscore_ls.view())
765 + x_ls_j_map.transpose_mul(weights_i.dscore_ls.view())
766 + fast_atv(x_ls, &secondweights.d2score_ls)),
767 );
768 let hessian_psi_psi = gaussian_joint_psisecondhessian_fromweights(
769 xmu,
770 x_ls,
771 xmu_i_map,
772 x_ls_i_map,
773 xmu_j_map,
774 x_ls_j_map,
775 xmu_ab_map,
776 x_ls_ab_map,
777 &weights_i,
778 &weights_j,
779 &secondweights,
780 )?;
781
782 Ok(gam_problem::ExactNewtonJointPsiSecondOrderTerms {
783 objective_psi_psi,
784 score_psi_psi,
785 hessian_psi_psi,
786 hessian_psi_psi_operator: None,
787 })
788 }
789
790 pub(crate) fn exact_newton_joint_psihessian_directional_derivative_from_parts(
791 &self,
792 block_states: &[ParameterBlockState],
793 dir_a: &LocationScaleJointPsiDirection,
794 d_beta_flat: &Array1<f64>,
795 xmu: &Array2<f64>,
796 x_ls: &Array2<f64>,
797 subsample: Option<&[crate::outer_subsample::WeightedOuterRow]>,
798 ) -> Result<Array2<f64>, String> {
799 let etamu = &block_states[Self::BLOCK_MU].eta;
800 let eta_ls = &block_states[Self::BLOCK_LOG_SIGMA].eta;
801 let pmu = xmu.ncols();
802 let p_ls = x_ls.ncols();
803 let xmu_map = dir_a.x_primary_psi.as_linear_map_ref();
804 let x_ls_map = dir_a.x_ls_psi.as_linear_map_ref();
805 let total = pmu + p_ls;
806 if d_beta_flat.len() != total {
807 return Err(GamlssError::DimensionMismatch { reason: format!(
808 "GaussianLocationScaleFamily joint psi hessian directional derivative length mismatch: got {}, expected {}",
809 d_beta_flat.len(),
810 total
811 ) }.into());
812 }
813 let u_mu = d_beta_flat.slice(s![0..pmu]);
818 let u_ls = d_beta_flat.slice(s![pmu..pmu + p_ls]);
819 let xi_mu = fast_av(xmu, &u_mu);
820 let xi_ls = fast_av(x_ls, &u_ls);
821 let uza_mu = xmu_map.forward_mul(u_mu);
822 let uza_ls = x_ls_map.forward_mul(u_ls);
823 let rows = self.get_or_compute_row_scalars(etamu, eta_ls)?;
865 let mut mixedweights = gaussian_joint_psi_mixed_driftweights(
866 &rows,
867 &xi_mu,
868 &xi_ls,
869 &dir_a.z_primary_psi,
870 &dir_a.z_ls_psi,
871 &uza_mu,
872 &uza_ls,
873 );
874 if let Some(sub_rows) = subsample {
875 apply_ht_mask_mixed(&mut mixedweights, sub_rows);
880 }
881
882 gaussian_joint_psi_mixedhessian_drift_fromweights(
883 xmu,
884 x_ls,
885 xmu_map,
886 x_ls_map,
887 &mixedweights,
888 )
889 }
890
891 pub fn block_effective_jacobian(
898 specs: &[ParameterBlockSpec],
899 block_idx: usize,
900 ) -> Result<Box<dyn BlockEffectiveJacobian>, String> {
901 crate::block_layout::block_jacobian::AdditiveWiggleBlockLayout {
902 family: "GaussianLocationScaleFamily",
903 n_outputs: 2,
904 additive_blocks: &[Self::BLOCK_MU, Self::BLOCK_LOG_SIGMA],
905 wiggle_block: None,
906 }
907 .block_effective_jacobian(specs, block_idx)
908 }
909}
910
911impl CustomFamily for GaussianLocationScaleFamily {
912 fn exact_newton_joint_hessian_beta_dependent(&self) -> bool {
924 true
925 }
926
927 fn outer_seed_config(&self, n_params: usize) -> crate::seeding::SeedConfig {
950 if n_params == 0 {
951 return crate::seeding::SeedConfig::default();
952 }
953 let mut config = crate::seeding::SeedConfig::default();
954 config.risk_profile = crate::seeding::SeedRiskProfile::GaussianLocationScale;
955 config.max_seeds = 4;
956 config.seed_budget = 2;
957 config
958 }
959
960 fn output_channel_assignment(&self, specs: &[ParameterBlockSpec]) -> Option<Vec<usize>> {
967 Some(
970 (0..specs.len())
971 .map(|i| usize::from(i == Self::BLOCK_LOG_SIGMA))
972 .collect(),
973 )
974 }
975
976 fn coefficient_hessian_cost(&self, specs: &[ParameterBlockSpec]) -> u64 {
977 crate::location_scale_engine::location_scale_coefficient_hessian_cost(
985 self.y.len() as u64,
986 specs,
987 )
988 }
989
990 fn evaluate(&self, block_states: &[ParameterBlockState]) -> Result<FamilyEvaluation, String> {
991 validate_block_count::<GamlssError>("GaussianLocationScaleFamily", 2, block_states.len())?;
992 let n = self.y.len();
993 let etamu = &block_states[Self::BLOCK_MU].eta;
994 let eta_log_sigma = &block_states[Self::BLOCK_LOG_SIGMA].eta;
995 if etamu.len() != n || eta_log_sigma.len() != n || self.weights.len() != n {
996 return Err(GamlssError::DimensionMismatch {
997 reason: "GaussianLocationScaleFamily input size mismatch".to_string(),
998 }
999 .into());
1000 }
1001
1002 let ln2pi = (2.0 * std::f64::consts::PI).ln();
1014 let certified: Vec<Result<GaussianDiagonalRowKernel, String>> = (0..n)
1015 .into_par_iter()
1016 .map(|i| {
1017 gaussian_diagonal_row_kernel(
1018 i,
1019 self.y[i],
1020 etamu[i],
1021 eta_log_sigma[i],
1022 self.weights[i],
1023 ln2pi,
1024 )
1025 })
1026 .collect();
1027 let mut rows = Vec::with_capacity(n);
1028 for row in certified {
1029 rows.push(row?);
1030 }
1031 let mut ll = 0.0;
1032 for (i, row) in rows.iter().enumerate() {
1033 ll += row.log_likelihood;
1034 if !ll.is_finite() {
1035 return Err(GamlssError::RowGeometryUnrepresentable {
1036 row: i,
1037 quantity: "Gaussian cumulative log likelihood",
1038 eta: eta_log_sigma[i],
1039 value: ll,
1040 }
1041 .into());
1042 }
1043 }
1044 let zmu = Array1::from_iter(rows.iter().map(|row| row.location_working_response));
1050 let wmu = Array1::from_iter(rows.iter().map(|row| row.location_working_weight));
1051 let z_ls = Array1::from_iter(rows.iter().map(|row| row.log_sigma_working_response));
1052 let w_ls = Array1::from_iter(rows.iter().map(|row| row.log_sigma_working_weight));
1053
1054 Ok(FamilyEvaluation {
1055 log_likelihood: ll,
1056 blockworking_sets: vec![
1057 BlockWorkingSet::diagonal_checked(zmu, wmu)?,
1058 BlockWorkingSet::diagonal_checked(z_ls, w_ls)?,
1059 ],
1060 })
1061 }
1062
1063 fn log_likelihood_only(&self, block_states: &[ParameterBlockState]) -> Result<f64, String> {
1064 validate_block_count::<GamlssError>("GaussianLocationScaleFamily", 2, block_states.len())?;
1065 let n = self.y.len();
1066 let etamu = &block_states[Self::BLOCK_MU].eta;
1067 let eta_log_sigma = &block_states[Self::BLOCK_LOG_SIGMA].eta;
1068 if etamu.len() != n || eta_log_sigma.len() != n || self.weights.len() != n {
1069 return Err(GamlssError::DimensionMismatch {
1070 reason: "GaussianLocationScaleFamily input size mismatch".to_string(),
1071 }
1072 .into());
1073 }
1074 let ln2pi = (2.0 * std::f64::consts::PI).ln();
1075 let mut ll = 0.0;
1076 for i in 0..n {
1077 ll += gaussian_diagonal_row_kernel(
1078 i,
1079 self.y[i],
1080 etamu[i],
1081 eta_log_sigma[i],
1082 self.weights[i],
1083 ln2pi,
1084 )?
1085 .log_likelihood;
1086 if !ll.is_finite() {
1087 return Err(GamlssError::RowGeometryUnrepresentable {
1088 row: i,
1089 quantity: "Gaussian cumulative log likelihood",
1090 eta: eta_log_sigma[i],
1091 value: ll,
1092 }
1093 .into());
1094 }
1095 }
1096 Ok(ll)
1097 }
1098
1099 fn log_likelihood_only_with_options(
1110 &self,
1111 block_states: &[ParameterBlockState],
1112 options: &BlockwiseFitOptions,
1113 ) -> Result<f64, String> {
1114 let Some(subsample) = options.outer_score_subsample.as_ref() else {
1115 return self.log_likelihood_only(block_states);
1116 };
1117 validate_block_count::<GamlssError>("GaussianLocationScaleFamily", 2, block_states.len())?;
1118 let n = self.y.len();
1119 let etamu = &block_states[Self::BLOCK_MU].eta;
1120 let eta_log_sigma = &block_states[Self::BLOCK_LOG_SIGMA].eta;
1121 if etamu.len() != n || eta_log_sigma.len() != n || self.weights.len() != n {
1122 return Err(GamlssError::DimensionMismatch {
1123 reason: "GaussianLocationScaleFamily input size mismatch".to_string(),
1124 }
1125 .into());
1126 }
1127 let ln2pi = (2.0 * std::f64::consts::PI).ln();
1128 let mut ll = 0.0;
1129 for sampled in subsample.rows.iter() {
1130 let i = sampled.index;
1131 let row_ll = gaussian_diagonal_row_kernel(
1132 i,
1133 self.y[i],
1134 etamu[i],
1135 eta_log_sigma[i],
1136 self.weights[i],
1137 ln2pi,
1138 )?
1139 .log_likelihood;
1140 let contribution = scaled_signed_product3(sampled.weight, row_ll, 1.0);
1141 ll += contribution;
1142 if !contribution.is_finite() || !ll.is_finite() {
1143 return Err(GamlssError::RowGeometryUnrepresentable {
1144 row: i,
1145 quantity: "Gaussian subsampled log likelihood",
1146 eta: eta_log_sigma[i],
1147 value: if contribution.is_finite() {
1148 ll
1149 } else {
1150 contribution
1151 },
1152 }
1153 .into());
1154 }
1155 }
1156 Ok(ll)
1157 }
1158
1159 fn exact_newton_joint_hessian(
1160 &self,
1161 block_states: &[ParameterBlockState],
1162 ) -> Result<Option<Array2<f64>>, String> {
1163 self.exact_newton_joint_hessian_for_specs(block_states, None)
1164 }
1165
1166 fn exact_newton_joint_gradient_evaluation(
1167 &self,
1168 block_states: &[ParameterBlockState],
1169 specs: &[ParameterBlockSpec],
1170 ) -> Result<Option<ExactNewtonJointGradientEvaluation>, String> {
1171 self.exact_newton_joint_gradient_for_specs(block_states, Some(specs))
1172 }
1173
1174 fn has_explicit_joint_hessian(&self) -> bool {
1175 true
1176 }
1177
1178 fn joint_jeffreys_term_required(&self) -> bool {
1194 false
1195 }
1196
1197 fn exact_newton_joint_hessian_directional_derivative(
1198 &self,
1199 block_states: &[ParameterBlockState],
1200 d_beta_flat: &Array1<f64>,
1201 ) -> Result<Option<Array2<f64>>, String> {
1202 self.exact_newton_joint_hessian_directional_derivative_for_specs(
1203 block_states,
1204 None,
1205 d_beta_flat,
1206 )
1207 }
1208
1209 fn exact_newton_joint_hessiansecond_directional_derivative(
1210 &self,
1211 block_states: &[ParameterBlockState],
1212 d_beta_u_flat: &Array1<f64>,
1213 d_betav_flat: &Array1<f64>,
1214 ) -> Result<Option<Array2<f64>>, String> {
1215 self.exact_newton_joint_hessian_second_directional_derivative_for_specs(
1216 block_states,
1217 None,
1218 d_beta_u_flat,
1219 d_betav_flat,
1220 )
1221 }
1222
1223 fn diagonalworking_weights_directional_derivative(
1224 &self,
1225 block_states: &[ParameterBlockState],
1226 block_idx: usize,
1227 d_eta: &Array1<f64>,
1228 ) -> Result<Option<Array1<f64>>, String> {
1229 validate_block_count::<GamlssError>("GaussianLocationScaleFamily", 2, block_states.len())?;
1230 let n = self.y.len();
1231 let eta_t = &block_states[Self::BLOCK_MU].eta;
1232 let eta_ls = &block_states[Self::BLOCK_LOG_SIGMA].eta;
1233 if eta_t.len() != n || eta_ls.len() != n || self.weights.len() != n || d_eta.len() != n {
1234 return Err(GamlssError::DimensionMismatch {
1235 reason: "GaussianLocationScaleFamily input size mismatch".to_string(),
1236 }
1237 .into());
1238 }
1239
1240 let sigma = eta_ls.mapv(logb_sigma_from_eta_scalar);
1241 let mut dw = Array1::<f64>::zeros(n);
1242 match block_idx {
1243 Self::BLOCK_MU => {
1244 Ok(Some(dw))
1252 }
1253 Self::BLOCK_LOG_SIGMA => {
1254 use rayon::iter::{IntoParallelIterator, ParallelIterator};
1269 let dw_vec: Vec<Result<f64, String>> = (0..n)
1270 .into_par_iter()
1271 .map(|i| {
1272 let d1 = crate::sigma_link::logb_sigma_jet1_scalar(eta_ls[i]).d1;
1273 gaussian_log_sigma_irlsinfo_directional_derivative(
1274 i,
1275 eta_ls[i],
1276 self.weights[i],
1277 sigma[i],
1278 d1,
1279 d_eta[i],
1280 )
1281 })
1282 .collect();
1283 for (i, v) in dw_vec.into_iter().enumerate() {
1284 dw[i] = v?;
1285 }
1286 Ok(Some(dw))
1287 }
1288 _ => Ok(None),
1289 }
1290 }
1291
1292 fn exact_newton_joint_hessian_with_specs(
1293 &self,
1294 block_states: &[ParameterBlockState],
1295 specs: &[ParameterBlockSpec],
1296 ) -> Result<Option<Array2<f64>>, String> {
1297 self.exact_newton_joint_hessian_for_specs(block_states, Some(specs))
1298 }
1299
1300 fn exact_newton_joint_hessian_directional_derivative_with_specs(
1301 &self,
1302 block_states: &[ParameterBlockState],
1303 specs: &[ParameterBlockSpec],
1304 d_beta_flat: &Array1<f64>,
1305 ) -> Result<Option<Array2<f64>>, String> {
1306 self.exact_newton_joint_hessian_directional_derivative_for_specs(
1307 block_states,
1308 Some(specs),
1309 d_beta_flat,
1310 )
1311 }
1312
1313 fn exact_newton_joint_hessian_second_directional_derivative_with_specs(
1314 &self,
1315 block_states: &[ParameterBlockState],
1316 specs: &[ParameterBlockSpec],
1317 d_beta_u_flat: &Array1<f64>,
1318 d_betav_flat: &Array1<f64>,
1319 ) -> Result<Option<Array2<f64>>, String> {
1320 self.exact_newton_joint_hessian_second_directional_derivative_for_specs(
1321 block_states,
1322 Some(specs),
1323 d_beta_u_flat,
1324 d_betav_flat,
1325 )
1326 }
1327
1328 fn exact_newton_joint_psi_terms(
1329 &self,
1330 block_states: &[ParameterBlockState],
1331 specs: &[ParameterBlockSpec],
1332 hyper_layout: &crate::custom_family::CustomFamilyHyperLayout,
1333 psi_index: usize,
1334 ) -> Result<Option<gam_problem::ExactNewtonJointPsiTerms>, String> {
1335 if hyper_layout.family_axis_count() != 0 {
1336 return Err("GaussianLocationScaleFamily does not declare family-owned hyper axes"
1337 .to_string());
1338 }
1339 self.exact_newton_joint_psi_terms_for_specs(
1340 block_states,
1341 specs,
1342 hyper_layout,
1343 psi_index,
1344 )
1345 }
1346
1347 fn exact_newton_joint_psisecond_order_terms(
1348 &self,
1349 block_states: &[ParameterBlockState],
1350 specs: &[ParameterBlockSpec],
1351 hyper_layout: &crate::custom_family::CustomFamilyHyperLayout,
1352 psi_i: usize,
1353 psi_j: usize,
1354 ) -> Result<Option<gam_problem::ExactNewtonJointPsiSecondOrderTerms>, String> {
1355 if hyper_layout.family_axis_count() != 0 {
1356 return Err("GaussianLocationScaleFamily does not declare family-owned hyper axes"
1357 .to_string());
1358 }
1359 self.exact_newton_joint_psisecond_order_terms_for_specs(
1360 block_states,
1361 specs,
1362 hyper_layout,
1363 psi_i,
1364 psi_j,
1365 )
1366 }
1367
1368 fn exact_newton_joint_psihessian_directional_derivative(
1369 &self,
1370 block_states: &[ParameterBlockState],
1371 specs: &[ParameterBlockSpec],
1372 hyper_layout: &crate::custom_family::CustomFamilyHyperLayout,
1373 psi_index: usize,
1374 d_beta_flat: &Array1<f64>,
1375 ) -> Result<Option<Array2<f64>>, String> {
1376 if hyper_layout.family_axis_count() != 0 {
1377 return Err("GaussianLocationScaleFamily does not declare family-owned hyper axes"
1378 .to_string());
1379 }
1380 self.exact_newton_joint_psihessian_directional_derivative_for_specs(
1381 block_states,
1382 specs,
1383 hyper_layout.design_derivative_blocks(),
1384 psi_index,
1385 d_beta_flat,
1386 )
1387 }
1388
1389 fn exact_newton_joint_psi_workspace(
1390 &self,
1391 block_states: &[ParameterBlockState],
1392 specs: &[ParameterBlockSpec],
1393 hyper_layout: &crate::custom_family::CustomFamilyHyperLayout,
1394 ) -> Result<Option<Arc<dyn ExactNewtonJointPsiWorkspace>>, String> {
1395 if hyper_layout.family_axis_count() != 0 {
1396 return Err("GaussianLocationScaleFamily does not declare family-owned hyper axes"
1397 .to_string());
1398 }
1399 let derivative_blocks = hyper_layout.design_derivative_blocks();
1400 validate_block_count::<GamlssError>("GaussianLocationScaleFamily", 2, block_states.len())?;
1401 if specs.len() != 2 || derivative_blocks.len() != 2 {
1402 return Err(GamlssError::DimensionMismatch { reason: format!(
1403 "GaussianLocationScaleFamily joint psi workspace expects 2 specs and 2 derivative block lists, got {} / {}",
1404 specs.len(),
1405 derivative_blocks.len()
1406 ) }.into());
1407 }
1408 Ok(Some(Arc::new(
1409 GaussianLocationScaleExactNewtonJointPsiWorkspace::new(
1410 self.clone(),
1411 block_states.to_vec(),
1412 specs,
1413 derivative_blocks.to_vec(),
1414 )?,
1415 )))
1416 }
1417
1418 fn exact_newton_joint_psi_workspace_with_options(
1436 &self,
1437 block_states: &[ParameterBlockState],
1438 specs: &[ParameterBlockSpec],
1439 hyper_layout: &crate::custom_family::CustomFamilyHyperLayout,
1440 options: &BlockwiseFitOptions,
1441 ) -> Result<Option<Arc<dyn ExactNewtonJointPsiWorkspace>>, String> {
1442 if hyper_layout.family_axis_count() != 0 {
1443 return Err("GaussianLocationScaleFamily does not declare family-owned hyper axes"
1444 .to_string());
1445 }
1446 let derivative_blocks = hyper_layout.design_derivative_blocks();
1447 validate_block_count::<GamlssError>("GaussianLocationScaleFamily", 2, block_states.len())?;
1448 if specs.len() != 2 || derivative_blocks.len() != 2 {
1449 return Err(GamlssError::DimensionMismatch { reason: format!(
1450 "GaussianLocationScaleFamily joint psi workspace expects 2 specs and 2 derivative block lists, got {} / {}",
1451 specs.len(),
1452 derivative_blocks.len()
1453 ) }.into());
1454 }
1455 Ok(Some(Arc::new(
1456 GaussianLocationScaleExactNewtonJointPsiWorkspace::new_with_subsample(
1457 self.clone(),
1458 block_states.to_vec(),
1459 specs,
1460 derivative_blocks.to_vec(),
1461 options.outer_score_subsample.clone(),
1462 )?,
1463 )))
1464 }
1465
1466 fn exact_newton_joint_hessian_workspace(
1467 &self,
1468 block_states: &[ParameterBlockState],
1469 specs: &[ParameterBlockSpec],
1470 ) -> Result<Option<Arc<dyn ExactNewtonJointHessianWorkspace>>, String> {
1471 let Some((xmu, x_ls)) = self.exact_joint_dense_block_designs(Some(specs))? else {
1472 return Ok(None);
1473 };
1474 let workspace = GaussianLocationScaleHessianWorkspace::new(
1475 self.clone(),
1476 block_states.to_vec(),
1477 xmu.into_owned(),
1478 x_ls.into_owned(),
1479 )?;
1480 Ok(Some(Arc::new(workspace)))
1481 }
1482
1483 fn exact_newton_joint_hessian_workspace_with_options(
1497 &self,
1498 block_states: &[ParameterBlockState],
1499 specs: &[ParameterBlockSpec],
1500 options: &BlockwiseFitOptions,
1501 ) -> Result<Option<Arc<dyn ExactNewtonJointHessianWorkspace>>, String> {
1502 let Some((xmu, x_ls)) = self.exact_joint_dense_block_designs(Some(specs))? else {
1503 return Ok(None);
1504 };
1505 let mut workspace = GaussianLocationScaleHessianWorkspace::new(
1506 self.clone(),
1507 block_states.to_vec(),
1508 xmu.into_owned(),
1509 x_ls.into_owned(),
1510 )?;
1511 if let Some(subsample) = options.outer_score_subsample.as_ref() {
1512 workspace.apply_outer_subsample(subsample.rows.as_ref());
1513 }
1514 Ok(Some(Arc::new(workspace)))
1515 }
1516
1517 fn inner_coefficient_hessian_hvp_available(&self, specs: &[ParameterBlockSpec]) -> bool {
1518 self.exact_joint_supported()
1524 && matches!(
1525 self.exact_joint_dense_block_designs(Some(specs)),
1526 Ok(Some(_))
1527 )
1528 }
1529
1530 fn outer_derivative_subsample_capable(&self) -> bool {
1552 true
1553 }
1554}
1555
1556impl CustomFamilyGenerative for GaussianLocationScaleFamily {
1557 fn generativespec(
1558 &self,
1559 block_states: &[ParameterBlockState],
1560 ) -> Result<GenerativeSpec, String> {
1561 validate_block_count::<GamlssError>("GaussianLocationScaleFamily", 2, block_states.len())?;
1562 let mu = block_states[Self::BLOCK_MU].eta.clone();
1563 let eta_log_sigma = &block_states[Self::BLOCK_LOG_SIGMA].eta;
1564 let sigma = gamlss_rowwise_map(eta_log_sigma.len(), |i| {
1565 logb_sigma_from_eta_scalar(eta_log_sigma[i])
1566 });
1567 Ok(GenerativeSpec {
1568 mean: mu,
1569 noise: NoiseModel::Gaussian { sigma },
1570 })
1571 }
1572}