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 derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
354 psi_index: usize,
355 ) -> Result<Option<gam_problem::ExactNewtonJointPsiTerms>, String> {
356 let Some((xmu, x_ls)) = self.exact_joint_dense_block_designs(Some(specs))? else {
357 return Ok(None);
358 };
359 self.exact_newton_joint_psi_terms_from_designs(
360 block_states,
361 specs,
362 derivative_blocks,
363 psi_index,
364 &xmu,
365 &x_ls,
366 )
367 }
368
369 pub(crate) fn exact_newton_joint_psisecond_order_terms_for_specs(
370 &self,
371 block_states: &[ParameterBlockState],
372 specs: &[ParameterBlockSpec],
373 derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
374 psi_i: usize,
375 psi_j: usize,
376 ) -> Result<Option<gam_problem::ExactNewtonJointPsiSecondOrderTerms>, String> {
377 let Some((xmu, x_ls)) = self.exact_joint_dense_block_designs(Some(specs))? else {
378 return Ok(None);
379 };
380 self.exact_newton_joint_psisecond_order_terms_from_designs(
381 block_states,
382 derivative_blocks,
383 psi_i,
384 psi_j,
385 &xmu,
386 &x_ls,
387 )
388 }
389
390 pub(crate) fn exact_newton_joint_psihessian_directional_derivative_for_specs(
391 &self,
392 block_states: &[ParameterBlockState],
393 specs: &[ParameterBlockSpec],
394 derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
395 psi_index: usize,
396 d_beta_flat: &Array1<f64>,
397 ) -> Result<Option<Array2<f64>>, String> {
398 let Some((xmu, x_ls)) = self.exact_joint_dense_block_designs(Some(specs))? else {
399 return Ok(None);
400 };
401 self.exact_newton_joint_psihessian_directional_derivative_from_designs(
402 block_states,
403 derivative_blocks,
404 psi_index,
405 d_beta_flat,
406 &xmu,
407 &x_ls,
408 )
409 }
410
411 pub(crate) fn exact_newton_joint_hessian_from_designs(
412 &self,
413 block_states: &[ParameterBlockState],
414 xmu: &DenseOrOperator<'_>,
415 x_ls: &DenseOrOperator<'_>,
416 ) -> Result<Option<Array2<f64>>, String> {
417 validate_block_count::<GamlssError>("GaussianLocationScaleFamily", 2, block_states.len())?;
418 let n = self.y.len();
419 let etamu = &block_states[Self::BLOCK_MU].eta;
420 let eta_ls = &block_states[Self::BLOCK_LOG_SIGMA].eta;
421 if etamu.len() != n || eta_ls.len() != n || self.weights.len() != n {
422 return Err(GamlssError::DimensionMismatch {
423 reason: "GaussianLocationScaleFamily input size mismatch".to_string(),
424 }
425 .into());
426 }
427
428 let rows = self.get_or_compute_row_scalars(etamu, eta_ls)?;
429 let (mm, cross, scale) = gaussian_locscale_observed_joint_row_coeffs(&rows);
434 Ok(Some(gaussian_joint_hessian_from_designs(
435 xmu, x_ls, &mm, &cross, &scale,
436 )?))
437 }
438
439 pub(crate) fn exact_newton_joint_hessian_directional_derivative_from_designs(
440 &self,
441 block_states: &[ParameterBlockState],
442 xmu: &DenseOrOperator<'_>,
443 x_ls: &DenseOrOperator<'_>,
444 d_beta_flat: &Array1<f64>,
445 ) -> Result<Option<Array2<f64>>, String> {
446 validate_block_count::<GamlssError>("GaussianLocationScaleFamily", 2, block_states.len())?;
447 let n = self.y.len();
448 let etamu = &block_states[Self::BLOCK_MU].eta;
449 let eta_ls = &block_states[Self::BLOCK_LOG_SIGMA].eta;
450 if etamu.len() != n || eta_ls.len() != n || self.weights.len() != n {
451 return Err(GamlssError::DimensionMismatch {
452 reason: "GaussianLocationScaleFamily input size mismatch".to_string(),
453 }
454 .into());
455 }
456
457 let pmu = xmu.ncols();
458 let p_ls = x_ls.ncols();
459 let total = pmu + p_ls;
460 if d_beta_flat.len() != total {
461 return Err(GamlssError::DimensionMismatch {
462 reason: format!(
463 "GaussianLocationScaleFamily joint d_beta length mismatch: got {}, expected {}",
464 d_beta_flat.len(),
465 total
466 ),
467 }
468 .into());
469 }
470 let ximu = xmu.dot(d_beta_flat.slice(s![0..pmu]));
471 let xi_ls = x_ls.dot(d_beta_flat.slice(s![pmu..pmu + p_ls]));
472 let rows = self.get_or_compute_row_scalars(etamu, eta_ls)?;
473 let directional = gaussian_joint_first_directionalweights(&rows, &ximu, &xi_ls);
474 let dhmumu = directional.0;
475 let dh_ls_ls = directional.2;
476 let dhmu_ls = directional.1;
484
485 Ok(Some(gaussian_joint_hessian_from_designs(
486 xmu, x_ls, &dhmumu, &dhmu_ls, &dh_ls_ls,
487 )?))
488 }
489
490 pub(crate) fn exact_newton_joint_hessiansecond_directional_derivative_from_designs(
491 &self,
492 block_states: &[ParameterBlockState],
493 xmu: &DenseOrOperator<'_>,
494 x_ls: &DenseOrOperator<'_>,
495 d_beta_u_flat: &Array1<f64>,
496 d_betav_flat: &Array1<f64>,
497 ) -> Result<Option<Array2<f64>>, String> {
498 validate_block_count::<GamlssError>("GaussianLocationScaleFamily", 2, block_states.len())?;
499 let n = self.y.len();
500 let etamu = &block_states[Self::BLOCK_MU].eta;
501 let eta_ls = &block_states[Self::BLOCK_LOG_SIGMA].eta;
502 if etamu.len() != n || eta_ls.len() != n || self.weights.len() != n {
503 return Err(GamlssError::DimensionMismatch {
504 reason: "GaussianLocationScaleFamily input size mismatch".to_string(),
505 }
506 .into());
507 }
508
509 let pmu = xmu.ncols();
510 let p_ls = x_ls.ncols();
511 let total = pmu + p_ls;
512 if d_beta_u_flat.len() != total || d_betav_flat.len() != total {
513 return Err(GamlssError::DimensionMismatch { reason: format!(
514 "GaussianLocationScaleFamily joint second directional derivative length mismatch: got {} and {}, expected {}",
515 d_beta_u_flat.len(),
516 d_betav_flat.len(),
517 total
518 ) }.into());
519 }
520 let ximu_u = xmu.dot(d_beta_u_flat.slice(s![0..pmu]));
521 let xi_ls_u = x_ls.dot(d_beta_u_flat.slice(s![pmu..pmu + p_ls]));
522 let ximuv = xmu.dot(d_betav_flat.slice(s![0..pmu]));
523 let xi_lsv = x_ls.dot(d_betav_flat.slice(s![pmu..pmu + p_ls]));
524 let rows = self.get_or_compute_row_scalars(etamu, eta_ls)?;
525 let second =
526 gaussian_jointsecond_directionalweights(&rows, &ximu_u, &xi_ls_u, &ximuv, &xi_lsv);
527 let d2hmumu = second.0;
528 let d2h_ls_ls = second.2;
529 let d2hmu_ls = second.1;
535
536 Ok(Some(gaussian_joint_hessian_from_designs(
537 xmu, x_ls, &d2hmumu, &d2hmu_ls, &d2h_ls_ls,
538 )?))
539 }
540
541 pub(crate) fn exact_newton_joint_psi_direction(
542 &self,
543 block_states: &[ParameterBlockState],
544 derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
545 psi_index: usize,
546 xmu: &Array2<f64>,
547 x_ls: &Array2<f64>,
548 policy: &gam_runtime::resource::ResourcePolicy,
549 ) -> Result<Option<LocationScaleJointPsiDirection>, String> {
550 let Some(parts) = locscale_joint_psi_direction_parts(
551 block_states,
552 derivative_blocks,
553 psi_index,
554 self.y.len(),
555 xmu.ncols(),
556 x_ls.ncols(),
557 Self::BLOCK_MU,
558 Self::BLOCK_LOG_SIGMA,
559 2,
560 "GaussianLocationScaleFamily",
561 "mu",
562 policy,
563 )?
564 else {
565 return Ok(None);
566 };
567 Ok(Some(LocationScaleJointPsiDirection {
568 block_idx: parts.block_idx,
569 local_idx: parts.local_idx,
570 z_primary_psi: parts.primary_z,
571 z_ls_psi: parts.log_sigma_z,
572 x_primary_psi: parts.primary_psi,
573 x_ls_psi: parts.log_sigma_psi,
574 }))
575 }
576
577 pub(crate) fn exact_newton_joint_psisecond_design_drifts(
578 &self,
579 block_states: &[ParameterBlockState],
580 derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
581 psi_a: &LocationScaleJointPsiDirection,
582 psi_b: &LocationScaleJointPsiDirection,
583 xmu: &Array2<f64>,
584 x_ls: &Array2<f64>,
585 ) -> Result<LocationScaleJointPsiSecondDrifts, String> {
586 locscale_joint_psisecond_design_drifts(
587 block_states,
588 derivative_blocks,
589 psi_a,
590 psi_b,
591 LocScalePsiDriftConfig {
592 n: self.y.len(),
593 p_primary: xmu.ncols(),
594 p_log_sigma: x_ls.ncols(),
595 primary_block_idx: Self::BLOCK_MU,
596 log_sigma_block_idx: Self::BLOCK_LOG_SIGMA,
597 family_name: "GaussianLocationScaleFamily",
598 primary_label: "mu",
599 policy: &self.policy,
600 },
601 )
602 }
603
604 pub(crate) fn exact_newton_joint_psi_terms_from_designs(
605 &self,
606 block_states: &[ParameterBlockState],
607 specs: &[ParameterBlockSpec],
608 derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
609 psi_index: usize,
610 xmu: &Array2<f64>,
611 x_ls: &Array2<f64>,
612 ) -> Result<Option<gam_problem::ExactNewtonJointPsiTerms>, String> {
613 validate_block_count::<GamlssError>("GaussianLocationScaleFamily", 2, block_states.len())?;
614 if specs.len() != 2 || derivative_blocks.len() != 2 {
615 return Err(GamlssError::DimensionMismatch { reason: format!(
616 "GaussianLocationScaleFamily joint psi terms expect 2 specs and 2 derivative blocks, got {} and {}",
617 specs.len(),
618 derivative_blocks.len()
619 ) }.into());
620 }
621 let Some(dir_a) = self.exact_newton_joint_psi_direction(
622 block_states,
623 derivative_blocks,
624 psi_index,
625 xmu,
626 x_ls,
627 &self.policy,
628 )?
629 else {
630 return Ok(None);
631 };
632 let etamu = &block_states[Self::BLOCK_MU].eta;
664 let eta_ls = &block_states[Self::BLOCK_LOG_SIGMA].eta;
665 let rows = self.get_or_compute_row_scalars(etamu, eta_ls)?;
666 let weights_a =
667 gaussian_joint_psi_firstweights(&rows, &dir_a.z_primary_psi, &dir_a.z_ls_psi);
668 let objective_psi = weights_a.objective_psirow.sum();
669 let xmu_map = dir_a.x_primary_psi.as_linear_map_ref();
670 let x_ls_map = dir_a.x_ls_psi.as_linear_map_ref();
671 let score_mu =
672 xmu_map.transpose_mul(weights_a.scoremu.view()) + fast_atv(xmu, &weights_a.dscoremu);
673 let score_ls = x_ls_map.transpose_mul(weights_a.score_ls.view())
674 + fast_atv(x_ls, &weights_a.dscore_ls);
675 let score_psi = gaussian_pack_joint_score(&score_mu, &score_ls);
676 let hessian_psi_operator = build_two_block_custom_family_joint_psi_operator_from_actions(
677 dir_a.x_primary_psi.cloned_first_action(),
678 dir_a.x_ls_psi.cloned_first_action(),
679 0..xmu.ncols(),
680 xmu.ncols()..xmu.ncols() + x_ls.ncols(),
681 xmu,
682 x_ls,
683 &weights_a.hmumu,
684 &weights_a.hmu_ls,
685 &weights_a.h_ls_ls,
686 &weights_a.dhmumu,
687 &weights_a.dhmu_ls,
688 &weights_a.dh_ls_ls,
689 )?;
690 let hessian_psi = if hessian_psi_operator.is_some() {
691 Array2::zeros((0, 0))
692 } else {
693 gaussian_joint_psihessian_fromweights(xmu, x_ls, xmu_map, x_ls_map, &weights_a)?
694 };
695
696 Ok(Some(gam_problem::ExactNewtonJointPsiTerms {
697 objective_psi,
698 score_psi,
699 hessian_psi,
700 hessian_psi_operator,
701 }))
702 }
703
704 pub(crate) fn exact_newton_joint_psisecond_order_terms_from_designs(
705 &self,
706 block_states: &[ParameterBlockState],
707 derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
708 psi_i: usize,
709 psi_j: usize,
710 xmu: &Array2<f64>,
711 x_ls: &Array2<f64>,
712 ) -> Result<Option<gam_problem::ExactNewtonJointPsiSecondOrderTerms>, String> {
713 let Some(dir_i) = self.exact_newton_joint_psi_direction(
714 block_states,
715 derivative_blocks,
716 psi_i,
717 xmu,
718 x_ls,
719 &self.policy,
720 )?
721 else {
722 return Ok(None);
723 };
724 let Some(dir_j) = self.exact_newton_joint_psi_direction(
725 block_states,
726 derivative_blocks,
727 psi_j,
728 xmu,
729 x_ls,
730 &self.policy,
731 )?
732 else {
733 return Ok(None);
734 };
735 Ok(Some(
736 self.exact_newton_joint_psisecond_order_terms_from_parts(
737 block_states,
738 derivative_blocks,
739 &dir_i,
740 &dir_j,
741 xmu,
742 x_ls,
743 None,
744 )?,
745 ))
746 }
747
748 pub(crate) fn exact_newton_joint_psisecond_order_terms_from_parts(
749 &self,
750 block_states: &[ParameterBlockState],
751 derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
752 dir_i: &LocationScaleJointPsiDirection,
753 dir_j: &LocationScaleJointPsiDirection,
754 xmu: &Array2<f64>,
755 x_ls: &Array2<f64>,
756 subsample: Option<&[crate::outer_subsample::WeightedOuterRow]>,
757 ) -> Result<gam_problem::ExactNewtonJointPsiSecondOrderTerms, String> {
758 let second_drifts = self.exact_newton_joint_psisecond_design_drifts(
759 block_states,
760 derivative_blocks,
761 dir_i,
762 dir_j,
763 xmu,
764 x_ls,
765 )?;
766 let n = self.y.len();
767 let xmu_i_map = dir_i.x_primary_psi.as_linear_map_ref();
768 let x_ls_i_map = dir_i.x_ls_psi.as_linear_map_ref();
769 let xmu_j_map = dir_j.x_primary_psi.as_linear_map_ref();
770 let x_ls_j_map = dir_j.x_ls_psi.as_linear_map_ref();
771 let xmu_ab_map = second_psi_linear_map(
772 second_drifts.x_primary_ab_action.as_ref(),
773 second_drifts.x_primary_ab.as_ref(),
774 n,
775 xmu.ncols(),
776 );
777 let x_ls_ab_map = second_psi_linear_map(
778 second_drifts.x_ls_ab_action.as_ref(),
779 second_drifts.x_ls_ab.as_ref(),
780 n,
781 x_ls.ncols(),
782 );
783 let etamu = &block_states[Self::BLOCK_MU].eta;
807 let eta_ls = &block_states[Self::BLOCK_LOG_SIGMA].eta;
808 let rows = self.get_or_compute_row_scalars(etamu, eta_ls)?;
809 let mut weights_i =
810 gaussian_joint_psi_firstweights(&rows, &dir_i.z_primary_psi, &dir_i.z_ls_psi);
811 let mut weights_j =
812 gaussian_joint_psi_firstweights(&rows, &dir_j.z_primary_psi, &dir_j.z_ls_psi);
813 let mut secondweights = gaussian_joint_psisecondweights(
814 &rows,
815 &dir_i.z_primary_psi,
816 &dir_i.z_ls_psi,
817 &dir_j.z_primary_psi,
818 &dir_j.z_ls_psi,
819 &second_drifts.z_primary_ab,
820 &second_drifts.z_ls_ab,
821 );
822 if let Some(sub_rows) = subsample {
823 apply_ht_mask_first(&mut weights_i, sub_rows);
829 apply_ht_mask_first(&mut weights_j, sub_rows);
830 apply_ht_mask_second(&mut secondweights, sub_rows);
831 }
832 let objective_psi_psi = secondweights.objective_psi_psirow.sum();
833
834 let score_psi_psi = gaussian_pack_joint_score(
835 &(xmu_ab_map.transpose_mul(weights_i.scoremu.view())
836 + xmu_i_map.transpose_mul(weights_j.dscoremu.view())
837 + xmu_j_map.transpose_mul(weights_i.dscoremu.view())
838 + fast_atv(xmu, &secondweights.d2scoremu)),
839 &(x_ls_ab_map.transpose_mul(weights_i.score_ls.view())
840 + x_ls_i_map.transpose_mul(weights_j.dscore_ls.view())
841 + x_ls_j_map.transpose_mul(weights_i.dscore_ls.view())
842 + fast_atv(x_ls, &secondweights.d2score_ls)),
843 );
844 let hessian_psi_psi = gaussian_joint_psisecondhessian_fromweights(
845 xmu,
846 x_ls,
847 xmu_i_map,
848 x_ls_i_map,
849 xmu_j_map,
850 x_ls_j_map,
851 xmu_ab_map,
852 x_ls_ab_map,
853 &weights_i,
854 &weights_j,
855 &secondweights,
856 )?;
857
858 Ok(gam_problem::ExactNewtonJointPsiSecondOrderTerms {
859 objective_psi_psi,
860 score_psi_psi,
861 hessian_psi_psi,
862 hessian_psi_psi_operator: None,
863 })
864 }
865
866 pub(crate) fn exact_newton_joint_psihessian_directional_derivative_from_designs(
867 &self,
868 block_states: &[ParameterBlockState],
869 derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
870 psi_index: usize,
871 d_beta_flat: &Array1<f64>,
872 xmu: &Array2<f64>,
873 x_ls: &Array2<f64>,
874 ) -> Result<Option<Array2<f64>>, String> {
875 let Some(dir_a) = self.exact_newton_joint_psi_direction(
876 block_states,
877 derivative_blocks,
878 psi_index,
879 xmu,
880 x_ls,
881 &self.policy,
882 )?
883 else {
884 return Ok(None);
885 };
886 Ok(Some(
887 self.exact_newton_joint_psihessian_directional_derivative_from_parts(
888 block_states,
889 &dir_a,
890 d_beta_flat,
891 xmu,
892 x_ls,
893 None,
894 )?,
895 ))
896 }
897
898 pub(crate) fn exact_newton_joint_psihessian_directional_derivative_from_parts(
899 &self,
900 block_states: &[ParameterBlockState],
901 dir_a: &LocationScaleJointPsiDirection,
902 d_beta_flat: &Array1<f64>,
903 xmu: &Array2<f64>,
904 x_ls: &Array2<f64>,
905 subsample: Option<&[crate::outer_subsample::WeightedOuterRow]>,
906 ) -> Result<Array2<f64>, String> {
907 let etamu = &block_states[Self::BLOCK_MU].eta;
908 let eta_ls = &block_states[Self::BLOCK_LOG_SIGMA].eta;
909 let pmu = xmu.ncols();
910 let p_ls = x_ls.ncols();
911 let xmu_map = dir_a.x_primary_psi.as_linear_map_ref();
912 let x_ls_map = dir_a.x_ls_psi.as_linear_map_ref();
913 let total = pmu + p_ls;
914 if d_beta_flat.len() != total {
915 return Err(GamlssError::DimensionMismatch { reason: format!(
916 "GaussianLocationScaleFamily joint psi hessian directional derivative length mismatch: got {}, expected {}",
917 d_beta_flat.len(),
918 total
919 ) }.into());
920 }
921 let u_mu = d_beta_flat.slice(s![0..pmu]);
926 let u_ls = d_beta_flat.slice(s![pmu..pmu + p_ls]);
927 let xi_mu = fast_av(xmu, &u_mu);
928 let xi_ls = fast_av(x_ls, &u_ls);
929 let uza_mu = xmu_map.forward_mul(u_mu);
930 let uza_ls = x_ls_map.forward_mul(u_ls);
931 let rows = self.get_or_compute_row_scalars(etamu, eta_ls)?;
973 let mut mixedweights = gaussian_joint_psi_mixed_driftweights(
974 &rows,
975 &xi_mu,
976 &xi_ls,
977 &dir_a.z_primary_psi,
978 &dir_a.z_ls_psi,
979 &uza_mu,
980 &uza_ls,
981 );
982 if let Some(sub_rows) = subsample {
983 apply_ht_mask_mixed(&mut mixedweights, sub_rows);
988 }
989
990 gaussian_joint_psi_mixedhessian_drift_fromweights(
991 xmu,
992 x_ls,
993 xmu_map,
994 x_ls_map,
995 &mixedweights,
996 )
997 }
998
999 pub fn block_effective_jacobian(
1006 specs: &[ParameterBlockSpec],
1007 block_idx: usize,
1008 ) -> Result<Box<dyn BlockEffectiveJacobian>, String> {
1009 crate::block_layout::block_jacobian::AdditiveWiggleBlockLayout {
1010 family: "GaussianLocationScaleFamily",
1011 n_outputs: 2,
1012 additive_blocks: &[Self::BLOCK_MU, Self::BLOCK_LOG_SIGMA],
1013 wiggle_block: None,
1014 }
1015 .block_effective_jacobian(specs, block_idx)
1016 }
1017}
1018
1019pub struct GaussianLocationScaleChannelHessian {
1039 pub(crate) h: ndarray::Array3<f64>,
1041}
1042
1043impl GaussianLocationScaleChannelHessian {
1044 pub fn from_pilot_observed_unclamped(
1057 y: &ndarray::Array1<f64>,
1058 w: &ndarray::Array1<f64>,
1059 eta_mu: &ndarray::Array1<f64>,
1060 eta_log_sigma: &ndarray::Array1<f64>,
1061 ) -> Result<Self, String> {
1062 let n = y.len();
1063 if w.len() != n || eta_mu.len() != n || eta_log_sigma.len() != n {
1064 return Err(format!(
1065 "GaussianLocationScaleChannelHessian::from_pilot_observed_unclamped: \
1066 length mismatch y={n} w={} eta_mu={} eta_log_sigma={}",
1067 w.len(),
1068 eta_mu.len(),
1069 eta_log_sigma.len(),
1070 ));
1071 }
1072 let mut h = ndarray::Array3::<f64>::zeros((n, 2, 2));
1073 for i in 0..n {
1074 let wi = w[i];
1075 let mu_i = eta_mu[i];
1076 let s_i = eta_log_sigma[i];
1077 let inv_sigma2 = (-2.0 * s_i).exp();
1078 let resid = y[i] - mu_i;
1079 h[[i, 0, 0]] = wi * inv_sigma2;
1080 h[[i, 1, 1]] = wi * 2.0 * resid * resid * inv_sigma2;
1081 h[[i, 0, 1]] = wi * 2.0 * resid * inv_sigma2;
1082 h[[i, 1, 0]] = h[[i, 0, 1]];
1083 }
1084 Ok(Self { h })
1085 }
1086
1087 pub fn from_pilot(
1095 y: &ndarray::Array1<f64>,
1096 w: &ndarray::Array1<f64>,
1097 eta_mu: &ndarray::Array1<f64>,
1098 eta_log_sigma: &ndarray::Array1<f64>,
1099 ) -> Result<Self, String> {
1100 let n = y.len();
1101 if w.len() != n || eta_mu.len() != n || eta_log_sigma.len() != n {
1102 return Err(format!(
1103 "GaussianLocationScaleChannelHessian::from_pilot: \
1104 length mismatch y={n} w={} eta_mu={} eta_log_sigma={}",
1105 w.len(),
1106 eta_mu.len(),
1107 eta_log_sigma.len(),
1108 ));
1109 }
1110 let mut h = ndarray::Array3::<f64>::zeros((n, 2, 2));
1111 for i in 0..n {
1112 let wi = w[i];
1113 let mu_i = eta_mu[i];
1114 let s_i = eta_log_sigma[i];
1115 let inv_sigma2 = (-2.0 * s_i).exp(); let resid = y[i] - mu_i;
1117 let h00 = wi * inv_sigma2;
1119 let h11 = wi * 2.0 * resid * resid * inv_sigma2;
1120 let h01 = wi * 2.0 * resid * inv_sigma2;
1121 let (e0, e1, u1_0, u1_1, u2_0, u2_1) = psd_clamp_2x2(h00, h01, h11);
1126 h[[i, 0, 0]] = e0 * u1_0 * u1_0 + e1 * u2_0 * u2_0;
1127 h[[i, 0, 1]] = e0 * u1_0 * u1_1 + e1 * u2_0 * u2_1;
1128 h[[i, 1, 0]] = h[[i, 0, 1]];
1129 h[[i, 1, 1]] = e0 * u1_1 * u1_1 + e1 * u2_1 * u2_1;
1130 }
1131 Ok(Self { h })
1132 }
1133}
1134
1135impl FamilyChannelHessian for GaussianLocationScaleChannelHessian {
1136 fn n_outputs(&self) -> usize {
1137 2
1138 }
1139
1140 fn n_subjects(&self) -> usize {
1141 self.h.shape()[0]
1142 }
1143
1144 fn fill_subject(&self, i: usize, out: &mut [f64]) {
1145 assert_eq!(out.len(), 4);
1146 out[0] = self.h[[i, 0, 0]];
1147 out[1] = self.h[[i, 0, 1]];
1148 out[2] = self.h[[i, 1, 0]];
1149 out[3] = self.h[[i, 1, 1]];
1150 }
1151
1152 fn evaluate_full(&self) -> ndarray::Array3<f64> {
1153 self.h.clone()
1154 }
1155}
1156
1157impl CustomFamily for GaussianLocationScaleFamily {
1158 fn exact_newton_joint_hessian_beta_dependent(&self) -> bool {
1170 true
1171 }
1172
1173 fn outer_seed_config(&self, n_params: usize) -> crate::seeding::SeedConfig {
1196 if n_params == 0 {
1197 return crate::seeding::SeedConfig::default();
1198 }
1199 let mut config = crate::seeding::SeedConfig::default();
1200 config.risk_profile = crate::seeding::SeedRiskProfile::GaussianLocationScale;
1201 config.max_seeds = 4;
1202 config.seed_budget = 2;
1203 config
1204 }
1205
1206 fn output_channel_assignment(&self, specs: &[ParameterBlockSpec]) -> Option<Vec<usize>> {
1213 Some(
1216 (0..specs.len())
1217 .map(|i| usize::from(i == Self::BLOCK_LOG_SIGMA))
1218 .collect(),
1219 )
1220 }
1221
1222 fn coefficient_hessian_cost(&self, specs: &[ParameterBlockSpec]) -> u64 {
1223 crate::location_scale_engine::location_scale_coefficient_hessian_cost(
1231 self.y.len() as u64,
1232 specs,
1233 )
1234 }
1235
1236 fn evaluate(&self, block_states: &[ParameterBlockState]) -> Result<FamilyEvaluation, String> {
1237 validate_block_count::<GamlssError>("GaussianLocationScaleFamily", 2, block_states.len())?;
1238 let n = self.y.len();
1239 let etamu = &block_states[Self::BLOCK_MU].eta;
1240 let eta_log_sigma = &block_states[Self::BLOCK_LOG_SIGMA].eta;
1241 if etamu.len() != n || eta_log_sigma.len() != n || self.weights.len() != n {
1242 return Err(GamlssError::DimensionMismatch {
1243 reason: "GaussianLocationScaleFamily input size mismatch".to_string(),
1244 }
1245 .into());
1246 }
1247
1248 let mut zmu = Array1::<f64>::zeros(n);
1260 let mut wmu = Array1::<f64>::zeros(n);
1261 let mut z_ls = Array1::<f64>::zeros(n);
1262 let mut w_ls = Array1::<f64>::zeros(n);
1263 let ln2pi = (2.0 * std::f64::consts::PI).ln();
1264 let mut ll = 0.0;
1265
1266 const CHUNK: usize = 1024;
1267 if let (
1268 Some(y_s),
1269 Some(w_s),
1270 Some(mu_s),
1271 Some(ls_s),
1272 Some(zmu_s),
1273 Some(wmu_s),
1274 Some(zls_s),
1275 Some(wls_s),
1276 ) = (
1277 self.y.as_slice_memory_order(),
1278 self.weights.as_slice_memory_order(),
1279 etamu.as_slice_memory_order(),
1280 eta_log_sigma.as_slice_memory_order(),
1281 zmu.as_slice_memory_order_mut(),
1282 wmu.as_slice_memory_order_mut(),
1283 z_ls.as_slice_memory_order_mut(),
1284 w_ls.as_slice_memory_order_mut(),
1285 ) {
1286 ll += zmu_s
1290 .par_chunks_mut(CHUNK)
1291 .zip(wmu_s.par_chunks_mut(CHUNK))
1292 .zip(zls_s.par_chunks_mut(CHUNK))
1293 .zip(wls_s.par_chunks_mut(CHUNK))
1294 .enumerate()
1295 .map(|(chunk_idx, (((zmu_c, wmu_c), zls_c), wls_c))| {
1296 let start = chunk_idx * CHUNK;
1297 let mut local_ll = 0.0;
1298 for local in 0..zmu_c.len() {
1299 let i = start + local;
1300 let row =
1301 gaussian_diagonal_row_kernel(y_s[i], mu_s[i], ls_s[i], w_s[i], ln2pi);
1302 zmu_c[local] = mu_s[i] + row.location_working_shift;
1303 wmu_c[local] = row.location_working_weight;
1304 zls_c[local] = row.log_sigma_working_response;
1305 wls_c[local] = row.log_sigma_working_weight;
1306 local_ll += row.log_likelihood;
1307 }
1308 local_ll
1309 })
1310 .sum::<f64>();
1311 } else {
1312 let y_view = self.y.view();
1315 let w_view = self.weights.view();
1316 let mu_view = etamu.view();
1317 let ls_view = eta_log_sigma.view();
1318 let zmu_s = zmu
1319 .as_slice_memory_order_mut()
1320 .expect("zeros is contiguous");
1321 let wmu_s = wmu
1322 .as_slice_memory_order_mut()
1323 .expect("zeros is contiguous");
1324 let zls_s = z_ls
1325 .as_slice_memory_order_mut()
1326 .expect("zeros is contiguous");
1327 let wls_s = w_ls
1328 .as_slice_memory_order_mut()
1329 .expect("zeros is contiguous");
1330 ll += zmu_s
1331 .par_chunks_mut(CHUNK)
1332 .zip(wmu_s.par_chunks_mut(CHUNK))
1333 .zip(zls_s.par_chunks_mut(CHUNK))
1334 .zip(wls_s.par_chunks_mut(CHUNK))
1335 .enumerate()
1336 .map(|(chunk_idx, (((zmu_c, wmu_c), zls_c), wls_c))| {
1337 let start = chunk_idx * CHUNK;
1338 let mut local_ll = 0.0;
1339 for local in 0..zmu_c.len() {
1340 let i = start + local;
1341 let row = gaussian_diagonal_row_kernel(
1342 y_view[i], mu_view[i], ls_view[i], w_view[i], ln2pi,
1343 );
1344 zmu_c[local] = mu_view[i] + row.location_working_shift;
1345 wmu_c[local] = row.location_working_weight;
1346 zls_c[local] = row.log_sigma_working_response;
1347 wls_c[local] = row.log_sigma_working_weight;
1348 local_ll += row.log_likelihood;
1349 }
1350 local_ll
1351 })
1352 .sum::<f64>();
1353 }
1354
1355 Ok(FamilyEvaluation {
1356 log_likelihood: ll,
1357 blockworking_sets: vec![
1358 BlockWorkingSet::diagonal_checked(zmu, wmu)?,
1359 BlockWorkingSet::diagonal_checked(z_ls, w_ls)?,
1360 ],
1361 })
1362 }
1363
1364 fn log_likelihood_only(&self, block_states: &[ParameterBlockState]) -> Result<f64, String> {
1365 validate_block_count::<GamlssError>("GaussianLocationScaleFamily", 2, block_states.len())?;
1366 let n = self.y.len();
1367 let etamu = &block_states[Self::BLOCK_MU].eta;
1368 let eta_log_sigma = &block_states[Self::BLOCK_LOG_SIGMA].eta;
1369 if etamu.len() != n || eta_log_sigma.len() != n || self.weights.len() != n {
1370 return Err(GamlssError::DimensionMismatch {
1371 reason: "GaussianLocationScaleFamily input size mismatch".to_string(),
1372 }
1373 .into());
1374 }
1375 let ln2pi = (2.0 * std::f64::consts::PI).ln();
1379 let mut ll = 0.0;
1380 if let (Some(y_s), Some(w_s), Some(mu_s), Some(ls_s)) = (
1381 self.y.as_slice_memory_order(),
1382 self.weights.as_slice_memory_order(),
1383 etamu.as_slice_memory_order(),
1384 eta_log_sigma.as_slice_memory_order(),
1385 ) {
1386 use rayon::iter::{IntoParallelIterator, ParallelIterator};
1387 ll += (0..n)
1388 .into_par_iter()
1389 .map(|i| {
1390 let wi = w_s[i];
1391 if wi == 0.0 {
1392 return 0.0;
1393 }
1394 let sigma_i = logb_sigma_from_eta_scalar(ls_s[i]);
1395 let inv_s2 = (sigma_i * sigma_i).recip();
1396 let r = y_s[i] - mu_s[i];
1397 wi * (-0.5 * (r * r * inv_s2 + ln2pi + 2.0 * sigma_i.ln()))
1398 })
1399 .sum::<f64>();
1400 } else {
1401 use rayon::iter::{IntoParallelIterator, ParallelIterator};
1402 ll += (0..n)
1403 .into_par_iter()
1404 .map(|i| {
1405 let wi = self.weights[i];
1406 if wi == 0.0 {
1407 return 0.0;
1408 }
1409 let sigma_i = logb_sigma_from_eta_scalar(eta_log_sigma[i]);
1410 let inv_s2 = (sigma_i * sigma_i).recip();
1411 let r = self.y[i] - etamu[i];
1412 wi * (-0.5 * (r * r * inv_s2 + ln2pi + 2.0 * sigma_i.ln()))
1413 })
1414 .sum::<f64>();
1415 }
1416 Ok(ll)
1417 }
1418
1419 fn log_likelihood_only_with_options(
1430 &self,
1431 block_states: &[ParameterBlockState],
1432 options: &BlockwiseFitOptions,
1433 ) -> Result<f64, String> {
1434 let Some(subsample) = options.outer_score_subsample.as_ref() else {
1435 return self.log_likelihood_only(block_states);
1436 };
1437 validate_block_count::<GamlssError>("GaussianLocationScaleFamily", 2, block_states.len())?;
1438 let n = self.y.len();
1439 let etamu = &block_states[Self::BLOCK_MU].eta;
1440 let eta_log_sigma = &block_states[Self::BLOCK_LOG_SIGMA].eta;
1441 if etamu.len() != n || eta_log_sigma.len() != n || self.weights.len() != n {
1442 return Err(GamlssError::DimensionMismatch {
1443 reason: "GaussianLocationScaleFamily input size mismatch".to_string(),
1444 }
1445 .into());
1446 }
1447 let ln2pi = (2.0 * std::f64::consts::PI).ln();
1448 use rayon::iter::ParallelIterator;
1449 let ll: f64 = subsample
1450 .rows
1451 .par_iter()
1452 .map(|row| {
1453 let i = row.index;
1454 let wi = self.weights[i];
1455 if wi == 0.0 {
1456 return 0.0;
1457 }
1458 let sigma_i = logb_sigma_from_eta_scalar(eta_log_sigma[i]);
1459 let inv_s2 = (sigma_i * sigma_i).recip();
1460 let r = self.y[i] - etamu[i];
1461 row.weight * wi * (-0.5 * (r * r * inv_s2 + ln2pi + 2.0 * sigma_i.ln()))
1462 })
1463 .sum();
1464 Ok(ll)
1465 }
1466
1467 fn exact_newton_joint_hessian(
1468 &self,
1469 block_states: &[ParameterBlockState],
1470 ) -> Result<Option<Array2<f64>>, String> {
1471 self.exact_newton_joint_hessian_for_specs(block_states, None)
1472 }
1473
1474 fn exact_newton_joint_gradient_evaluation(
1475 &self,
1476 block_states: &[ParameterBlockState],
1477 specs: &[ParameterBlockSpec],
1478 ) -> Result<Option<ExactNewtonJointGradientEvaluation>, String> {
1479 self.exact_newton_joint_gradient_for_specs(block_states, Some(specs))
1480 }
1481
1482 fn has_explicit_joint_hessian(&self) -> bool {
1483 true
1484 }
1485
1486 fn joint_jeffreys_term_required(&self) -> bool {
1502 false
1503 }
1504
1505 fn exact_newton_joint_hessian_directional_derivative(
1506 &self,
1507 block_states: &[ParameterBlockState],
1508 d_beta_flat: &Array1<f64>,
1509 ) -> Result<Option<Array2<f64>>, String> {
1510 self.exact_newton_joint_hessian_directional_derivative_for_specs(
1511 block_states,
1512 None,
1513 d_beta_flat,
1514 )
1515 }
1516
1517 fn exact_newton_joint_hessiansecond_directional_derivative(
1518 &self,
1519 block_states: &[ParameterBlockState],
1520 d_beta_u_flat: &Array1<f64>,
1521 d_betav_flat: &Array1<f64>,
1522 ) -> Result<Option<Array2<f64>>, String> {
1523 self.exact_newton_joint_hessian_second_directional_derivative_for_specs(
1524 block_states,
1525 None,
1526 d_beta_u_flat,
1527 d_betav_flat,
1528 )
1529 }
1530
1531 fn diagonalworking_weights_directional_derivative(
1532 &self,
1533 block_states: &[ParameterBlockState],
1534 block_idx: usize,
1535 d_eta: &Array1<f64>,
1536 ) -> Result<Option<Array1<f64>>, String> {
1537 validate_block_count::<GamlssError>("GaussianLocationScaleFamily", 2, block_states.len())?;
1538 let n = self.y.len();
1539 let eta_t = &block_states[Self::BLOCK_MU].eta;
1540 let eta_ls = &block_states[Self::BLOCK_LOG_SIGMA].eta;
1541 if eta_t.len() != n || eta_ls.len() != n || self.weights.len() != n || d_eta.len() != n {
1542 return Err(GamlssError::DimensionMismatch {
1543 reason: "GaussianLocationScaleFamily input size mismatch".to_string(),
1544 }
1545 .into());
1546 }
1547
1548 let sigma = eta_ls.mapv(logb_sigma_from_eta_scalar);
1549 let mut dw = Array1::<f64>::zeros(n);
1550 match block_idx {
1551 Self::BLOCK_MU => {
1552 Ok(Some(dw))
1560 }
1561 Self::BLOCK_LOG_SIGMA => {
1562 use rayon::iter::{IntoParallelIterator, ParallelIterator};
1585 let dw_vec: Vec<f64> = (0..n)
1586 .into_par_iter()
1587 .map(|i| {
1588 let d1 = crate::sigma_link::logb_sigma_jet1_scalar(eta_ls[i]).d1;
1589 gaussian_log_sigma_irlsinfo_directional_derivative(
1590 self.weights[i],
1591 sigma[i],
1592 d1,
1593 d_eta[i],
1594 )
1595 })
1596 .collect();
1597 for (i, v) in dw_vec.into_iter().enumerate() {
1598 dw[i] = v;
1599 }
1600 Ok(Some(dw))
1601 }
1602 _ => Ok(None),
1603 }
1604 }
1605
1606 fn exact_newton_joint_hessian_with_specs(
1607 &self,
1608 block_states: &[ParameterBlockState],
1609 specs: &[ParameterBlockSpec],
1610 ) -> Result<Option<Array2<f64>>, String> {
1611 self.exact_newton_joint_hessian_for_specs(block_states, Some(specs))
1612 }
1613
1614 fn exact_newton_joint_hessian_directional_derivative_with_specs(
1615 &self,
1616 block_states: &[ParameterBlockState],
1617 specs: &[ParameterBlockSpec],
1618 d_beta_flat: &Array1<f64>,
1619 ) -> Result<Option<Array2<f64>>, String> {
1620 self.exact_newton_joint_hessian_directional_derivative_for_specs(
1621 block_states,
1622 Some(specs),
1623 d_beta_flat,
1624 )
1625 }
1626
1627 fn exact_newton_joint_hessian_second_directional_derivative_with_specs(
1628 &self,
1629 block_states: &[ParameterBlockState],
1630 specs: &[ParameterBlockSpec],
1631 d_beta_u_flat: &Array1<f64>,
1632 d_betav_flat: &Array1<f64>,
1633 ) -> Result<Option<Array2<f64>>, String> {
1634 self.exact_newton_joint_hessian_second_directional_derivative_for_specs(
1635 block_states,
1636 Some(specs),
1637 d_beta_u_flat,
1638 d_betav_flat,
1639 )
1640 }
1641
1642 fn exact_newton_joint_psi_terms(
1643 &self,
1644 block_states: &[ParameterBlockState],
1645 specs: &[ParameterBlockSpec],
1646 derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
1647 psi_index: usize,
1648 ) -> Result<Option<gam_problem::ExactNewtonJointPsiTerms>, String> {
1649 self.exact_newton_joint_psi_terms_for_specs(
1650 block_states,
1651 specs,
1652 derivative_blocks,
1653 psi_index,
1654 )
1655 }
1656
1657 fn exact_newton_joint_psisecond_order_terms(
1658 &self,
1659 block_states: &[ParameterBlockState],
1660 specs: &[ParameterBlockSpec],
1661 derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
1662 psi_i: usize,
1663 psi_j: usize,
1664 ) -> Result<Option<gam_problem::ExactNewtonJointPsiSecondOrderTerms>, String> {
1665 self.exact_newton_joint_psisecond_order_terms_for_specs(
1666 block_states,
1667 specs,
1668 derivative_blocks,
1669 psi_i,
1670 psi_j,
1671 )
1672 }
1673
1674 fn exact_newton_joint_psihessian_directional_derivative(
1675 &self,
1676 block_states: &[ParameterBlockState],
1677 specs: &[ParameterBlockSpec],
1678 derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
1679 psi_index: usize,
1680 d_beta_flat: &Array1<f64>,
1681 ) -> Result<Option<Array2<f64>>, String> {
1682 self.exact_newton_joint_psihessian_directional_derivative_for_specs(
1683 block_states,
1684 specs,
1685 derivative_blocks,
1686 psi_index,
1687 d_beta_flat,
1688 )
1689 }
1690
1691 fn exact_newton_joint_psi_workspace(
1692 &self,
1693 block_states: &[ParameterBlockState],
1694 specs: &[ParameterBlockSpec],
1695 derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
1696 ) -> Result<Option<Arc<dyn ExactNewtonJointPsiWorkspace>>, String> {
1697 validate_block_count::<GamlssError>("GaussianLocationScaleFamily", 2, block_states.len())?;
1698 if specs.len() != 2 || derivative_blocks.len() != 2 {
1699 return Err(GamlssError::DimensionMismatch { reason: format!(
1700 "GaussianLocationScaleFamily joint psi workspace expects 2 specs and 2 derivative block lists, got {} / {}",
1701 specs.len(),
1702 derivative_blocks.len()
1703 ) }.into());
1704 }
1705 Ok(Some(Arc::new(
1706 GaussianLocationScaleExactNewtonJointPsiWorkspace::new(
1707 self.clone(),
1708 block_states.to_vec(),
1709 specs,
1710 derivative_blocks.to_vec(),
1711 )?,
1712 )))
1713 }
1714
1715 fn exact_newton_joint_psi_workspace_with_options(
1733 &self,
1734 block_states: &[ParameterBlockState],
1735 specs: &[ParameterBlockSpec],
1736 derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
1737 options: &BlockwiseFitOptions,
1738 ) -> Result<Option<Arc<dyn ExactNewtonJointPsiWorkspace>>, String> {
1739 validate_block_count::<GamlssError>("GaussianLocationScaleFamily", 2, block_states.len())?;
1740 if specs.len() != 2 || derivative_blocks.len() != 2 {
1741 return Err(GamlssError::DimensionMismatch { reason: format!(
1742 "GaussianLocationScaleFamily joint psi workspace expects 2 specs and 2 derivative block lists, got {} / {}",
1743 specs.len(),
1744 derivative_blocks.len()
1745 ) }.into());
1746 }
1747 Ok(Some(Arc::new(
1748 GaussianLocationScaleExactNewtonJointPsiWorkspace::new_with_subsample(
1749 self.clone(),
1750 block_states.to_vec(),
1751 specs,
1752 derivative_blocks.to_vec(),
1753 options.outer_score_subsample.clone(),
1754 )?,
1755 )))
1756 }
1757
1758 fn exact_newton_joint_hessian_workspace(
1759 &self,
1760 block_states: &[ParameterBlockState],
1761 specs: &[ParameterBlockSpec],
1762 ) -> Result<Option<Arc<dyn ExactNewtonJointHessianWorkspace>>, String> {
1763 let Some((xmu, x_ls)) = self.exact_joint_dense_block_designs(Some(specs))? else {
1764 return Ok(None);
1765 };
1766 let workspace = GaussianLocationScaleHessianWorkspace::new(
1767 self.clone(),
1768 block_states.to_vec(),
1769 xmu.into_owned(),
1770 x_ls.into_owned(),
1771 )?;
1772 Ok(Some(Arc::new(workspace)))
1773 }
1774
1775 fn exact_newton_joint_hessian_workspace_with_options(
1789 &self,
1790 block_states: &[ParameterBlockState],
1791 specs: &[ParameterBlockSpec],
1792 options: &BlockwiseFitOptions,
1793 ) -> Result<Option<Arc<dyn ExactNewtonJointHessianWorkspace>>, String> {
1794 let Some((xmu, x_ls)) = self.exact_joint_dense_block_designs(Some(specs))? else {
1795 return Ok(None);
1796 };
1797 let mut workspace = GaussianLocationScaleHessianWorkspace::new(
1798 self.clone(),
1799 block_states.to_vec(),
1800 xmu.into_owned(),
1801 x_ls.into_owned(),
1802 )?;
1803 if let Some(subsample) = options.outer_score_subsample.as_ref() {
1804 workspace.apply_outer_subsample(subsample.rows.as_ref());
1805 }
1806 Ok(Some(Arc::new(workspace)))
1807 }
1808
1809 fn inner_coefficient_hessian_hvp_available(&self, specs: &[ParameterBlockSpec]) -> bool {
1810 self.exact_joint_supported()
1816 && matches!(
1817 self.exact_joint_dense_block_designs(Some(specs)),
1818 Ok(Some(_))
1819 )
1820 }
1821
1822 fn outer_derivative_subsample_capable(&self) -> bool {
1844 true
1845 }
1846}
1847
1848impl CustomFamilyGenerative for GaussianLocationScaleFamily {
1849 fn generativespec(
1850 &self,
1851 block_states: &[ParameterBlockState],
1852 ) -> Result<GenerativeSpec, String> {
1853 validate_block_count::<GamlssError>("GaussianLocationScaleFamily", 2, block_states.len())?;
1854 let mu = block_states[Self::BLOCK_MU].eta.clone();
1855 let eta_log_sigma = &block_states[Self::BLOCK_LOG_SIGMA].eta;
1856 let sigma = gamlss_rowwise_map(eta_log_sigma.len(), |i| {
1857 logb_sigma_from_eta_scalar(eta_log_sigma[i])
1858 });
1859 Ok(GenerativeSpec {
1860 mean: mu,
1861 noise: NoiseModel::Gaussian { sigma },
1862 })
1863 }
1864}