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".to_string(),
294 }
295 .into());
296 }
297 let rows = self.get_or_compute_row_scalars(etamu, eta_ls)?;
298 let zero = Array1::<f64>::zeros(n);
299 let weights = gaussian_joint_psi_firstweights(&rows, &zero, &zero);
300 let grad_eta_mu = -&weights.scoremu;
301 let grad_eta_ls = -&weights.score_ls;
302 let grad_mu = fast_atv(xmu, &grad_eta_mu);
303 let grad_ls = fast_atv(x_ls, &grad_eta_ls);
304 let gradient = gaussian_pack_joint_score(&grad_mu, &grad_ls);
305 let log_likelihood = self.log_likelihood_only(block_states)?;
306 Ok(ExactNewtonJointGradientEvaluation {
307 log_likelihood,
308 gradient,
309 })
310 }
311
312 pub(crate) fn exact_newton_joint_hessian_directional_derivative_for_specs(
313 &self,
314 block_states: &[ParameterBlockState],
315 specs: Option<&[ParameterBlockSpec]>,
316 d_beta_flat: &Array1<f64>,
317 ) -> Result<Option<Array2<f64>>, String> {
318 let Some((xmu, x_ls)) = self.exact_joint_block_designs(specs)? else {
319 return Ok(None);
320 };
321 self.exact_newton_joint_hessian_directional_derivative_from_designs(
322 block_states,
323 &xmu,
324 &x_ls,
325 d_beta_flat,
326 )
327 }
328
329 pub(crate) fn exact_newton_joint_hessian_second_directional_derivative_for_specs(
330 &self,
331 block_states: &[ParameterBlockState],
332 specs: Option<&[ParameterBlockSpec]>,
333 d_beta_u_flat: &Array1<f64>,
334 d_betav_flat: &Array1<f64>,
335 ) -> Result<Option<Array2<f64>>, String> {
336 let Some((xmu, x_ls)) = self.exact_joint_block_designs(specs)? else {
337 return Ok(None);
338 };
339 self.exact_newton_joint_hessiansecond_directional_derivative_from_designs(
340 block_states,
341 &xmu,
342 &x_ls,
343 d_beta_u_flat,
344 d_betav_flat,
345 )
346 }
347
348 pub(crate) fn exact_newton_joint_psi_terms_for_specs(
349 &self,
350 block_states: &[ParameterBlockState],
351 specs: &[ParameterBlockSpec],
352 derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
353 psi_index: usize,
354 ) -> Result<Option<crate::custom_family::ExactNewtonJointPsiTerms>, String> {
355 let Some((xmu, x_ls)) = self.exact_joint_dense_block_designs(Some(specs))? else {
356 return Ok(None);
357 };
358 self.exact_newton_joint_psi_terms_from_designs(
359 block_states,
360 specs,
361 derivative_blocks,
362 psi_index,
363 &xmu,
364 &x_ls,
365 )
366 }
367
368 pub(crate) fn exact_newton_joint_psisecond_order_terms_for_specs(
369 &self,
370 block_states: &[ParameterBlockState],
371 specs: &[ParameterBlockSpec],
372 derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
373 psi_i: usize,
374 psi_j: usize,
375 ) -> Result<Option<crate::custom_family::ExactNewtonJointPsiSecondOrderTerms>, String> {
376 let Some((xmu, x_ls)) = self.exact_joint_dense_block_designs(Some(specs))? else {
377 return Ok(None);
378 };
379 self.exact_newton_joint_psisecond_order_terms_from_designs(
380 block_states,
381 derivative_blocks,
382 psi_i,
383 psi_j,
384 &xmu,
385 &x_ls,
386 )
387 }
388
389 pub(crate) fn exact_newton_joint_psihessian_directional_derivative_for_specs(
390 &self,
391 block_states: &[ParameterBlockState],
392 specs: &[ParameterBlockSpec],
393 derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
394 psi_index: usize,
395 d_beta_flat: &Array1<f64>,
396 ) -> Result<Option<Array2<f64>>, String> {
397 let Some((xmu, x_ls)) = self.exact_joint_dense_block_designs(Some(specs))? else {
398 return Ok(None);
399 };
400 self.exact_newton_joint_psihessian_directional_derivative_from_designs(
401 block_states,
402 derivative_blocks,
403 psi_index,
404 d_beta_flat,
405 &xmu,
406 &x_ls,
407 )
408 }
409
410 pub(crate) fn exact_newton_joint_hessian_from_designs(
411 &self,
412 block_states: &[ParameterBlockState],
413 xmu: &DenseOrOperator<'_>,
414 x_ls: &DenseOrOperator<'_>,
415 ) -> Result<Option<Array2<f64>>, String> {
416 validate_block_count::<GamlssError>("GaussianLocationScaleFamily", 2, block_states.len())?;
417 let n = self.y.len();
418 let etamu = &block_states[Self::BLOCK_MU].eta;
419 let eta_ls = &block_states[Self::BLOCK_LOG_SIGMA].eta;
420 if etamu.len() != n || eta_ls.len() != n || self.weights.len() != n {
421 return Err(GamlssError::DimensionMismatch {
422 reason: "GaussianLocationScaleFamily input size mismatch".to_string(),
423 }
424 .into());
425 }
426
427 let rows = self.get_or_compute_row_scalars(etamu, eta_ls)?;
428 let (mm, cross, scale) = gaussian_locscale_fisher_joint_row_coeffs(&rows);
433 Ok(Some(gaussian_joint_hessian_from_designs(
434 xmu, x_ls, &mm, &cross, &scale,
435 )?))
436 }
437
438 pub(crate) fn exact_newton_joint_hessian_directional_derivative_from_designs(
439 &self,
440 block_states: &[ParameterBlockState],
441 xmu: &DenseOrOperator<'_>,
442 x_ls: &DenseOrOperator<'_>,
443 d_beta_flat: &Array1<f64>,
444 ) -> Result<Option<Array2<f64>>, String> {
445 validate_block_count::<GamlssError>("GaussianLocationScaleFamily", 2, block_states.len())?;
446 let n = self.y.len();
447 let etamu = &block_states[Self::BLOCK_MU].eta;
448 let eta_ls = &block_states[Self::BLOCK_LOG_SIGMA].eta;
449 if etamu.len() != n || eta_ls.len() != n || self.weights.len() != n {
450 return Err(GamlssError::DimensionMismatch {
451 reason: "GaussianLocationScaleFamily input size mismatch".to_string(),
452 }
453 .into());
454 }
455
456 let pmu = xmu.ncols();
457 let p_ls = x_ls.ncols();
458 let total = pmu + p_ls;
459 if d_beta_flat.len() != total {
460 return Err(GamlssError::DimensionMismatch {
461 reason: format!(
462 "GaussianLocationScaleFamily joint d_beta length mismatch: got {}, expected {}",
463 d_beta_flat.len(),
464 total
465 ),
466 }
467 .into());
468 }
469 let ximu = xmu.dot(d_beta_flat.slice(s![0..pmu]));
470 let xi_ls = x_ls.dot(d_beta_flat.slice(s![pmu..pmu + p_ls]));
471 let rows = self.get_or_compute_row_scalars(etamu, eta_ls)?;
472 let directional = gaussian_joint_first_directionalweights(&rows, &ximu, &xi_ls);
473 let dhmumu = directional.0;
474 let dh_ls_ls = directional.2;
475 let dhmu_ls = Array1::<f64>::zeros(dhmumu.len());
482
483 Ok(Some(gaussian_joint_hessian_from_designs(
484 xmu, x_ls, &dhmumu, &dhmu_ls, &dh_ls_ls,
485 )?))
486 }
487
488 pub(crate) fn exact_newton_joint_hessiansecond_directional_derivative_from_designs(
489 &self,
490 block_states: &[ParameterBlockState],
491 xmu: &DenseOrOperator<'_>,
492 x_ls: &DenseOrOperator<'_>,
493 d_beta_u_flat: &Array1<f64>,
494 d_betav_flat: &Array1<f64>,
495 ) -> Result<Option<Array2<f64>>, String> {
496 validate_block_count::<GamlssError>("GaussianLocationScaleFamily", 2, block_states.len())?;
497 let n = self.y.len();
498 let etamu = &block_states[Self::BLOCK_MU].eta;
499 let eta_ls = &block_states[Self::BLOCK_LOG_SIGMA].eta;
500 if etamu.len() != n || eta_ls.len() != n || self.weights.len() != n {
501 return Err(GamlssError::DimensionMismatch {
502 reason: "GaussianLocationScaleFamily input size mismatch".to_string(),
503 }
504 .into());
505 }
506
507 let pmu = xmu.ncols();
508 let p_ls = x_ls.ncols();
509 let total = pmu + p_ls;
510 if d_beta_u_flat.len() != total || d_betav_flat.len() != total {
511 return Err(GamlssError::DimensionMismatch { reason: format!(
512 "GaussianLocationScaleFamily joint second directional derivative length mismatch: got {} and {}, expected {}",
513 d_beta_u_flat.len(),
514 d_betav_flat.len(),
515 total
516 ) }.into());
517 }
518 let ximu_u = xmu.dot(d_beta_u_flat.slice(s![0..pmu]));
519 let xi_ls_u = x_ls.dot(d_beta_u_flat.slice(s![pmu..pmu + p_ls]));
520 let ximuv = xmu.dot(d_betav_flat.slice(s![0..pmu]));
521 let xi_lsv = x_ls.dot(d_betav_flat.slice(s![pmu..pmu + p_ls]));
522 let rows = self.get_or_compute_row_scalars(etamu, eta_ls)?;
523 let second =
524 gaussian_jointsecond_directionalweights(&rows, &ximu_u, &xi_ls_u, &ximuv, &xi_lsv);
525 let d2hmumu = second.0;
526 let d2h_ls_ls = second.2;
527 let d2hmu_ls = Array1::<f64>::zeros(d2hmumu.len());
531
532 Ok(Some(gaussian_joint_hessian_from_designs(
533 xmu, x_ls, &d2hmumu, &d2hmu_ls, &d2h_ls_ls,
534 )?))
535 }
536
537 pub(crate) fn exact_newton_joint_psi_direction(
538 &self,
539 block_states: &[ParameterBlockState],
540 derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
541 psi_index: usize,
542 xmu: &Array2<f64>,
543 x_ls: &Array2<f64>,
544 policy: &gam_runtime::resource::ResourcePolicy,
545 ) -> Result<Option<LocationScaleJointPsiDirection>, String> {
546 let Some(parts) = locscale_joint_psi_direction_parts(
547 block_states,
548 derivative_blocks,
549 psi_index,
550 self.y.len(),
551 xmu.ncols(),
552 x_ls.ncols(),
553 Self::BLOCK_MU,
554 Self::BLOCK_LOG_SIGMA,
555 2,
556 "GaussianLocationScaleFamily",
557 "mu",
558 policy,
559 )?
560 else {
561 return Ok(None);
562 };
563 Ok(Some(LocationScaleJointPsiDirection {
564 block_idx: parts.block_idx,
565 local_idx: parts.local_idx,
566 z_primary_psi: parts.primary_z,
567 z_ls_psi: parts.log_sigma_z,
568 x_primary_psi: parts.primary_psi,
569 x_ls_psi: parts.log_sigma_psi,
570 }))
571 }
572
573 pub(crate) fn exact_newton_joint_psisecond_design_drifts(
574 &self,
575 block_states: &[ParameterBlockState],
576 derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
577 psi_a: &LocationScaleJointPsiDirection,
578 psi_b: &LocationScaleJointPsiDirection,
579 xmu: &Array2<f64>,
580 x_ls: &Array2<f64>,
581 ) -> Result<LocationScaleJointPsiSecondDrifts, String> {
582 locscale_joint_psisecond_design_drifts(
583 block_states,
584 derivative_blocks,
585 psi_a,
586 psi_b,
587 LocScalePsiDriftConfig {
588 n: self.y.len(),
589 p_primary: xmu.ncols(),
590 p_log_sigma: x_ls.ncols(),
591 primary_block_idx: Self::BLOCK_MU,
592 log_sigma_block_idx: Self::BLOCK_LOG_SIGMA,
593 family_name: "GaussianLocationScaleFamily",
594 primary_label: "mu",
595 policy: &self.policy,
596 },
597 )
598 }
599
600 pub(crate) fn exact_newton_joint_psi_terms_from_designs(
601 &self,
602 block_states: &[ParameterBlockState],
603 specs: &[ParameterBlockSpec],
604 derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
605 psi_index: usize,
606 xmu: &Array2<f64>,
607 x_ls: &Array2<f64>,
608 ) -> Result<Option<crate::custom_family::ExactNewtonJointPsiTerms>, String> {
609 validate_block_count::<GamlssError>("GaussianLocationScaleFamily", 2, block_states.len())?;
610 if specs.len() != 2 || derivative_blocks.len() != 2 {
611 return Err(GamlssError::DimensionMismatch { reason: format!(
612 "GaussianLocationScaleFamily joint psi terms expect 2 specs and 2 derivative blocks, got {} and {}",
613 specs.len(),
614 derivative_blocks.len()
615 ) }.into());
616 }
617 let Some(dir_a) = self.exact_newton_joint_psi_direction(
618 block_states,
619 derivative_blocks,
620 psi_index,
621 xmu,
622 x_ls,
623 &self.policy,
624 )?
625 else {
626 return Ok(None);
627 };
628 let etamu = &block_states[Self::BLOCK_MU].eta;
660 let eta_ls = &block_states[Self::BLOCK_LOG_SIGMA].eta;
661 let rows = self.get_or_compute_row_scalars(etamu, eta_ls)?;
662 let weights_a =
663 gaussian_joint_psi_firstweights(&rows, &dir_a.z_primary_psi, &dir_a.z_ls_psi);
664 let objective_psi = weights_a.objective_psirow.sum();
665 let xmu_map = dir_a.x_primary_psi.as_linear_map_ref();
666 let x_ls_map = dir_a.x_ls_psi.as_linear_map_ref();
667 let score_mu =
668 xmu_map.transpose_mul(weights_a.scoremu.view()) + fast_atv(xmu, &weights_a.dscoremu);
669 let score_ls = x_ls_map.transpose_mul(weights_a.score_ls.view())
670 + fast_atv(x_ls, &weights_a.dscore_ls);
671 let score_psi = gaussian_pack_joint_score(&score_mu, &score_ls);
672 let hessian_psi_operator = build_two_block_custom_family_joint_psi_operator_from_actions(
673 dir_a.x_primary_psi.cloned_first_action(),
674 dir_a.x_ls_psi.cloned_first_action(),
675 0..xmu.ncols(),
676 xmu.ncols()..xmu.ncols() + x_ls.ncols(),
677 xmu,
678 x_ls,
679 &weights_a.hmumu,
680 &weights_a.hmu_ls,
681 &weights_a.h_ls_ls,
682 &weights_a.dhmumu,
683 &weights_a.dhmu_ls,
684 &weights_a.dh_ls_ls,
685 )?;
686 let hessian_psi = if hessian_psi_operator.is_some() {
687 Array2::zeros((0, 0))
688 } else {
689 gaussian_joint_psihessian_fromweights(xmu, x_ls, xmu_map, x_ls_map, &weights_a)?
690 };
691
692 Ok(Some(crate::custom_family::ExactNewtonJointPsiTerms {
693 objective_psi,
694 score_psi,
695 hessian_psi,
696 hessian_psi_operator,
697 }))
698 }
699
700 pub(crate) fn exact_newton_joint_psisecond_order_terms_from_designs(
701 &self,
702 block_states: &[ParameterBlockState],
703 derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
704 psi_i: usize,
705 psi_j: usize,
706 xmu: &Array2<f64>,
707 x_ls: &Array2<f64>,
708 ) -> Result<Option<crate::custom_family::ExactNewtonJointPsiSecondOrderTerms>, String> {
709 let Some(dir_i) = self.exact_newton_joint_psi_direction(
710 block_states,
711 derivative_blocks,
712 psi_i,
713 xmu,
714 x_ls,
715 &self.policy,
716 )?
717 else {
718 return Ok(None);
719 };
720 let Some(dir_j) = self.exact_newton_joint_psi_direction(
721 block_states,
722 derivative_blocks,
723 psi_j,
724 xmu,
725 x_ls,
726 &self.policy,
727 )?
728 else {
729 return Ok(None);
730 };
731 Ok(Some(
732 self.exact_newton_joint_psisecond_order_terms_from_parts(
733 block_states,
734 derivative_blocks,
735 &dir_i,
736 &dir_j,
737 xmu,
738 x_ls,
739 None,
740 )?,
741 ))
742 }
743
744 pub(crate) fn exact_newton_joint_psisecond_order_terms_from_parts(
745 &self,
746 block_states: &[ParameterBlockState],
747 derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
748 dir_i: &LocationScaleJointPsiDirection,
749 dir_j: &LocationScaleJointPsiDirection,
750 xmu: &Array2<f64>,
751 x_ls: &Array2<f64>,
752 subsample: Option<&[crate::outer_subsample::WeightedOuterRow]>,
753 ) -> Result<crate::custom_family::ExactNewtonJointPsiSecondOrderTerms, String> {
754 let second_drifts = self.exact_newton_joint_psisecond_design_drifts(
755 block_states,
756 derivative_blocks,
757 dir_i,
758 dir_j,
759 xmu,
760 x_ls,
761 )?;
762 let n = self.y.len();
763 let xmu_i_map = dir_i.x_primary_psi.as_linear_map_ref();
764 let x_ls_i_map = dir_i.x_ls_psi.as_linear_map_ref();
765 let xmu_j_map = dir_j.x_primary_psi.as_linear_map_ref();
766 let x_ls_j_map = dir_j.x_ls_psi.as_linear_map_ref();
767 let xmu_ab_map = second_psi_linear_map(
768 second_drifts.x_primary_ab_action.as_ref(),
769 second_drifts.x_primary_ab.as_ref(),
770 n,
771 xmu.ncols(),
772 );
773 let x_ls_ab_map = second_psi_linear_map(
774 second_drifts.x_ls_ab_action.as_ref(),
775 second_drifts.x_ls_ab.as_ref(),
776 n,
777 x_ls.ncols(),
778 );
779 let etamu = &block_states[Self::BLOCK_MU].eta;
803 let eta_ls = &block_states[Self::BLOCK_LOG_SIGMA].eta;
804 let rows = self.get_or_compute_row_scalars(etamu, eta_ls)?;
805 let mut weights_i =
806 gaussian_joint_psi_firstweights(&rows, &dir_i.z_primary_psi, &dir_i.z_ls_psi);
807 let mut weights_j =
808 gaussian_joint_psi_firstweights(&rows, &dir_j.z_primary_psi, &dir_j.z_ls_psi);
809 let mut secondweights = gaussian_joint_psisecondweights(
810 &rows,
811 &dir_i.z_primary_psi,
812 &dir_i.z_ls_psi,
813 &dir_j.z_primary_psi,
814 &dir_j.z_ls_psi,
815 &second_drifts.z_primary_ab,
816 &second_drifts.z_ls_ab,
817 );
818 if let Some(sub_rows) = subsample {
819 apply_ht_mask_first(&mut weights_i, sub_rows);
825 apply_ht_mask_first(&mut weights_j, sub_rows);
826 apply_ht_mask_second(&mut secondweights, sub_rows);
827 }
828 let objective_psi_psi = secondweights.objective_psi_psirow.sum();
829
830 let score_psi_psi = gaussian_pack_joint_score(
831 &(xmu_ab_map.transpose_mul(weights_i.scoremu.view())
832 + xmu_i_map.transpose_mul(weights_j.dscoremu.view())
833 + xmu_j_map.transpose_mul(weights_i.dscoremu.view())
834 + fast_atv(xmu, &secondweights.d2scoremu)),
835 &(x_ls_ab_map.transpose_mul(weights_i.score_ls.view())
836 + x_ls_i_map.transpose_mul(weights_j.dscore_ls.view())
837 + x_ls_j_map.transpose_mul(weights_i.dscore_ls.view())
838 + fast_atv(x_ls, &secondweights.d2score_ls)),
839 );
840 let hessian_psi_psi = gaussian_joint_psisecondhessian_fromweights(
841 xmu,
842 x_ls,
843 xmu_i_map,
844 x_ls_i_map,
845 xmu_j_map,
846 x_ls_j_map,
847 xmu_ab_map,
848 x_ls_ab_map,
849 &weights_i,
850 &weights_j,
851 &secondweights,
852 )?;
853
854 Ok(crate::custom_family::ExactNewtonJointPsiSecondOrderTerms {
855 objective_psi_psi,
856 score_psi_psi,
857 hessian_psi_psi,
858 hessian_psi_psi_operator: None,
859 })
860 }
861
862 pub(crate) fn exact_newton_joint_psihessian_directional_derivative_from_designs(
863 &self,
864 block_states: &[ParameterBlockState],
865 derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
866 psi_index: usize,
867 d_beta_flat: &Array1<f64>,
868 xmu: &Array2<f64>,
869 x_ls: &Array2<f64>,
870 ) -> Result<Option<Array2<f64>>, String> {
871 let Some(dir_a) = self.exact_newton_joint_psi_direction(
872 block_states,
873 derivative_blocks,
874 psi_index,
875 xmu,
876 x_ls,
877 &self.policy,
878 )?
879 else {
880 return Ok(None);
881 };
882 Ok(Some(
883 self.exact_newton_joint_psihessian_directional_derivative_from_parts(
884 block_states,
885 &dir_a,
886 d_beta_flat,
887 xmu,
888 x_ls,
889 None,
890 )?,
891 ))
892 }
893
894 pub(crate) fn exact_newton_joint_psihessian_directional_derivative_from_parts(
895 &self,
896 block_states: &[ParameterBlockState],
897 dir_a: &LocationScaleJointPsiDirection,
898 d_beta_flat: &Array1<f64>,
899 xmu: &Array2<f64>,
900 x_ls: &Array2<f64>,
901 subsample: Option<&[crate::outer_subsample::WeightedOuterRow]>,
902 ) -> Result<Array2<f64>, String> {
903 let etamu = &block_states[Self::BLOCK_MU].eta;
904 let eta_ls = &block_states[Self::BLOCK_LOG_SIGMA].eta;
905 let pmu = xmu.ncols();
906 let p_ls = x_ls.ncols();
907 let xmu_map = dir_a.x_primary_psi.as_linear_map_ref();
908 let x_ls_map = dir_a.x_ls_psi.as_linear_map_ref();
909 let total = pmu + p_ls;
910 if d_beta_flat.len() != total {
911 return Err(GamlssError::DimensionMismatch { reason: format!(
912 "GaussianLocationScaleFamily joint psi hessian directional derivative length mismatch: got {}, expected {}",
913 d_beta_flat.len(),
914 total
915 ) }.into());
916 }
917 let u_ls = d_beta_flat.slice(s![pmu..pmu + p_ls]);
921 let xi_ls = fast_av(x_ls, &u_ls);
922 let uza_ls = x_ls_map.forward_mul(u_ls);
923 let rows = self.get_or_compute_row_scalars(etamu, eta_ls)?;
965 let mut mixedweights =
966 gaussian_joint_psi_mixed_driftweights(&rows, &xi_ls, &dir_a.z_ls_psi, &uza_ls);
967 if let Some(sub_rows) = subsample {
968 apply_ht_mask_mixed(&mut mixedweights, sub_rows);
973 }
974
975 gaussian_joint_psi_mixedhessian_drift_fromweights(
976 xmu,
977 x_ls,
978 xmu_map,
979 x_ls_map,
980 &mixedweights,
981 )
982 }
983
984 pub fn block_effective_jacobian(
991 specs: &[ParameterBlockSpec],
992 block_idx: usize,
993 ) -> Result<Box<dyn BlockEffectiveJacobian>, String> {
994 crate::block_layout::block_jacobian::AdditiveWiggleBlockLayout {
995 family: "GaussianLocationScaleFamily",
996 n_outputs: 2,
997 additive_blocks: &[Self::BLOCK_MU, Self::BLOCK_LOG_SIGMA],
998 wiggle_block: None,
999 }
1000 .block_effective_jacobian(specs, block_idx)
1001 }
1002}
1003
1004pub struct GaussianLocationScaleChannelHessian {
1024 pub(crate) h: ndarray::Array3<f64>,
1026}
1027
1028impl GaussianLocationScaleChannelHessian {
1029 pub fn from_pilot_observed_unclamped(
1042 y: &ndarray::Array1<f64>,
1043 w: &ndarray::Array1<f64>,
1044 eta_mu: &ndarray::Array1<f64>,
1045 eta_log_sigma: &ndarray::Array1<f64>,
1046 ) -> Result<Self, String> {
1047 let n = y.len();
1048 if w.len() != n || eta_mu.len() != n || eta_log_sigma.len() != n {
1049 return Err(format!(
1050 "GaussianLocationScaleChannelHessian::from_pilot_observed_unclamped: \
1051 length mismatch y={n} w={} eta_mu={} eta_log_sigma={}",
1052 w.len(),
1053 eta_mu.len(),
1054 eta_log_sigma.len(),
1055 ));
1056 }
1057 let mut h = ndarray::Array3::<f64>::zeros((n, 2, 2));
1058 for i in 0..n {
1059 let wi = w[i];
1060 let mu_i = eta_mu[i];
1061 let s_i = eta_log_sigma[i];
1062 let inv_sigma2 = (-2.0 * s_i).exp();
1063 let resid = y[i] - mu_i;
1064 h[[i, 0, 0]] = wi * inv_sigma2;
1065 h[[i, 1, 1]] = wi * 2.0 * resid * resid * inv_sigma2;
1066 h[[i, 0, 1]] = wi * 2.0 * resid * inv_sigma2;
1067 h[[i, 1, 0]] = h[[i, 0, 1]];
1068 }
1069 Ok(Self { h })
1070 }
1071
1072 pub fn from_pilot(
1080 y: &ndarray::Array1<f64>,
1081 w: &ndarray::Array1<f64>,
1082 eta_mu: &ndarray::Array1<f64>,
1083 eta_log_sigma: &ndarray::Array1<f64>,
1084 ) -> Result<Self, String> {
1085 let n = y.len();
1086 if w.len() != n || eta_mu.len() != n || eta_log_sigma.len() != n {
1087 return Err(format!(
1088 "GaussianLocationScaleChannelHessian::from_pilot: \
1089 length mismatch y={n} w={} eta_mu={} eta_log_sigma={}",
1090 w.len(),
1091 eta_mu.len(),
1092 eta_log_sigma.len(),
1093 ));
1094 }
1095 let mut h = ndarray::Array3::<f64>::zeros((n, 2, 2));
1096 for i in 0..n {
1097 let wi = w[i];
1098 let mu_i = eta_mu[i];
1099 let s_i = eta_log_sigma[i];
1100 let inv_sigma2 = (-2.0 * s_i).exp(); let resid = y[i] - mu_i;
1102 let h00 = wi * inv_sigma2;
1104 let h11 = wi * 2.0 * resid * resid * inv_sigma2;
1105 let h01 = wi * 2.0 * resid * inv_sigma2;
1106 let (e0, e1, u1_0, u1_1, u2_0, u2_1) = psd_clamp_2x2(h00, h01, h11);
1111 h[[i, 0, 0]] = e0 * u1_0 * u1_0 + e1 * u2_0 * u2_0;
1112 h[[i, 0, 1]] = e0 * u1_0 * u1_1 + e1 * u2_0 * u2_1;
1113 h[[i, 1, 0]] = h[[i, 0, 1]];
1114 h[[i, 1, 1]] = e0 * u1_1 * u1_1 + e1 * u2_1 * u2_1;
1115 }
1116 Ok(Self { h })
1117 }
1118}
1119
1120impl FamilyChannelHessian for GaussianLocationScaleChannelHessian {
1121 fn n_outputs(&self) -> usize {
1122 2
1123 }
1124
1125 fn n_subjects(&self) -> usize {
1126 self.h.shape()[0]
1127 }
1128
1129 fn fill_subject(&self, i: usize, out: &mut [f64]) {
1130 assert_eq!(out.len(), 4);
1131 out[0] = self.h[[i, 0, 0]];
1132 out[1] = self.h[[i, 0, 1]];
1133 out[2] = self.h[[i, 1, 0]];
1134 out[3] = self.h[[i, 1, 1]];
1135 }
1136
1137 fn evaluate_full(&self) -> ndarray::Array3<f64> {
1138 self.h.clone()
1139 }
1140}
1141
1142impl CustomFamily for GaussianLocationScaleFamily {
1143 fn exact_newton_joint_hessian_beta_dependent(&self) -> bool {
1157 true
1158 }
1159
1160 fn outer_seed_config(&self, n_params: usize) -> crate::seeding::SeedConfig {
1183 if n_params == 0 {
1184 return crate::seeding::SeedConfig::default();
1185 }
1186 let mut config = crate::seeding::SeedConfig::default();
1187 config.risk_profile = crate::seeding::SeedRiskProfile::GaussianLocationScale;
1188 config.max_seeds = 4;
1189 config.seed_budget = 2;
1190 config
1191 }
1192
1193 fn output_channel_assignment(&self, specs: &[ParameterBlockSpec]) -> Option<Vec<usize>> {
1200 Some(
1203 (0..specs.len())
1204 .map(|i| usize::from(i == Self::BLOCK_LOG_SIGMA))
1205 .collect(),
1206 )
1207 }
1208
1209 fn coefficient_hessian_cost(&self, specs: &[ParameterBlockSpec]) -> u64 {
1210 crate::location_scale_engine::location_scale_coefficient_hessian_cost(
1218 self.y.len() as u64,
1219 specs,
1220 )
1221 }
1222
1223 fn evaluate(&self, block_states: &[ParameterBlockState]) -> Result<FamilyEvaluation, String> {
1224 validate_block_count::<GamlssError>("GaussianLocationScaleFamily", 2, block_states.len())?;
1225 let n = self.y.len();
1226 let etamu = &block_states[Self::BLOCK_MU].eta;
1227 let eta_log_sigma = &block_states[Self::BLOCK_LOG_SIGMA].eta;
1228 if etamu.len() != n || eta_log_sigma.len() != n || self.weights.len() != n {
1229 return Err(GamlssError::DimensionMismatch {
1230 reason: "GaussianLocationScaleFamily input size mismatch".to_string(),
1231 }
1232 .into());
1233 }
1234
1235 let mut zmu = Array1::<f64>::zeros(n);
1247 let mut wmu = Array1::<f64>::zeros(n);
1248 let mut z_ls = Array1::<f64>::zeros(n);
1249 let mut w_ls = Array1::<f64>::zeros(n);
1250 let ln2pi = (2.0 * std::f64::consts::PI).ln();
1251 let mut ll = 0.0;
1252
1253 const CHUNK: usize = 1024;
1254 if let (
1255 Some(y_s),
1256 Some(w_s),
1257 Some(mu_s),
1258 Some(ls_s),
1259 Some(zmu_s),
1260 Some(wmu_s),
1261 Some(zls_s),
1262 Some(wls_s),
1263 ) = (
1264 self.y.as_slice_memory_order(),
1265 self.weights.as_slice_memory_order(),
1266 etamu.as_slice_memory_order(),
1267 eta_log_sigma.as_slice_memory_order(),
1268 zmu.as_slice_memory_order_mut(),
1269 wmu.as_slice_memory_order_mut(),
1270 z_ls.as_slice_memory_order_mut(),
1271 w_ls.as_slice_memory_order_mut(),
1272 ) {
1273 ll += zmu_s
1277 .par_chunks_mut(CHUNK)
1278 .zip(wmu_s.par_chunks_mut(CHUNK))
1279 .zip(zls_s.par_chunks_mut(CHUNK))
1280 .zip(wls_s.par_chunks_mut(CHUNK))
1281 .enumerate()
1282 .map(|(chunk_idx, (((zmu_c, wmu_c), zls_c), wls_c))| {
1283 let start = chunk_idx * CHUNK;
1284 let mut local_ll = 0.0;
1285 for local in 0..zmu_c.len() {
1286 let i = start + local;
1287 let row =
1288 gaussian_diagonal_row_kernel(y_s[i], mu_s[i], ls_s[i], w_s[i], ln2pi);
1289 zmu_c[local] = mu_s[i] + row.location_working_shift;
1290 wmu_c[local] = row.location_working_weight;
1291 zls_c[local] = row.log_sigma_working_response;
1292 wls_c[local] = row.log_sigma_working_weight;
1293 local_ll += row.log_likelihood;
1294 }
1295 local_ll
1296 })
1297 .sum::<f64>();
1298 } else {
1299 let y_view = self.y.view();
1302 let w_view = self.weights.view();
1303 let mu_view = etamu.view();
1304 let ls_view = eta_log_sigma.view();
1305 let zmu_s = zmu
1306 .as_slice_memory_order_mut()
1307 .expect("zeros is contiguous");
1308 let wmu_s = wmu
1309 .as_slice_memory_order_mut()
1310 .expect("zeros is contiguous");
1311 let zls_s = z_ls
1312 .as_slice_memory_order_mut()
1313 .expect("zeros is contiguous");
1314 let wls_s = w_ls
1315 .as_slice_memory_order_mut()
1316 .expect("zeros is contiguous");
1317 ll += zmu_s
1318 .par_chunks_mut(CHUNK)
1319 .zip(wmu_s.par_chunks_mut(CHUNK))
1320 .zip(zls_s.par_chunks_mut(CHUNK))
1321 .zip(wls_s.par_chunks_mut(CHUNK))
1322 .enumerate()
1323 .map(|(chunk_idx, (((zmu_c, wmu_c), zls_c), wls_c))| {
1324 let start = chunk_idx * CHUNK;
1325 let mut local_ll = 0.0;
1326 for local in 0..zmu_c.len() {
1327 let i = start + local;
1328 let row = gaussian_diagonal_row_kernel(
1329 y_view[i], mu_view[i], ls_view[i], w_view[i], ln2pi,
1330 );
1331 zmu_c[local] = mu_view[i] + row.location_working_shift;
1332 wmu_c[local] = row.location_working_weight;
1333 zls_c[local] = row.log_sigma_working_response;
1334 wls_c[local] = row.log_sigma_working_weight;
1335 local_ll += row.log_likelihood;
1336 }
1337 local_ll
1338 })
1339 .sum::<f64>();
1340 }
1341
1342 Ok(FamilyEvaluation {
1343 log_likelihood: ll,
1344 blockworking_sets: vec![
1345 BlockWorkingSet::diagonal_checked(zmu, wmu)?,
1346 BlockWorkingSet::diagonal_checked(z_ls, w_ls)?,
1347 ],
1348 })
1349 }
1350
1351 fn log_likelihood_only(&self, block_states: &[ParameterBlockState]) -> Result<f64, String> {
1352 validate_block_count::<GamlssError>("GaussianLocationScaleFamily", 2, block_states.len())?;
1353 let n = self.y.len();
1354 let etamu = &block_states[Self::BLOCK_MU].eta;
1355 let eta_log_sigma = &block_states[Self::BLOCK_LOG_SIGMA].eta;
1356 if etamu.len() != n || eta_log_sigma.len() != n || self.weights.len() != n {
1357 return Err(GamlssError::DimensionMismatch {
1358 reason: "GaussianLocationScaleFamily input size mismatch".to_string(),
1359 }
1360 .into());
1361 }
1362 let ln2pi = (2.0 * std::f64::consts::PI).ln();
1366 let mut ll = 0.0;
1367 if let (Some(y_s), Some(w_s), Some(mu_s), Some(ls_s)) = (
1368 self.y.as_slice_memory_order(),
1369 self.weights.as_slice_memory_order(),
1370 etamu.as_slice_memory_order(),
1371 eta_log_sigma.as_slice_memory_order(),
1372 ) {
1373 use rayon::iter::{IntoParallelIterator, ParallelIterator};
1374 ll += (0..n)
1375 .into_par_iter()
1376 .map(|i| {
1377 let wi = w_s[i];
1378 if wi == 0.0 {
1379 return 0.0;
1380 }
1381 let sigma_i = logb_sigma_from_eta_scalar(ls_s[i]);
1382 let inv_s2 = (sigma_i * sigma_i).recip();
1383 let r = y_s[i] - mu_s[i];
1384 wi * (-0.5 * (r * r * inv_s2 + ln2pi + 2.0 * sigma_i.ln()))
1385 })
1386 .sum::<f64>();
1387 } else {
1388 use rayon::iter::{IntoParallelIterator, ParallelIterator};
1389 ll += (0..n)
1390 .into_par_iter()
1391 .map(|i| {
1392 let wi = self.weights[i];
1393 if wi == 0.0 {
1394 return 0.0;
1395 }
1396 let sigma_i = logb_sigma_from_eta_scalar(eta_log_sigma[i]);
1397 let inv_s2 = (sigma_i * sigma_i).recip();
1398 let r = self.y[i] - etamu[i];
1399 wi * (-0.5 * (r * r * inv_s2 + ln2pi + 2.0 * sigma_i.ln()))
1400 })
1401 .sum::<f64>();
1402 }
1403 Ok(ll)
1404 }
1405
1406 fn log_likelihood_only_with_options(
1417 &self,
1418 block_states: &[ParameterBlockState],
1419 options: &BlockwiseFitOptions,
1420 ) -> Result<f64, String> {
1421 let Some(subsample) = options.outer_score_subsample.as_ref() else {
1422 return self.log_likelihood_only(block_states);
1423 };
1424 validate_block_count::<GamlssError>("GaussianLocationScaleFamily", 2, block_states.len())?;
1425 let n = self.y.len();
1426 let etamu = &block_states[Self::BLOCK_MU].eta;
1427 let eta_log_sigma = &block_states[Self::BLOCK_LOG_SIGMA].eta;
1428 if etamu.len() != n || eta_log_sigma.len() != n || self.weights.len() != n {
1429 return Err(GamlssError::DimensionMismatch {
1430 reason: "GaussianLocationScaleFamily input size mismatch".to_string(),
1431 }
1432 .into());
1433 }
1434 let ln2pi = (2.0 * std::f64::consts::PI).ln();
1435 use rayon::iter::ParallelIterator;
1436 let ll: f64 = subsample
1437 .rows
1438 .par_iter()
1439 .map(|row| {
1440 let i = row.index;
1441 let wi = self.weights[i];
1442 if wi == 0.0 {
1443 return 0.0;
1444 }
1445 let sigma_i = logb_sigma_from_eta_scalar(eta_log_sigma[i]);
1446 let inv_s2 = (sigma_i * sigma_i).recip();
1447 let r = self.y[i] - etamu[i];
1448 row.weight * wi * (-0.5 * (r * r * inv_s2 + ln2pi + 2.0 * sigma_i.ln()))
1449 })
1450 .sum();
1451 Ok(ll)
1452 }
1453
1454 fn exact_newton_joint_hessian(
1455 &self,
1456 block_states: &[ParameterBlockState],
1457 ) -> Result<Option<Array2<f64>>, String> {
1458 self.exact_newton_joint_hessian_for_specs(block_states, None)
1459 }
1460
1461 fn exact_newton_joint_gradient_evaluation(
1462 &self,
1463 block_states: &[ParameterBlockState],
1464 specs: &[ParameterBlockSpec],
1465 ) -> Result<Option<ExactNewtonJointGradientEvaluation>, String> {
1466 self.exact_newton_joint_gradient_for_specs(block_states, Some(specs))
1467 }
1468
1469 fn has_explicit_joint_hessian(&self) -> bool {
1470 true
1471 }
1472
1473 fn joint_jeffreys_term_required(&self) -> bool {
1489 false
1490 }
1491
1492 fn exact_newton_joint_hessian_directional_derivative(
1493 &self,
1494 block_states: &[ParameterBlockState],
1495 d_beta_flat: &Array1<f64>,
1496 ) -> Result<Option<Array2<f64>>, String> {
1497 self.exact_newton_joint_hessian_directional_derivative_for_specs(
1498 block_states,
1499 None,
1500 d_beta_flat,
1501 )
1502 }
1503
1504 fn exact_newton_joint_hessiansecond_directional_derivative(
1505 &self,
1506 block_states: &[ParameterBlockState],
1507 d_beta_u_flat: &Array1<f64>,
1508 d_betav_flat: &Array1<f64>,
1509 ) -> Result<Option<Array2<f64>>, String> {
1510 self.exact_newton_joint_hessian_second_directional_derivative_for_specs(
1511 block_states,
1512 None,
1513 d_beta_u_flat,
1514 d_betav_flat,
1515 )
1516 }
1517
1518 fn diagonalworking_weights_directional_derivative(
1519 &self,
1520 block_states: &[ParameterBlockState],
1521 block_idx: usize,
1522 d_eta: &Array1<f64>,
1523 ) -> Result<Option<Array1<f64>>, String> {
1524 validate_block_count::<GamlssError>("GaussianLocationScaleFamily", 2, block_states.len())?;
1525 let n = self.y.len();
1526 let eta_t = &block_states[Self::BLOCK_MU].eta;
1527 let eta_ls = &block_states[Self::BLOCK_LOG_SIGMA].eta;
1528 if eta_t.len() != n || eta_ls.len() != n || self.weights.len() != n || d_eta.len() != n {
1529 return Err(GamlssError::DimensionMismatch {
1530 reason: "GaussianLocationScaleFamily input size mismatch".to_string(),
1531 }
1532 .into());
1533 }
1534
1535 let sigma = eta_ls.mapv(logb_sigma_from_eta_scalar);
1536 let mut dw = Array1::<f64>::zeros(n);
1537 match block_idx {
1538 Self::BLOCK_MU => {
1539 Ok(Some(dw))
1547 }
1548 Self::BLOCK_LOG_SIGMA => {
1549 use rayon::iter::{IntoParallelIterator, ParallelIterator};
1572 let dw_vec: Vec<f64> = (0..n)
1573 .into_par_iter()
1574 .map(|i| {
1575 let d1 = crate::sigma_link::logb_sigma_jet1_scalar(eta_ls[i]).d1;
1576 gaussian_log_sigma_irlsinfo_directional_derivative(
1577 self.weights[i],
1578 sigma[i],
1579 d1,
1580 d_eta[i],
1581 )
1582 })
1583 .collect();
1584 for (i, v) in dw_vec.into_iter().enumerate() {
1585 dw[i] = v;
1586 }
1587 Ok(Some(dw))
1588 }
1589 _ => Ok(None),
1590 }
1591 }
1592
1593 fn exact_newton_joint_hessian_with_specs(
1594 &self,
1595 block_states: &[ParameterBlockState],
1596 specs: &[ParameterBlockSpec],
1597 ) -> Result<Option<Array2<f64>>, String> {
1598 self.exact_newton_joint_hessian_for_specs(block_states, Some(specs))
1599 }
1600
1601 fn exact_newton_joint_hessian_directional_derivative_with_specs(
1602 &self,
1603 block_states: &[ParameterBlockState],
1604 specs: &[ParameterBlockSpec],
1605 d_beta_flat: &Array1<f64>,
1606 ) -> Result<Option<Array2<f64>>, String> {
1607 self.exact_newton_joint_hessian_directional_derivative_for_specs(
1608 block_states,
1609 Some(specs),
1610 d_beta_flat,
1611 )
1612 }
1613
1614 fn exact_newton_joint_hessian_second_directional_derivative_with_specs(
1615 &self,
1616 block_states: &[ParameterBlockState],
1617 specs: &[ParameterBlockSpec],
1618 d_beta_u_flat: &Array1<f64>,
1619 d_betav_flat: &Array1<f64>,
1620 ) -> Result<Option<Array2<f64>>, String> {
1621 self.exact_newton_joint_hessian_second_directional_derivative_for_specs(
1622 block_states,
1623 Some(specs),
1624 d_beta_u_flat,
1625 d_betav_flat,
1626 )
1627 }
1628
1629 fn exact_newton_joint_psi_terms(
1630 &self,
1631 block_states: &[ParameterBlockState],
1632 specs: &[ParameterBlockSpec],
1633 derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
1634 psi_index: usize,
1635 ) -> Result<Option<crate::custom_family::ExactNewtonJointPsiTerms>, String> {
1636 self.exact_newton_joint_psi_terms_for_specs(
1637 block_states,
1638 specs,
1639 derivative_blocks,
1640 psi_index,
1641 )
1642 }
1643
1644 fn exact_newton_joint_psisecond_order_terms(
1645 &self,
1646 block_states: &[ParameterBlockState],
1647 specs: &[ParameterBlockSpec],
1648 derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
1649 psi_i: usize,
1650 psi_j: usize,
1651 ) -> Result<Option<crate::custom_family::ExactNewtonJointPsiSecondOrderTerms>, String> {
1652 self.exact_newton_joint_psisecond_order_terms_for_specs(
1653 block_states,
1654 specs,
1655 derivative_blocks,
1656 psi_i,
1657 psi_j,
1658 )
1659 }
1660
1661 fn exact_newton_joint_psihessian_directional_derivative(
1662 &self,
1663 block_states: &[ParameterBlockState],
1664 specs: &[ParameterBlockSpec],
1665 derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
1666 psi_index: usize,
1667 d_beta_flat: &Array1<f64>,
1668 ) -> Result<Option<Array2<f64>>, String> {
1669 self.exact_newton_joint_psihessian_directional_derivative_for_specs(
1670 block_states,
1671 specs,
1672 derivative_blocks,
1673 psi_index,
1674 d_beta_flat,
1675 )
1676 }
1677
1678 fn exact_newton_joint_psi_workspace(
1679 &self,
1680 block_states: &[ParameterBlockState],
1681 specs: &[ParameterBlockSpec],
1682 derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
1683 ) -> Result<Option<Arc<dyn ExactNewtonJointPsiWorkspace>>, String> {
1684 validate_block_count::<GamlssError>("GaussianLocationScaleFamily", 2, block_states.len())?;
1685 if specs.len() != 2 || derivative_blocks.len() != 2 {
1686 return Err(GamlssError::DimensionMismatch { reason: format!(
1687 "GaussianLocationScaleFamily joint psi workspace expects 2 specs and 2 derivative block lists, got {} / {}",
1688 specs.len(),
1689 derivative_blocks.len()
1690 ) }.into());
1691 }
1692 Ok(Some(Arc::new(
1693 GaussianLocationScaleExactNewtonJointPsiWorkspace::new(
1694 self.clone(),
1695 block_states.to_vec(),
1696 specs,
1697 derivative_blocks.to_vec(),
1698 )?,
1699 )))
1700 }
1701
1702 fn exact_newton_joint_psi_workspace_with_options(
1720 &self,
1721 block_states: &[ParameterBlockState],
1722 specs: &[ParameterBlockSpec],
1723 derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
1724 options: &BlockwiseFitOptions,
1725 ) -> Result<Option<Arc<dyn ExactNewtonJointPsiWorkspace>>, String> {
1726 validate_block_count::<GamlssError>("GaussianLocationScaleFamily", 2, block_states.len())?;
1727 if specs.len() != 2 || derivative_blocks.len() != 2 {
1728 return Err(GamlssError::DimensionMismatch { reason: format!(
1729 "GaussianLocationScaleFamily joint psi workspace expects 2 specs and 2 derivative block lists, got {} / {}",
1730 specs.len(),
1731 derivative_blocks.len()
1732 ) }.into());
1733 }
1734 Ok(Some(Arc::new(
1735 GaussianLocationScaleExactNewtonJointPsiWorkspace::new_with_subsample(
1736 self.clone(),
1737 block_states.to_vec(),
1738 specs,
1739 derivative_blocks.to_vec(),
1740 options.outer_score_subsample.clone(),
1741 )?,
1742 )))
1743 }
1744
1745 fn exact_newton_joint_hessian_workspace(
1746 &self,
1747 block_states: &[ParameterBlockState],
1748 specs: &[ParameterBlockSpec],
1749 ) -> Result<Option<Arc<dyn ExactNewtonJointHessianWorkspace>>, String> {
1750 let Some((xmu, x_ls)) = self.exact_joint_dense_block_designs(Some(specs))? else {
1751 return Ok(None);
1752 };
1753 let workspace = GaussianLocationScaleHessianWorkspace::new(
1754 self.clone(),
1755 block_states.to_vec(),
1756 xmu.into_owned(),
1757 x_ls.into_owned(),
1758 )?;
1759 Ok(Some(Arc::new(workspace)))
1760 }
1761
1762 fn exact_newton_joint_hessian_workspace_with_options(
1776 &self,
1777 block_states: &[ParameterBlockState],
1778 specs: &[ParameterBlockSpec],
1779 options: &BlockwiseFitOptions,
1780 ) -> Result<Option<Arc<dyn ExactNewtonJointHessianWorkspace>>, String> {
1781 let Some((xmu, x_ls)) = self.exact_joint_dense_block_designs(Some(specs))? else {
1782 return Ok(None);
1783 };
1784 let mut workspace = GaussianLocationScaleHessianWorkspace::new(
1785 self.clone(),
1786 block_states.to_vec(),
1787 xmu.into_owned(),
1788 x_ls.into_owned(),
1789 )?;
1790 if let Some(subsample) = options.outer_score_subsample.as_ref() {
1791 workspace.apply_outer_subsample(subsample.rows.as_ref());
1792 }
1793 Ok(Some(Arc::new(workspace)))
1794 }
1795
1796 fn inner_coefficient_hessian_hvp_available(&self, specs: &[ParameterBlockSpec]) -> bool {
1797 self.exact_joint_supported()
1803 && matches!(
1804 self.exact_joint_dense_block_designs(Some(specs)),
1805 Ok(Some(_))
1806 )
1807 }
1808
1809 fn outer_derivative_subsample_capable(&self) -> bool {
1831 true
1832 }
1833}
1834
1835impl CustomFamilyGenerative for GaussianLocationScaleFamily {
1836 fn generativespec(
1837 &self,
1838 block_states: &[ParameterBlockState],
1839 ) -> Result<GenerativeSpec, String> {
1840 validate_block_count::<GamlssError>("GaussianLocationScaleFamily", 2, block_states.len())?;
1841 let mu = block_states[Self::BLOCK_MU].eta.clone();
1842 let eta_log_sigma = &block_states[Self::BLOCK_LOG_SIGMA].eta;
1843 let sigma = gamlss_rowwise_map(eta_log_sigma.len(), |i| {
1844 logb_sigma_from_eta_scalar(eta_log_sigma[i])
1845 });
1846 Ok(GenerativeSpec {
1847 mean: mu,
1848 noise: NoiseModel::Gaussian { sigma },
1849 })
1850 }
1851}