1use super::*;
6use gam_row_macros::row_atom;
7
8row_atom! {
17 fn gaussian_normalized_row [generic, order2_at_zero, third_at_zero, fourth_at_zero](
18 delta_mu,
19 delta_eta;
20 obs_weight: f64,
21 standardized_residual: f64,
22 inv_sigma: f64,
23 kappa: f64
24 ) {
25 obs_weight * ln((1.0 - kappa) + kappa * exp(delta_eta))
26 + 0.5
27 * obs_weight
28 * (standardized_residual - delta_mu * inv_sigma)
29 * (standardized_residual - delta_mu * inv_sigma)
30 / ((1.0 - kappa) + kappa * exp(delta_eta))
31 / ((1.0 - kappa) + kappa * exp(delta_eta))
32 }
33}
34
35pub(crate) struct LocationScaleJointPsiDirection {
36 pub(crate) block_idx: usize,
37 pub(crate) local_idx: usize,
38 pub(crate) x_primary_psi: PsiDesignMap,
39 pub(crate) x_ls_psi: PsiDesignMap,
40 pub(crate) z_primary_psi: Array1<f64>,
41 pub(crate) z_ls_psi: Array1<f64>,
42}
43
44pub(crate) struct LocationScaleJointPsiSecondDrifts {
45 pub(crate) x_primary_ab_action: Option<CustomFamilyPsiSecondDesignAction>,
46 pub(crate) x_ls_ab_action: Option<CustomFamilyPsiSecondDesignAction>,
47 pub(crate) x_primary_ab: Option<Array2<f64>>,
48 pub(crate) x_ls_ab: Option<Array2<f64>>,
49 pub(crate) z_primary_ab: Array1<f64>,
50 pub(crate) z_ls_ab: Array1<f64>,
51}
52
53pub(crate) trait LocationScaleJointPsiFamily: Clone + Send + Sync + 'static {
69
70 const LABEL: &'static str;
73
74 fn ws_policy(&self) -> &gam_runtime::resource::ResourcePolicy;
75
76 fn ws_exact_joint_dense_block_designs<'a>(
77 &'a self,
78 specs: Option<&'a [ParameterBlockSpec]>,
79 ) -> Result<Option<(Cow<'a, Array2<f64>>, Cow<'a, Array2<f64>>)>, String>;
80
81 fn ws_psi_direction(
82 &self,
83 block_states: &[ParameterBlockState],
84 derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
85 psi_index: usize,
86 design_loc: &Array2<f64>,
87 design_scale: &Array2<f64>,
88 policy: &gam_runtime::resource::ResourcePolicy,
89 ) -> Result<Option<LocationScaleJointPsiDirection>, String>;
90
91 fn ws_psi_second_order_terms_from_parts(
92 &self,
93 block_states: &[ParameterBlockState],
94 derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
95 psi_a: &LocationScaleJointPsiDirection,
96 psi_b: &LocationScaleJointPsiDirection,
97 design_loc: &Array2<f64>,
98 design_scale: &Array2<f64>,
99 subsample: Option<&[crate::outer_subsample::WeightedOuterRow]>,
100 ) -> Result<ExactNewtonJointPsiSecondOrderTerms, String>;
101
102 fn ws_psi_hessian_directional_from_parts(
103 &self,
104 block_states: &[ParameterBlockState],
105 psi_dir: &LocationScaleJointPsiDirection,
106 d_beta_flat: &Array1<f64>,
107 design_loc: &Array2<f64>,
108 design_scale: &Array2<f64>,
109 subsample: Option<&[crate::outer_subsample::WeightedOuterRow]>,
110 ) -> Result<Array2<f64>, String>;
111
112 const PRIMARY_LABEL: &'static str;
117
118 const PRIMARY_BLOCK: usize;
123
124 const LOG_SIGMA_BLOCK: usize = 1;
127
128 const N_BLOCKS: usize;
131
132 fn ws_n_obs(&self) -> usize;
134
135 fn exact_newton_joint_psi_direction(
143 &self,
144 block_states: &[ParameterBlockState],
145 derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
146 psi_index: usize,
147 x_primary: &Array2<f64>,
148 x_ls: &Array2<f64>,
149 policy: &gam_runtime::resource::ResourcePolicy,
150 ) -> Result<Option<LocationScaleJointPsiDirection>, String> {
151 let Some(parts) = locscale_joint_psi_direction_parts(
152 block_states,
153 derivative_blocks,
154 psi_index,
155 self.ws_n_obs(),
156 x_primary.ncols(),
157 x_ls.ncols(),
158 Self::PRIMARY_BLOCK,
159 Self::LOG_SIGMA_BLOCK,
160 Self::N_BLOCKS,
161 Self::LABEL,
162 Self::PRIMARY_LABEL,
163 policy,
164 )?
165 else {
166 return Ok(None);
167 };
168 Ok(Some(LocationScaleJointPsiDirection {
169 block_idx: parts.block_idx,
170 local_idx: parts.local_idx,
171 z_primary_psi: parts.primary_z,
172 z_ls_psi: parts.log_sigma_z,
173 x_primary_psi: parts.primary_psi,
174 x_ls_psi: parts.log_sigma_psi,
175 }))
176 }
177
178 fn exact_newton_joint_psisecond_design_drifts(
184 &self,
185 block_states: &[ParameterBlockState],
186 derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
187 psi_a: &LocationScaleJointPsiDirection,
188 psi_b: &LocationScaleJointPsiDirection,
189 x_primary: &Array2<f64>,
190 x_ls: &Array2<f64>,
191 ) -> Result<LocationScaleJointPsiSecondDrifts, String> {
192 locscale_joint_psisecond_design_drifts(
193 block_states,
194 derivative_blocks,
195 psi_a,
196 psi_b,
197 LocScalePsiDriftConfig {
198 n: self.ws_n_obs(),
199 p_primary: x_primary.ncols(),
200 p_log_sigma: x_ls.ncols(),
201 primary_block_idx: Self::PRIMARY_BLOCK,
202 log_sigma_block_idx: Self::LOG_SIGMA_BLOCK,
203 family_name: Self::LABEL,
204 primary_label: Self::PRIMARY_LABEL,
205 policy: self.ws_policy(),
206 },
207 )
208 }
209
210 fn exact_newton_joint_psihessian_directional_derivative_from_designs(
222 &self,
223 block_states: &[ParameterBlockState],
224 derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
225 psi_index: usize,
226 d_beta_flat: &Array1<f64>,
227 x_primary: &Array2<f64>,
228 x_ls: &Array2<f64>,
229 ) -> Result<Option<Array2<f64>>, String> {
230 let Some(dir_a) = self.exact_newton_joint_psi_direction(
231 block_states,
232 derivative_blocks,
233 psi_index,
234 x_primary,
235 x_ls,
236 self.ws_policy(),
237 )?
238 else {
239 return Ok(None);
240 };
241 Ok(Some(self.ws_psi_hessian_directional_from_parts(
242 block_states,
243 &dir_a,
244 d_beta_flat,
245 x_primary,
246 x_ls,
247 None,
248 )?))
249 }
250
251 fn exact_newton_joint_psihessian_directional_derivative_for_specs(
262 &self,
263 block_states: &[ParameterBlockState],
264 specs: &[ParameterBlockSpec],
265 derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
266 psi_index: usize,
267 d_beta_flat: &Array1<f64>,
268 ) -> Result<Option<Array2<f64>>, String> {
269 let Some((x_primary, x_ls)) = self.ws_exact_joint_dense_block_designs(Some(specs))? else {
270 return Ok(None);
271 };
272 self.exact_newton_joint_psihessian_directional_derivative_from_designs(
273 block_states,
274 derivative_blocks,
275 psi_index,
276 d_beta_flat,
277 &x_primary,
278 &x_ls,
279 )
280 }
281}
282
283impl LocationScaleJointPsiFamily for GaussianLocationScaleFamily {
284 const LABEL: &'static str = "GaussianLocationScaleFamily";
285
286 const PRIMARY_LABEL: &'static str = "mu";
287 const PRIMARY_BLOCK: usize = Self::BLOCK_MU;
288 const N_BLOCKS: usize = 2;
289
290 fn ws_n_obs(&self) -> usize {
291 self.y.len()
292 }
293
294 fn ws_policy(&self) -> &gam_runtime::resource::ResourcePolicy {
295 &self.policy
296 }
297
298 fn ws_exact_joint_dense_block_designs<'a>(
299 &'a self,
300 specs: Option<&'a [ParameterBlockSpec]>,
301 ) -> Result<Option<(Cow<'a, Array2<f64>>, Cow<'a, Array2<f64>>)>, String> {
302 self.exact_joint_dense_block_designs(specs)
303 }
304
305 fn ws_psi_direction(
306 &self,
307 block_states: &[ParameterBlockState],
308 derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
309 psi_index: usize,
310 design_loc: &Array2<f64>,
311 design_scale: &Array2<f64>,
312 policy: &gam_runtime::resource::ResourcePolicy,
313 ) -> Result<Option<LocationScaleJointPsiDirection>, String> {
314 self.exact_newton_joint_psi_direction(
315 block_states,
316 derivative_blocks,
317 psi_index,
318 design_loc,
319 design_scale,
320 policy,
321 )
322 }
323
324 fn ws_psi_second_order_terms_from_parts(
325 &self,
326 block_states: &[ParameterBlockState],
327 derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
328 psi_a: &LocationScaleJointPsiDirection,
329 psi_b: &LocationScaleJointPsiDirection,
330 design_loc: &Array2<f64>,
331 design_scale: &Array2<f64>,
332 subsample: Option<&[crate::outer_subsample::WeightedOuterRow]>,
333 ) -> Result<ExactNewtonJointPsiSecondOrderTerms, String> {
334 self.exact_newton_joint_psisecond_order_terms_from_parts(
335 block_states,
336 derivative_blocks,
337 psi_a,
338 psi_b,
339 design_loc,
340 design_scale,
341 subsample,
342 )
343 }
344
345 fn ws_psi_hessian_directional_from_parts(
346 &self,
347 block_states: &[ParameterBlockState],
348 psi_dir: &LocationScaleJointPsiDirection,
349 d_beta_flat: &Array1<f64>,
350 design_loc: &Array2<f64>,
351 design_scale: &Array2<f64>,
352 subsample: Option<&[crate::outer_subsample::WeightedOuterRow]>,
353 ) -> Result<Array2<f64>, String> {
354 self.exact_newton_joint_psihessian_directional_derivative_from_parts(
355 block_states,
356 psi_dir,
357 d_beta_flat,
358 design_loc,
359 design_scale,
360 subsample,
361 )
362 }
363}
364
365impl LocationScaleJointPsiFamily for GaussianLocationScaleWiggleFamily {
366 const LABEL: &'static str = "GaussianLocationScaleWiggleFamily";
367
368 const PRIMARY_LABEL: &'static str = "mu";
369 const PRIMARY_BLOCK: usize = Self::BLOCK_MU;
370 const N_BLOCKS: usize = 3;
371
372 fn ws_n_obs(&self) -> usize {
373 self.y.len()
374 }
375
376 fn ws_policy(&self) -> &gam_runtime::resource::ResourcePolicy {
377 &self.policy
378 }
379
380 fn ws_exact_joint_dense_block_designs<'a>(
381 &'a self,
382 specs: Option<&'a [ParameterBlockSpec]>,
383 ) -> Result<Option<(Cow<'a, Array2<f64>>, Cow<'a, Array2<f64>>)>, String> {
384 self.exact_joint_dense_block_designs(specs)
385 }
386
387 fn ws_psi_direction(
388 &self,
389 block_states: &[ParameterBlockState],
390 derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
391 psi_index: usize,
392 design_loc: &Array2<f64>,
393 design_scale: &Array2<f64>,
394 policy: &gam_runtime::resource::ResourcePolicy,
395 ) -> Result<Option<LocationScaleJointPsiDirection>, String> {
396 self.exact_newton_joint_psi_direction(
397 block_states,
398 derivative_blocks,
399 psi_index,
400 design_loc,
401 design_scale,
402 policy,
403 )
404 }
405
406 fn ws_psi_second_order_terms_from_parts(
407 &self,
408 block_states: &[ParameterBlockState],
409 derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
410 psi_a: &LocationScaleJointPsiDirection,
411 psi_b: &LocationScaleJointPsiDirection,
412 design_loc: &Array2<f64>,
413 design_scale: &Array2<f64>,
414 subsample_rows: Option<&[crate::outer_subsample::WeightedOuterRow]>,
415 ) -> Result<ExactNewtonJointPsiSecondOrderTerms, String> {
416 if let Some(rows) = subsample_rows {
420 if rows.len() > design_loc.nrows() {
421 return Err(format!(
422 "ws_psi_second_order_terms_from_parts: {} weighted outer rows for a \
423 {}-row design",
424 rows.len(),
425 design_loc.nrows()
426 ));
427 }
428 }
429 self.exact_newton_joint_psisecond_order_terms_from_parts(
466 block_states,
467 derivative_blocks,
468 psi_a,
469 psi_b,
470 design_loc,
471 design_scale,
472 )
473 }
474
475 fn ws_psi_hessian_directional_from_parts(
476 &self,
477 block_states: &[ParameterBlockState],
478 psi_dir: &LocationScaleJointPsiDirection,
479 d_beta_flat: &Array1<f64>,
480 design_loc: &Array2<f64>,
481 design_scale: &Array2<f64>,
482 subsample_rows: Option<&[crate::outer_subsample::WeightedOuterRow]>,
483 ) -> Result<Array2<f64>, String> {
484 if let Some(rows) = subsample_rows {
486 if rows.len() > design_loc.nrows() {
487 return Err(format!(
488 "ws_psi_hessian_directional_from_parts: {} weighted outer rows for a \
489 {}-row design",
490 rows.len(),
491 design_loc.nrows()
492 ));
493 }
494 }
495 self.exact_newton_joint_psihessian_directional_derivative_from_parts(
500 block_states,
501 psi_dir,
502 d_beta_flat,
503 design_loc,
504 design_scale,
505 )
506 }
507}
508
509pub(crate) struct LocationScaleJointPsiWorkspace<F: LocationScaleJointPsiFamily> {
525 pub(crate) family: F,
526 pub(crate) block_states: Vec<ParameterBlockState>,
527 pub(crate) derivative_blocks: Vec<Vec<CustomFamilyBlockPsiDerivative>>,
528 pub(crate) design_loc: Arc<Array2<f64>>,
529 pub(crate) design_scale: Arc<Array2<f64>>,
530 pub(crate) psi_directions: ExactNewtonJointPsiDirectCache<LocationScaleJointPsiDirection>,
531 pub(crate) outer_score_subsample: Option<Arc<crate::outer_subsample::OuterScoreSubsample>>,
532}
533
534impl<F: LocationScaleJointPsiFamily> LocationScaleJointPsiWorkspace<F> {
535 pub(crate) fn new(
536 family: F,
537 block_states: Vec<ParameterBlockState>,
538 specs: &[ParameterBlockSpec],
539 derivative_blocks: Vec<Vec<CustomFamilyBlockPsiDerivative>>,
540 ) -> Result<Self, String> {
541 Self::new_with_subsample(family, block_states, specs, derivative_blocks, None)
542 }
543
544 pub(crate) fn new_with_subsample(
545 family: F,
546 block_states: Vec<ParameterBlockState>,
547 specs: &[ParameterBlockSpec],
548 derivative_blocks: Vec<Vec<CustomFamilyBlockPsiDerivative>>,
549 outer_score_subsample: Option<Arc<crate::outer_subsample::OuterScoreSubsample>>,
550 ) -> Result<Self, String> {
551 let Some((design_loc, design_scale)) =
552 family.ws_exact_joint_dense_block_designs(Some(specs))?
553 else {
554 return Err(GamlssError::UnsupportedConfiguration {
555 reason: format!(
556 "{} exact joint psi workspace requires dense block designs",
557 F::LABEL,
558 ),
559 }
560 .into());
561 };
562 let design_loc = shared_dense_arc(design_loc.as_ref());
563 let design_scale = shared_dense_arc(design_scale.as_ref());
564 let psi_dim = derivative_blocks.iter().map(Vec::len).sum();
565 Ok(Self {
566 family,
567 block_states,
568 derivative_blocks,
569 design_loc,
570 design_scale,
571 psi_directions: ExactNewtonJointPsiDirectCache::new(psi_dim),
572 outer_score_subsample,
573 })
574 }
575
576 pub(crate) fn psi_direction(
577 &self,
578 psi_index: usize,
579 ) -> Result<Option<Arc<LocationScaleJointPsiDirection>>, String> {
580 self.psi_directions
581 .get_or_try_init(psi_index, || {
582 self.family
583 .ws_psi_direction(
584 &self.block_states,
585 &self.derivative_blocks,
586 psi_index,
587 self.design_loc.as_ref(),
588 self.design_scale.as_ref(),
589 self.family.ws_policy(),
590 )
591 .map_err(gam_problem::CustomFamilyError::trial_point)
592 })
593 .map_err(|error| error.to_string())
596 }
597
598 pub(crate) fn subsample_rows(&self) -> Option<&[crate::outer_subsample::WeightedOuterRow]> {
599 self.outer_score_subsample
600 .as_ref()
601 .map(|s| s.rows.as_ref().as_slice())
602 }
603}
604
605impl<F> ExactNewtonJointPsiWorkspace for LocationScaleJointPsiWorkspace<F>
606where
607 F: LocationScaleJointPsiFamily,
608{
609 fn second_order_terms(
610 &self,
611 psi_i: usize,
612 psi_j: usize,
613 ) -> Result<Option<ExactNewtonJointPsiSecondOrderTerms>, String> {
614 let Some(dir_i) = self.psi_direction(psi_i)? else {
615 return Ok(None);
616 };
617 let Some(dir_j) = self.psi_direction(psi_j)? else {
618 return Ok(None);
619 };
620 Ok(Some(self.family.ws_psi_second_order_terms_from_parts(
621 &self.block_states,
622 &self.derivative_blocks,
623 dir_i.as_ref(),
624 dir_j.as_ref(),
625 self.design_loc.as_ref(),
626 self.design_scale.as_ref(),
627 self.subsample_rows(),
628 )?))
629 }
630
631 fn hessian_directional_derivative(
632 &self,
633 psi_index: usize,
634 d_beta_flat: &Array1<f64>,
635 ) -> Result<Option<gam_problem::DriftDerivResult>, String> {
636 let Some(dir) = self.psi_direction(psi_index)? else {
637 return Ok(None);
638 };
639 Ok(Some(gam_problem::DriftDerivResult::Dense(
640 self.family.ws_psi_hessian_directional_from_parts(
641 &self.block_states,
642 dir.as_ref(),
643 d_beta_flat,
644 self.design_loc.as_ref(),
645 self.design_scale.as_ref(),
646 self.subsample_rows(),
647 )?,
648 )))
649 }
650}
651
652pub(crate) type GaussianLocationScaleExactNewtonJointPsiWorkspace =
653 LocationScaleJointPsiWorkspace<GaussianLocationScaleFamily>;
654
655pub(crate) type GaussianLocationScaleWiggleExactNewtonJointPsiWorkspace =
656 LocationScaleJointPsiWorkspace<GaussianLocationScaleWiggleFamily>;
657
658#[derive(Clone)]
659pub struct GaussianJointRowScalars {
660 pub(crate) obs_weight: Array1<f64>,
661 pub(crate) standardized_residual: Array1<f64>,
663 pub(crate) inv_sigma: Array1<f64>,
665 pub(crate) kappa: Array1<f64>,
669}
670
671pub struct GaussianJointRowProgram<'a> {
680 rows: &'a GaussianJointRowScalars,
681}
682
683impl<'a> GaussianJointRowProgram<'a> {
684 pub fn new(rows: &'a GaussianJointRowScalars) -> Self {
686 Self { rows }
687 }
688
689 fn require_row(&self, row: usize) -> Result<(), String> {
690 if row >= self.rows.obs_weight.len() {
691 return Err(format!(
692 "GaussianJointRowProgram row {row} out of range for {} rows",
693 self.rows.obs_weight.len()
694 ));
695 }
696 Ok(())
697 }
698
699 #[inline(always)]
704 pub(crate) fn row_order2(
705 &self,
706 row: usize,
707 ) -> gam_math::jet_scalar::StaticOrder2Atom<2, 3, 3, 7> {
708 gaussian_normalized_row_order2_at_zero(
709 self.rows.obs_weight[row],
710 self.rows.standardized_residual[row],
711 self.rows.inv_sigma[row],
712 self.rows.kappa[row],
713 )
714 }
715
716 #[inline(always)]
718 pub(crate) fn row_third_contracted(&self, row: usize, direction: &[f64; 2]) -> [[f64; 2]; 2] {
719 gaussian_normalized_row_third_contracted_at_zero(
720 self.rows.obs_weight[row],
721 self.rows.standardized_residual[row],
722 self.rows.inv_sigma[row],
723 self.rows.kappa[row],
724 direction,
725 )
726 }
727
728 #[inline(always)]
730 pub(crate) fn row_fourth_contracted(
731 &self,
732 row: usize,
733 direction_u: &[f64; 2],
734 direction_v: &[f64; 2],
735 ) -> [[f64; 2]; 2] {
736 gaussian_normalized_row_fourth_contracted_at_zero(
737 self.rows.obs_weight[row],
738 self.rows.standardized_residual[row],
739 self.rows.inv_sigma[row],
740 self.rows.kappa[row],
741 direction_u,
742 direction_v,
743 )
744 }
745}
746
747#[inline(always)]
748fn matrix_vector_2(matrix: &[[f64; 2]; 2], vector: &[f64; 2]) -> [f64; 2] {
749 [
750 matrix[0][0] * vector[0] + matrix[0][1] * vector[1],
751 matrix[1][0] * vector[0] + matrix[1][1] * vector[1],
752 ]
753}
754
755#[inline(always)]
756fn dot_2(left: &[f64; 2], right: &[f64; 2]) -> f64 {
757 left[0] * right[0] + left[1] * right[1]
758}
759
760#[inline(always)]
761fn add_vector_2(left: [f64; 2], right: [f64; 2]) -> [f64; 2] {
762 [left[0] + right[0], left[1] + right[1]]
763}
764
765#[inline(always)]
766fn add_matrix_2(left: [[f64; 2]; 2], right: [[f64; 2]; 2]) -> [[f64; 2]; 2] {
767 [
768 [left[0][0] + right[0][0], left[0][1] + right[0][1]],
769 [left[1][0] + right[1][0], left[1][1] + right[1][1]],
770 ]
771}
772
773pub(crate) struct GaussianRowChannels {
777 pub(crate) gradient_mu: Array1<f64>,
778 pub(crate) gradient_ls: Array1<f64>,
779 pub(crate) hessian_mm: Array1<f64>,
780 pub(crate) hessian_ml: Array1<f64>,
781 pub(crate) hessian_ll: Array1<f64>,
782}
783
784impl GaussianRowChannels {
785 fn zeros(n: usize) -> Self {
786 Self {
787 gradient_mu: Array1::zeros(n),
788 gradient_ls: Array1::zeros(n),
789 hessian_mm: Array1::zeros(n),
790 hessian_ml: Array1::zeros(n),
791 hessian_ll: Array1::zeros(n),
792 }
793 }
794}
795
796pub(crate) struct GaussianRowFirstTower {
797 pub(crate) base: GaussianRowChannels,
798 pub(crate) first: GaussianRowChannels,
799}
800
801pub(crate) struct GaussianRowSecondTower {
802 pub(crate) base: GaussianRowChannels,
803 pub(crate) first_a: GaussianRowChannels,
804 pub(crate) first_b: GaussianRowChannels,
805 pub(crate) second: GaussianRowChannels,
806}
807
808fn write_base_channels(
809 channels: &mut GaussianRowChannels,
810 row: usize,
811 atom: &gam_math::jet_scalar::StaticOrder2Atom<2, 3, 3, 7>,
812) -> [[f64; 2]; 2] {
813 let gradient = atom.gradient();
814 let hessian = [
815 [atom.hessian_at(0, 0), atom.hessian_at(0, 1)],
816 [atom.hessian_at(1, 0), atom.hessian_at(1, 1)],
817 ];
818 channels.gradient_mu[row] = gradient[0];
819 channels.gradient_ls[row] = gradient[1];
820 channels.hessian_mm[row] = hessian[0][0];
821 channels.hessian_ml[row] = hessian[0][1];
822 channels.hessian_ll[row] = hessian[1][1];
823 hessian
824}
825
826fn write_directional_channels(
827 channels: &mut GaussianRowChannels,
828 row: usize,
829 gradient: [f64; 2],
830 hessian: [[f64; 2]; 2],
831) {
832 channels.gradient_mu[row] = gradient[0];
833 channels.gradient_ls[row] = gradient[1];
834 channels.hessian_mm[row] = hessian[0][0];
835 channels.hessian_ml[row] = hessian[0][1];
836 channels.hessian_ll[row] = hessian[1][1];
837}
838
839pub(crate) fn gaussian_row_channels(rows: &GaussianJointRowScalars) -> GaussianRowChannels {
841 let n = rows.obs_weight.len();
842 let program = GaussianJointRowProgram::new(rows);
843 let mut base = GaussianRowChannels::zeros(n);
844 for row in 0..n {
845 let atom = program.row_order2(row);
846 write_base_channels(&mut base, row, &atom);
847 }
848 base
849}
850
851pub(crate) fn gaussian_row_first_tower(
854 rows: &GaussianJointRowScalars,
855 direction_mu: &Array1<f64>,
856 direction_ls: &Array1<f64>,
857) -> GaussianRowFirstTower {
858 let n = rows.obs_weight.len();
859 let program = GaussianJointRowProgram::new(rows);
860 let mut base = GaussianRowChannels::zeros(n);
861 let mut first = GaussianRowChannels::zeros(n);
862 for row in 0..n {
863 let direction = [direction_mu[row], direction_ls[row]];
864 let atom = program.row_order2(row);
865 let hessian = write_base_channels(&mut base, row, &atom);
866 write_directional_channels(
867 &mut first,
868 row,
869 matrix_vector_2(&hessian, &direction),
870 program.row_third_contracted(row, &direction),
871 );
872 }
873 GaussianRowFirstTower { base, first }
874}
875
876pub(crate) fn gaussian_row_second_tower(
879 rows: &GaussianJointRowScalars,
880 direction_a_mu: &Array1<f64>,
881 direction_a_ls: &Array1<f64>,
882 direction_b_mu: &Array1<f64>,
883 direction_b_ls: &Array1<f64>,
884 direction_ab_mu: &Array1<f64>,
885 direction_ab_ls: &Array1<f64>,
886) -> GaussianRowSecondTower {
887 let n = rows.obs_weight.len();
888 let program = GaussianJointRowProgram::new(rows);
889 let mut base = GaussianRowChannels::zeros(n);
890 let mut first_a = GaussianRowChannels::zeros(n);
891 let mut first_b = GaussianRowChannels::zeros(n);
892 let mut second = GaussianRowChannels::zeros(n);
893 for row in 0..n {
894 let direction_a = [direction_a_mu[row], direction_a_ls[row]];
895 let direction_b = [direction_b_mu[row], direction_b_ls[row]];
896 let direction_ab = [direction_ab_mu[row], direction_ab_ls[row]];
897 let atom = program.row_order2(row);
898 let hessian = write_base_channels(&mut base, row, &atom);
899 let hessian_a = program.row_third_contracted(row, &direction_a);
900 let hessian_b = program.row_third_contracted(row, &direction_b);
901 write_directional_channels(
902 &mut first_a,
903 row,
904 matrix_vector_2(&hessian, &direction_a),
905 hessian_a,
906 );
907 write_directional_channels(
908 &mut first_b,
909 row,
910 matrix_vector_2(&hessian, &direction_b),
911 hessian_b,
912 );
913 write_directional_channels(
914 &mut second,
915 row,
916 add_vector_2(
917 matrix_vector_2(&hessian_a, &direction_b),
918 matrix_vector_2(&hessian, &direction_ab),
919 ),
920 add_matrix_2(
921 program.row_fourth_contracted(row, &direction_a, &direction_b),
922 program.row_third_contracted(row, &direction_ab),
923 ),
924 );
925 }
926 GaussianRowSecondTower {
927 base,
928 first_a,
929 first_b,
930 second,
931 }
932}
933
934impl gam_math::jet_tower::RowProgram<2> for GaussianJointRowProgram<'_> {
935 fn n_rows(&self) -> usize {
936 self.rows.obs_weight.len()
937 }
938
939 fn primaries(&self, row: usize) -> Result<[f64; 2], String> {
940 self.require_row(row)?;
941 Ok([0.0, 0.0])
942 }
943
944 fn eval<S: gam_math::jet_scalar::JetScalar<2>>(
945 &self,
946 row: usize,
947 p: &[S; 2],
948 ) -> Result<S, String> {
949 self.require_row(row)?;
950 Ok(gaussian_normalized_row(
951 &p[0],
952 &p[1],
953 self.rows.obs_weight[row],
954 self.rows.standardized_residual[row],
955 self.rows.inv_sigma[row],
956 self.rows.kappa[row],
957 ))
958 }
959}
960
961pub(crate) struct GaussianJointPsiFirstWeights {
962 pub(crate) objective_psirow: Array1<f64>,
963 pub(crate) scoremu: Array1<f64>,
964 pub(crate) score_ls: Array1<f64>,
965 pub(crate) dscoremu: Array1<f64>,
966 pub(crate) dscore_ls: Array1<f64>,
967 pub(crate) hmumu: Array1<f64>,
968 pub(crate) hmu_ls: Array1<f64>,
969 pub(crate) h_ls_ls: Array1<f64>,
970 pub(crate) dhmumu: Array1<f64>,
971 pub(crate) dhmu_ls: Array1<f64>,
972 pub(crate) dh_ls_ls: Array1<f64>,
973}
974
975pub(crate) struct GaussianJointPsiSecondWeights {
976 pub(crate) objective_psi_psirow: Array1<f64>,
977 pub(crate) d2scoremu: Array1<f64>,
978 pub(crate) d2score_ls: Array1<f64>,
979 pub(crate) d2hmumu: Array1<f64>,
980 pub(crate) d2hmu_ls: Array1<f64>,
981 pub(crate) d2h_ls_ls: Array1<f64>,
982}
983
984pub(crate) struct GaussianJointPsiMixedDriftWeights {
985 pub(crate) dhmumu_u: Array1<f64>,
986 pub(crate) dhmu_ls_u: Array1<f64>,
987 pub(crate) dh_ls_ls_u: Array1<f64>,
988 pub(crate) d2hmumu: Array1<f64>,
989 pub(crate) d2hmu_ls: Array1<f64>,
990 pub(crate) d2h_ls_ls: Array1<f64>,
991}
992
993pub(crate) fn apply_ht_mask_first(
1003 weights: &mut GaussianJointPsiFirstWeights,
1004 rows: &[crate::outer_subsample::WeightedOuterRow],
1005) {
1006 let n = weights.objective_psirow.len();
1007 let mut obj = Array1::<f64>::zeros(n);
1008 let mut smu = Array1::<f64>::zeros(n);
1009 let mut sls = Array1::<f64>::zeros(n);
1010 let mut dsmu = Array1::<f64>::zeros(n);
1011 let mut dsls = Array1::<f64>::zeros(n);
1012 let mut hmm = Array1::<f64>::zeros(n);
1013 let mut hml = Array1::<f64>::zeros(n);
1014 let mut hll = Array1::<f64>::zeros(n);
1015 let mut dhmm = Array1::<f64>::zeros(n);
1016 let mut dhml = Array1::<f64>::zeros(n);
1017 let mut dhll = Array1::<f64>::zeros(n);
1018 for r in rows {
1019 let i = r.index;
1020 let w = r.weight;
1021 obj[i] = weights.objective_psirow[i] * w;
1022 smu[i] = weights.scoremu[i] * w;
1023 sls[i] = weights.score_ls[i] * w;
1024 dsmu[i] = weights.dscoremu[i] * w;
1025 dsls[i] = weights.dscore_ls[i] * w;
1026 hmm[i] = weights.hmumu[i] * w;
1027 hml[i] = weights.hmu_ls[i] * w;
1028 hll[i] = weights.h_ls_ls[i] * w;
1029 dhmm[i] = weights.dhmumu[i] * w;
1030 dhml[i] = weights.dhmu_ls[i] * w;
1031 dhll[i] = weights.dh_ls_ls[i] * w;
1032 }
1033 weights.objective_psirow = obj;
1034 weights.scoremu = smu;
1035 weights.score_ls = sls;
1036 weights.dscoremu = dsmu;
1037 weights.dscore_ls = dsls;
1038 weights.hmumu = hmm;
1039 weights.hmu_ls = hml;
1040 weights.h_ls_ls = hll;
1041 weights.dhmumu = dhmm;
1042 weights.dhmu_ls = dhml;
1043 weights.dh_ls_ls = dhll;
1044}
1045
1046pub(crate) fn apply_ht_mask_second(
1052 weights: &mut GaussianJointPsiSecondWeights,
1053 rows: &[crate::outer_subsample::WeightedOuterRow],
1054) {
1055 let n = weights.objective_psi_psirow.len();
1056 let mut obj = Array1::<f64>::zeros(n);
1057 let mut d2smu = Array1::<f64>::zeros(n);
1058 let mut d2sls = Array1::<f64>::zeros(n);
1059 let mut d2hmm = Array1::<f64>::zeros(n);
1060 let mut d2hml = Array1::<f64>::zeros(n);
1061 let mut d2hll = Array1::<f64>::zeros(n);
1062 for r in rows {
1063 let i = r.index;
1064 let w = r.weight;
1065 obj[i] = weights.objective_psi_psirow[i] * w;
1066 d2smu[i] = weights.d2scoremu[i] * w;
1067 d2sls[i] = weights.d2score_ls[i] * w;
1068 d2hmm[i] = weights.d2hmumu[i] * w;
1069 d2hml[i] = weights.d2hmu_ls[i] * w;
1070 d2hll[i] = weights.d2h_ls_ls[i] * w;
1071 }
1072 weights.objective_psi_psirow = obj;
1073 weights.d2scoremu = d2smu;
1074 weights.d2score_ls = d2sls;
1075 weights.d2hmumu = d2hmm;
1076 weights.d2hmu_ls = d2hml;
1077 weights.d2h_ls_ls = d2hll;
1078}
1079
1080pub(crate) fn apply_ht_mask_mixed(
1084 weights: &mut GaussianJointPsiMixedDriftWeights,
1085 rows: &[crate::outer_subsample::WeightedOuterRow],
1086) {
1087 let n = weights.dhmumu_u.len();
1088 let mut dhmm_u = Array1::<f64>::zeros(n);
1089 let mut dhml_u = Array1::<f64>::zeros(n);
1090 let mut dhll_u = Array1::<f64>::zeros(n);
1091 let mut d2hmm = Array1::<f64>::zeros(n);
1092 let mut d2hml = Array1::<f64>::zeros(n);
1093 let mut d2hll = Array1::<f64>::zeros(n);
1094 for r in rows {
1095 let i = r.index;
1096 let w = r.weight;
1097 dhmm_u[i] = weights.dhmumu_u[i] * w;
1098 dhml_u[i] = weights.dhmu_ls_u[i] * w;
1099 dhll_u[i] = weights.dh_ls_ls_u[i] * w;
1100 d2hmm[i] = weights.d2hmumu[i] * w;
1101 d2hml[i] = weights.d2hmu_ls[i] * w;
1102 d2hll[i] = weights.d2h_ls_ls[i] * w;
1103 }
1104 weights.dhmumu_u = dhmm_u;
1105 weights.dhmu_ls_u = dhml_u;
1106 weights.dh_ls_ls_u = dhll_u;
1107 weights.d2hmumu = d2hmm;
1108 weights.d2hmu_ls = d2hml;
1109 weights.d2h_ls_ls = d2hll;
1110}
1111
1112pub(crate) fn gaussian_jointrow_scalars(
1113 y: &Array1<f64>,
1114 etamu: &Array1<f64>,
1115 eta_ls: &Array1<f64>,
1116 weights: &Array1<f64>,
1117) -> Result<GaussianJointRowScalars, String> {
1118 let nobs = y.len();
1119 if etamu.len() != nobs || eta_ls.len() != nobs || weights.len() != nobs {
1120 return Err(GamlssError::DimensionMismatch {
1121 reason: "Gaussian joint row scalar input size mismatch".to_string(),
1122 }
1123 .into());
1124 }
1125 let mut obs_weight = Array1::<f64>::uninit(nobs);
1126 let mut standardized_residual = Array1::<f64>::uninit(nobs);
1127 let mut inv_sigma = Array1::<f64>::uninit(nobs);
1128 let mut kappa = Array1::<f64>::uninit(nobs);
1129 let ln2pi = (2.0 * std::f64::consts::PI).ln();
1130 let certified: Vec<Result<GaussianDiagonalRowKernel, String>> = (0..nobs)
1134 .into_par_iter()
1135 .map(|i| gaussian_diagonal_row_kernel(i, y[i], etamu[i], eta_ls[i], weights[i], ln2pi))
1136 .collect();
1137 for (i, row) in certified.into_iter().enumerate() {
1138 let row = row?;
1139 obs_weight[i].write(weights[i]);
1140 standardized_residual[i].write(row.standardized_residual);
1141 inv_sigma[i].write(row.inv_sigma);
1142 kappa[i].write(row.kappa);
1143 }
1144 let (obs_weight, standardized_residual, inv_sigma, kappa) = unsafe {
1148 (
1149 obs_weight.assume_init(),
1150 standardized_residual.assume_init(),
1151 inv_sigma.assume_init(),
1152 kappa.assume_init(),
1153 )
1154 };
1155 Ok(GaussianJointRowScalars {
1156 obs_weight,
1157 standardized_residual,
1158 inv_sigma,
1159 kappa,
1160 })
1161}
1162
1163pub(crate) fn gaussian_joint_first_directionalweights(
1166 scalars: &GaussianJointRowScalars,
1167 dotmu: &Array1<f64>,
1168 dot_eta: &Array1<f64>,
1169) -> (Array1<f64>, Array1<f64>, Array1<f64>) {
1170 let nobs = scalars.obs_weight.len();
1171 let program = GaussianJointRowProgram::new(scalars);
1172 let mut w_u = Array1::<f64>::zeros(nobs);
1173 let mut c_u = Array1::<f64>::zeros(nobs);
1174 let mut d_u = Array1::<f64>::zeros(nobs);
1175 for i in 0..nobs {
1176 let matrix = program.row_third_contracted(i, &[dotmu[i], dot_eta[i]]);
1177 w_u[i] = matrix[0][0];
1178 c_u[i] = matrix[0][1];
1179 d_u[i] = matrix[1][1];
1180 }
1181 (w_u, c_u, d_u)
1182}
1183
1184pub(crate) fn gaussian_jointsecond_directionalweights(
1187 scalars: &GaussianJointRowScalars,
1188 dotmu_u: &Array1<f64>,
1189 dot_eta_u: &Array1<f64>,
1190 dotmuv: &Array1<f64>,
1191 dot_etav: &Array1<f64>,
1192) -> (Array1<f64>, Array1<f64>, Array1<f64>) {
1193 let nobs = scalars.obs_weight.len();
1194 let program = GaussianJointRowProgram::new(scalars);
1195 let mut w_uv = Array1::<f64>::zeros(nobs);
1196 let mut c_uv = Array1::<f64>::zeros(nobs);
1197 let mut d_uv = Array1::<f64>::zeros(nobs);
1198 for i in 0..nobs {
1199 let matrix = program.row_fourth_contracted(
1200 i,
1201 &[dotmu_u[i], dot_eta_u[i]],
1202 &[dotmuv[i], dot_etav[i]],
1203 );
1204 w_uv[i] = matrix[0][0];
1205 c_uv[i] = matrix[0][1];
1206 d_uv[i] = matrix[1][1];
1207 }
1208 (w_uv, c_uv, d_uv)
1209}
1210
1211pub(crate) fn gaussian_joint_psi_firstweights(
1212 scalars: &GaussianJointRowScalars,
1213 mu_a: &Array1<f64>,
1214 eta_a: &Array1<f64>,
1215) -> GaussianJointPsiFirstWeights {
1216 let nobs = scalars.obs_weight.len();
1217 let program = GaussianJointRowProgram::new(scalars);
1218 let mut objective_psirow = Array1::<f64>::uninit(nobs);
1219 let mut scoremu = Array1::<f64>::uninit(nobs);
1220 let mut score_ls = Array1::<f64>::uninit(nobs);
1221 let mut dscoremu = Array1::<f64>::uninit(nobs);
1222 let mut dscore_ls = Array1::<f64>::uninit(nobs);
1223 let mut hmumu = Array1::<f64>::uninit(nobs);
1224 let mut hmu_ls = Array1::<f64>::uninit(nobs);
1225 let mut h_ls_ls = Array1::<f64>::uninit(nobs);
1226 let mut dhmumu = Array1::<f64>::uninit(nobs);
1227 let mut dhmu_ls = Array1::<f64>::uninit(nobs);
1228 let mut dh_ls_ls = Array1::<f64>::uninit(nobs);
1229 for i in 0..nobs {
1230 let direction = [mu_a[i], eta_a[i]];
1231 let atom = program.row_order2(i);
1232 let score = atom.gradient();
1233 let hessian = [
1234 [atom.hessian_at(0, 0), atom.hessian_at(0, 1)],
1235 [atom.hessian_at(1, 0), atom.hessian_at(1, 1)],
1236 ];
1237 let score_direction = matrix_vector_2(&hessian, &direction);
1238 let hessian_direction = program.row_third_contracted(i, &direction);
1239 objective_psirow[i].write(dot_2(&score, &direction));
1240 scoremu[i].write(score[0]);
1241 score_ls[i].write(score[1]);
1242 dscoremu[i].write(score_direction[0]);
1243 dscore_ls[i].write(score_direction[1]);
1244 hmumu[i].write(hessian[0][0]);
1245 hmu_ls[i].write(hessian[0][1]);
1246 h_ls_ls[i].write(hessian[1][1]);
1247 dhmumu[i].write(hessian_direction[0][0]);
1248 dhmu_ls[i].write(hessian_direction[0][1]);
1249 dh_ls_ls[i].write(hessian_direction[1][1]);
1250 }
1251 unsafe {
1254 GaussianJointPsiFirstWeights {
1255 objective_psirow: objective_psirow.assume_init(),
1256 scoremu: scoremu.assume_init(),
1257 score_ls: score_ls.assume_init(),
1258 dscoremu: dscoremu.assume_init(),
1259 dscore_ls: dscore_ls.assume_init(),
1260 hmumu: hmumu.assume_init(),
1261 hmu_ls: hmu_ls.assume_init(),
1262 h_ls_ls: h_ls_ls.assume_init(),
1263 dhmumu: dhmumu.assume_init(),
1264 dhmu_ls: dhmu_ls.assume_init(),
1265 dh_ls_ls: dh_ls_ls.assume_init(),
1266 }
1267 }
1268}
1269
1270pub(crate) fn gaussian_joint_psisecondweights(
1271 scalars: &GaussianJointRowScalars,
1272 mu_a: &Array1<f64>,
1273 eta_a: &Array1<f64>,
1274 mu_b: &Array1<f64>,
1275 eta_b: &Array1<f64>,
1276 mu_ab: &Array1<f64>,
1277 eta_ab: &Array1<f64>,
1278) -> GaussianJointPsiSecondWeights {
1279 let nobs = scalars.obs_weight.len();
1280 let program = GaussianJointRowProgram::new(scalars);
1281 let mut objective_psi_psirow = Array1::<f64>::uninit(nobs);
1282 let mut d2scoremu = Array1::<f64>::uninit(nobs);
1283 let mut d2score_ls = Array1::<f64>::uninit(nobs);
1284 let mut d2hmumu = Array1::<f64>::uninit(nobs);
1285 let mut d2hmu_ls = Array1::<f64>::uninit(nobs);
1286 let mut d2h_ls_ls = Array1::<f64>::uninit(nobs);
1287 for i in 0..nobs {
1288 let direction_a = [mu_a[i], eta_a[i]];
1289 let direction_b = [mu_b[i], eta_b[i]];
1290 let direction_ab = [mu_ab[i], eta_ab[i]];
1291 let atom = program.row_order2(i);
1292 let score = atom.gradient();
1293 let hessian = [
1294 [atom.hessian_at(0, 0), atom.hessian_at(0, 1)],
1295 [atom.hessian_at(1, 0), atom.hessian_at(1, 1)],
1296 ];
1297 let hessian_a = program.row_third_contracted(i, &direction_a);
1298 let hessian_ab = program.row_third_contracted(i, &direction_ab);
1299 let hessian_a_b = program.row_fourth_contracted(i, &direction_a, &direction_b);
1300 let score_second = add_vector_2(
1301 matrix_vector_2(&hessian_a, &direction_b),
1302 matrix_vector_2(&hessian, &direction_ab),
1303 );
1304 let hessian_second = add_matrix_2(hessian_a_b, hessian_ab);
1305 objective_psi_psirow[i].write(
1306 dot_2(&direction_a, &matrix_vector_2(&hessian, &direction_b))
1307 + dot_2(&score, &direction_ab),
1308 );
1309 d2scoremu[i].write(score_second[0]);
1310 d2score_ls[i].write(score_second[1]);
1311 d2hmumu[i].write(hessian_second[0][0]);
1312 d2hmu_ls[i].write(hessian_second[0][1]);
1313 d2h_ls_ls[i].write(hessian_second[1][1]);
1314 }
1315 unsafe {
1318 GaussianJointPsiSecondWeights {
1319 objective_psi_psirow: objective_psi_psirow.assume_init(),
1320 d2scoremu: d2scoremu.assume_init(),
1321 d2score_ls: d2score_ls.assume_init(),
1322 d2hmumu: d2hmumu.assume_init(),
1323 d2hmu_ls: d2hmu_ls.assume_init(),
1324 d2h_ls_ls: d2h_ls_ls.assume_init(),
1325 }
1326 }
1327}
1328
1329pub(crate) fn gaussian_joint_psi_mixed_driftweights(
1330 scalars: &GaussianJointRowScalars,
1331 dot_mu: &Array1<f64>,
1332 dot_eta: &Array1<f64>,
1333 mu_a: &Array1<f64>,
1334 eta_a: &Array1<f64>,
1335 dot_mu_a: &Array1<f64>,
1336 dot_eta_a: &Array1<f64>,
1337) -> GaussianJointPsiMixedDriftWeights {
1338 let nobs = scalars.obs_weight.len();
1339 let program = GaussianJointRowProgram::new(scalars);
1340 let mut dhmumu_u = Array1::<f64>::uninit(nobs);
1341 let mut dhmu_ls_u = Array1::<f64>::uninit(nobs);
1342 let mut dh_ls_ls_u = Array1::<f64>::uninit(nobs);
1343 let mut d2hmumu = Array1::<f64>::uninit(nobs);
1344 let mut d2hmu_ls = Array1::<f64>::uninit(nobs);
1345 let mut d2h_ls_ls = Array1::<f64>::uninit(nobs);
1346 for i in 0..nobs {
1347 let drift = [dot_mu[i], dot_eta[i]];
1348 let psi = [mu_a[i], eta_a[i]];
1349 let mixed_direction = [dot_mu_a[i], dot_eta_a[i]];
1350 let hessian_drift = program.row_third_contracted(i, &drift);
1351 let hessian_mixed = add_matrix_2(
1352 program.row_fourth_contracted(i, &drift, &psi),
1353 program.row_third_contracted(i, &mixed_direction),
1354 );
1355 dhmumu_u[i].write(hessian_drift[0][0]);
1356 dhmu_ls_u[i].write(hessian_drift[0][1]);
1357 dh_ls_ls_u[i].write(hessian_drift[1][1]);
1358 d2hmumu[i].write(hessian_mixed[0][0]);
1359 d2hmu_ls[i].write(hessian_mixed[0][1]);
1360 d2h_ls_ls[i].write(hessian_mixed[1][1]);
1361 }
1362 unsafe {
1365 GaussianJointPsiMixedDriftWeights {
1366 dhmumu_u: dhmumu_u.assume_init(),
1367 dhmu_ls_u: dhmu_ls_u.assume_init(),
1368 dh_ls_ls_u: dh_ls_ls_u.assume_init(),
1369 d2hmumu: d2hmumu.assume_init(),
1370 d2hmu_ls: d2hmu_ls.assume_init(),
1371 d2h_ls_ls: d2h_ls_ls.assume_init(),
1372 }
1373 }
1374}
1375
1376pub(crate) fn gaussian_locscale_observed_joint_row_coeffs(
1398 rows: &GaussianJointRowScalars,
1399) -> (Array1<f64>, Array1<f64>, Array1<f64>) {
1400 let n = rows.obs_weight.len();
1401 let program = GaussianJointRowProgram::new(rows);
1402 let mut mm = Array1::<f64>::zeros(n);
1403 let mut ml = Array1::<f64>::zeros(n);
1404 let mut ll = Array1::<f64>::zeros(n);
1405 for row in 0..n {
1406 let atom = program.row_order2(row);
1407 mm[row] = atom.hessian_at(0, 0);
1408 ml[row] = atom.hessian_at(0, 1);
1409 ll[row] = atom.hessian_at(1, 1);
1410 }
1411 (mm, ml, ll)
1412}
1413
1414pub(crate) fn gaussian_joint_hessian_from_designs(
1415 xmu: &DenseOrOperator<'_>,
1416 x_ls: &DenseOrOperator<'_>,
1417 hmumu_coeff: &Array1<f64>,
1418 hmu_ls_coeff: &Array1<f64>,
1419 h_ls_ls_coeff: &Array1<f64>,
1420) -> Result<Array2<f64>, String> {
1421 if xmu.nrows() != hmumu_coeff.len()
1422 || xmu.nrows() != hmu_ls_coeff.len()
1423 || xmu.nrows() != h_ls_ls_coeff.len()
1424 || x_ls.nrows() != xmu.nrows()
1425 {
1426 return Err(GamlssError::DimensionMismatch { reason: format!(
1427 "gaussian_joint_hessian_from_designs dimension mismatch: xmu {}x{}, x_ls {}x{}, coeffs {}/{}/{}",
1428 xmu.nrows(),
1429 xmu.ncols(),
1430 x_ls.nrows(),
1431 x_ls.ncols(),
1432 hmumu_coeff.len(),
1433 hmu_ls_coeff.len(),
1434 h_ls_ls_coeff.len()
1435 ) }.into());
1436 }
1437
1438 let n = xmu.nrows();
1439 let pmu = xmu.ncols();
1440 let p_ls = x_ls.ncols();
1441 let total = pmu + p_ls;
1442 let mut out = Array2::<f64>::zeros((total, total));
1443 for rows in exact_design_row_chunks(n, pmu.max(p_ls)) {
1444 let xmu_chunk = xmu.row_chunk(rows.clone())?;
1445 let xls_chunk = x_ls.row_chunk(rows.clone())?;
1446 let hmumu = hmumu_coeff.slice(s![rows.clone()]);
1447 let hmu_ls = hmu_ls_coeff.slice(s![rows.clone()]);
1448 let h_ls_ls = h_ls_ls_coeff.slice(s![rows.clone()]);
1449 let chunk_hessian =
1450 fast_joint_hessian_2x2(&xmu_chunk, &xls_chunk, &hmumu, &hmu_ls, &h_ls_ls);
1451 out += &chunk_hessian;
1452 }
1453 Ok(out)
1454}
1455
1456pub(crate) fn gaussian_joint_psihessian_fromweights(
1457 xmu: &Array2<f64>,
1458 x_ls: &Array2<f64>,
1459 xmu_psi: CustomFamilyPsiLinearMapRef<'_>,
1460 x_ls_psi: CustomFamilyPsiLinearMapRef<'_>,
1461 weights: &GaussianJointPsiFirstWeights,
1462) -> Result<Array2<f64>, String> {
1463 let a_mu = weighted_crossprod_psi_maps(
1467 xmu_psi,
1468 weights.hmumu.view(),
1469 CustomFamilyPsiLinearMapRef::Dense(xmu),
1470 ).map_err(|error| error.to_string())?;
1471 let hmumu = &a_mu + &a_mu.t() + &xt_diag_x_dense(xmu, &weights.dhmumu)?;
1472 let hmu_ls = weighted_crossprod_psi_maps(
1473 xmu_psi,
1474 weights.hmu_ls.view(),
1475 CustomFamilyPsiLinearMapRef::Dense(x_ls),
1476 ).map_err(|error| error.to_string())? + &weighted_crossprod_psi_maps(
1477 CustomFamilyPsiLinearMapRef::Dense(xmu),
1478 weights.hmu_ls.view(),
1479 x_ls_psi,
1480 ).map_err(|error| error.to_string())? + &xt_diag_y_dense(xmu, &weights.dhmu_ls, x_ls)?;
1481 let a_ls = weighted_crossprod_psi_maps(
1482 x_ls_psi,
1483 weights.h_ls_ls.view(),
1484 CustomFamilyPsiLinearMapRef::Dense(x_ls),
1485 ).map_err(|error| error.to_string())?;
1486 let h_ls_ls = &a_ls + &a_ls.t() + &xt_diag_x_dense(x_ls, &weights.dh_ls_ls)?;
1487 Ok(gaussian_pack_joint_symmetrichessian(
1488 &hmumu, &hmu_ls, &h_ls_ls,
1489 ))
1490}
1491
1492pub(crate) fn build_two_block_custom_family_joint_psi_operator_from_actions(
1493 left_action: Option<CustomFamilyPsiDesignAction>,
1494 right_action: Option<CustomFamilyPsiDesignAction>,
1495 left_range: std::ops::Range<usize>,
1496 right_range: std::ops::Range<usize>,
1497 left_design: &Array2<f64>,
1498 right_design: &Array2<f64>,
1499 left_weights: &Array1<f64>,
1500 cross_weights: &Array1<f64>,
1501 right_weights: &Array1<f64>,
1502 left_drift_weights: &Array1<f64>,
1503 cross_drift_weights: &Array1<f64>,
1504 right_drift_weights: &Array1<f64>,
1505) -> Result<Option<std::sync::Arc<dyn gam_problem::HyperOperator>>, String> {
1506 if left_action.is_none() && right_action.is_none() {
1507 return Ok(None);
1508 }
1509
1510 let total = left_design.ncols() + right_design.ncols();
1511 let channels = vec![
1512 CustomFamilyJointDesignChannel::new(left_range, shared_dense_arc(left_design), left_action),
1513 CustomFamilyJointDesignChannel::new(
1514 right_range,
1515 shared_dense_arc(right_design),
1516 right_action,
1517 ),
1518 ];
1519 let pair_contributions = vec![
1520 CustomFamilyJointDesignPairContribution::new(
1521 0,
1522 0,
1523 left_weights.clone(),
1524 left_drift_weights.clone(),
1525 ),
1526 CustomFamilyJointDesignPairContribution::new(
1527 0,
1528 1,
1529 cross_weights.clone(),
1530 cross_drift_weights.clone(),
1531 ),
1532 CustomFamilyJointDesignPairContribution::new(
1533 1,
1534 0,
1535 cross_weights.clone(),
1536 cross_drift_weights.clone(),
1537 ),
1538 CustomFamilyJointDesignPairContribution::new(
1539 1,
1540 1,
1541 right_weights.clone(),
1542 right_drift_weights.clone(),
1543 ),
1544 ];
1545
1546 Ok(Some(std::sync::Arc::new(
1547 CustomFamilyJointPsiOperator::new(total, channels, pair_contributions),
1548 )))
1549}
1550
1551pub(crate) fn gaussian_joint_psisecondhessian_fromweights(
1552 xmu: &Array2<f64>,
1553 x_ls: &Array2<f64>,
1554 xmu_i: CustomFamilyPsiLinearMapRef<'_>,
1555 x_ls_i: CustomFamilyPsiLinearMapRef<'_>,
1556 xmu_j: CustomFamilyPsiLinearMapRef<'_>,
1557 x_ls_j: CustomFamilyPsiLinearMapRef<'_>,
1558 xmu_ab: CustomFamilyPsiLinearMapRef<'_>,
1559 x_ls_ab: CustomFamilyPsiLinearMapRef<'_>,
1560 weights_i: &GaussianJointPsiFirstWeights,
1561 weights_j: &GaussianJointPsiFirstWeights,
1562 secondweights: &GaussianJointPsiSecondWeights,
1563) -> Result<Array2<f64>, String> {
1564 let a_ab_mu = weighted_crossprod_psi_maps(
1568 xmu_ab,
1569 weights_i.hmumu.view(),
1570 CustomFamilyPsiLinearMapRef::Dense(xmu),
1571 ).map_err(|error| error.to_string())?;
1572 let a_ij_mu = weighted_crossprod_psi_maps(xmu_i, weights_i.hmumu.view(), xmu_j).map_err(|error| error.to_string())?;
1573 let a_iwj_mu = weighted_crossprod_psi_maps(
1574 xmu_i,
1575 weights_j.dhmumu.view(),
1576 CustomFamilyPsiLinearMapRef::Dense(xmu),
1577 ).map_err(|error| error.to_string())?;
1578 let a_jwi_mu = weighted_crossprod_psi_maps(
1579 xmu_j,
1580 weights_i.dhmumu.view(),
1581 CustomFamilyPsiLinearMapRef::Dense(xmu),
1582 ).map_err(|error| error.to_string())?;
1583 let hmumu = &a_ab_mu
1584 + &a_ab_mu.t()
1585 + &a_ij_mu
1586 + a_ij_mu.t()
1587 + &a_iwj_mu
1588 + a_iwj_mu.t()
1589 + &a_jwi_mu
1590 + a_jwi_mu.t()
1591 + &xt_diag_x_dense(xmu, &secondweights.d2hmumu)?;
1592 let hmu_ls = weighted_crossprod_psi_maps(
1593 xmu_ab,
1594 weights_i.hmu_ls.view(),
1595 CustomFamilyPsiLinearMapRef::Dense(x_ls),
1596 ).map_err(|error| error.to_string())? + &weighted_crossprod_psi_maps(xmu_i, weights_i.hmu_ls.view(), x_ls_j).map_err(|error| error.to_string())?
1597 + &weighted_crossprod_psi_maps(xmu_j, weights_i.hmu_ls.view(), x_ls_i).map_err(|error| error.to_string())?
1598 + &weighted_crossprod_psi_maps(
1599 xmu_i,
1600 weights_j.dhmu_ls.view(),
1601 CustomFamilyPsiLinearMapRef::Dense(x_ls),
1602 ).map_err(|error| error.to_string())?
1603 + &weighted_crossprod_psi_maps(
1604 xmu_j,
1605 weights_i.dhmu_ls.view(),
1606 CustomFamilyPsiLinearMapRef::Dense(x_ls),
1607 ).map_err(|error| error.to_string())?
1608 + &weighted_crossprod_psi_maps(
1609 CustomFamilyPsiLinearMapRef::Dense(xmu),
1610 weights_i.dhmu_ls.view(),
1611 x_ls_j,
1612 ).map_err(|error| error.to_string())?
1613 + &weighted_crossprod_psi_maps(
1614 CustomFamilyPsiLinearMapRef::Dense(xmu),
1615 weights_j.dhmu_ls.view(),
1616 x_ls_i,
1617 ).map_err(|error| error.to_string())?
1618 + &xt_diag_y_dense(xmu, &secondweights.d2hmu_ls, x_ls)?
1619 + &weighted_crossprod_psi_maps(
1620 CustomFamilyPsiLinearMapRef::Dense(xmu),
1621 weights_i.hmu_ls.view(),
1622 x_ls_ab,
1623 ).map_err(|error| error.to_string())?;
1624 let a_ab_ls = weighted_crossprod_psi_maps(
1625 x_ls_ab,
1626 weights_i.h_ls_ls.view(),
1627 CustomFamilyPsiLinearMapRef::Dense(x_ls),
1628 ).map_err(|error| error.to_string())?;
1629 let a_ij_ls = weighted_crossprod_psi_maps(x_ls_i, weights_i.h_ls_ls.view(), x_ls_j).map_err(|error| error.to_string())?;
1630 let a_iwj_ls = weighted_crossprod_psi_maps(
1631 x_ls_i,
1632 weights_j.dh_ls_ls.view(),
1633 CustomFamilyPsiLinearMapRef::Dense(x_ls),
1634 ).map_err(|error| error.to_string())?;
1635 let a_jwi_ls = weighted_crossprod_psi_maps(
1636 x_ls_j,
1637 weights_i.dh_ls_ls.view(),
1638 CustomFamilyPsiLinearMapRef::Dense(x_ls),
1639 ).map_err(|error| error.to_string())?;
1640 let h_ls_ls = &a_ab_ls
1641 + &a_ab_ls.t()
1642 + &a_ij_ls
1643 + a_ij_ls.t()
1644 + &a_iwj_ls
1645 + a_iwj_ls.t()
1646 + &a_jwi_ls
1647 + a_jwi_ls.t()
1648 + &xt_diag_x_dense(x_ls, &secondweights.d2h_ls_ls)?;
1649 Ok(gaussian_pack_joint_symmetrichessian(
1650 &hmumu, &hmu_ls, &h_ls_ls,
1651 ))
1652}
1653
1654pub(crate) fn gaussian_joint_psi_mixedhessian_drift_fromweights(
1655 xmu: &Array2<f64>,
1656 x_ls: &Array2<f64>,
1657 xmu_psi: CustomFamilyPsiLinearMapRef<'_>,
1658 x_ls_psi: CustomFamilyPsiLinearMapRef<'_>,
1659 mixedweights: &GaussianJointPsiMixedDriftWeights,
1660) -> Result<Array2<f64>, String> {
1661 let a_mu = weighted_crossprod_psi_maps(
1662 xmu_psi,
1663 mixedweights.dhmumu_u.view(),
1664 CustomFamilyPsiLinearMapRef::Dense(xmu),
1665 ).map_err(|error| error.to_string())?;
1666 let hmumu = &a_mu + &a_mu.t() + &xt_diag_x_dense(xmu, &mixedweights.d2hmumu)?;
1667 let hmu_ls = weighted_crossprod_psi_maps(
1668 xmu_psi,
1669 mixedweights.dhmu_ls_u.view(),
1670 CustomFamilyPsiLinearMapRef::Dense(x_ls),
1671 ).map_err(|error| error.to_string())? + &weighted_crossprod_psi_maps(
1672 CustomFamilyPsiLinearMapRef::Dense(xmu),
1673 mixedweights.dhmu_ls_u.view(),
1674 x_ls_psi,
1675 ).map_err(|error| error.to_string())? + &xt_diag_y_dense(xmu, &mixedweights.d2hmu_ls, x_ls)?;
1676 let a_ls = weighted_crossprod_psi_maps(
1677 x_ls_psi,
1678 mixedweights.dh_ls_ls_u.view(),
1679 CustomFamilyPsiLinearMapRef::Dense(x_ls),
1680 ).map_err(|error| error.to_string())?;
1681 let h_ls_ls = &a_ls + &a_ls.t() + &xt_diag_x_dense(x_ls, &mixedweights.d2h_ls_ls)?;
1682 Ok(gaussian_pack_joint_symmetrichessian(
1683 &hmumu, &hmu_ls, &h_ls_ls,
1684 ))
1685}
1686
1687#[inline]
1688pub(crate) fn exp_sigma_derivs_up_to_fourth_array(
1689 eta: ArrayView1<'_, f64>,
1690) -> (
1691 Array1<f64>,
1692 Array1<f64>,
1693 Array1<f64>,
1694 Array1<f64>,
1695 Array1<f64>,
1696) {
1697 use rayon::iter::{IntoParallelIterator, ParallelIterator};
1698 let n = eta.len();
1699 let tuples: Vec<(f64, f64, f64, f64, f64)> = (0..n)
1700 .into_par_iter()
1701 .map(|i| exp_sigma_derivs_up_to_fourth_scalar(eta[i]))
1702 .collect();
1703 let mut sigma = Array1::<f64>::zeros(n);
1704 let mut d1 = Array1::<f64>::zeros(n);
1705 let mut d2 = Array1::<f64>::zeros(n);
1706 let mut d3 = Array1::<f64>::zeros(n);
1707 let mut d4 = Array1::<f64>::zeros(n);
1708 for (i, (s_i, d1_i, d2_i, d3_i, d4_i)) in tuples.into_iter().enumerate() {
1709 sigma[i] = s_i;
1710 d1[i] = d1_i;
1711 d2[i] = d2_i;
1712 d3[i] = d3_i;
1713 d4[i] = d4_i;
1714 }
1715 (sigma, d1, d2, d3, d4)
1716}
1717
1718#[cfg(test)]
1719mod observed_single_source_oracle_tests {
1720 use super::*;
1735 use ndarray::array;
1736
1737 fn row_nll(y: f64, mu: f64, eta_ls: f64, a: f64) -> f64 {
1740 let ln2pi = (2.0 * std::f64::consts::PI).ln();
1741 -gaussian_diagonal_row_kernel(0, y, mu, eta_ls, a, ln2pi)
1742 .expect("representable Gaussian oracle row")
1743 .log_likelihood
1744 }
1745
1746 fn gradient_fd(y: f64, mu: f64, eta_ls: f64, a: f64, h: f64) -> [f64; 2] {
1747 [
1748 (row_nll(y, mu + h, eta_ls, a) - row_nll(y, mu - h, eta_ls, a)) / (2.0 * h),
1749 (row_nll(y, mu, eta_ls + h, a) - row_nll(y, mu, eta_ls - h, a)) / (2.0 * h),
1750 ]
1751 }
1752
1753 fn observed_hessian_fd(y: f64, mu: f64, eta_ls: f64, a: f64, h: f64) -> (f64, f64, f64) {
1756 let hmm = (row_nll(y, mu + h, eta_ls, a) - 2.0 * row_nll(y, mu, eta_ls, a)
1757 + row_nll(y, mu - h, eta_ls, a))
1758 / (h * h);
1759 let hll = (row_nll(y, mu, eta_ls + h, a) - 2.0 * row_nll(y, mu, eta_ls, a)
1760 + row_nll(y, mu, eta_ls - h, a))
1761 / (h * h);
1762 let hml = (row_nll(y, mu + h, eta_ls + h, a)
1763 - row_nll(y, mu + h, eta_ls - h, a)
1764 - row_nll(y, mu - h, eta_ls + h, a)
1765 + row_nll(y, mu - h, eta_ls - h, a))
1766 / (4.0 * h * h);
1767 (hmm, hml, hll)
1768 }
1769
1770 fn hessian_fd(y: f64, mu: f64, eta_ls: f64, a: f64, h: f64) -> [[f64; 2]; 2] {
1771 let (mm, ml, ll) = observed_hessian_fd(y, mu, eta_ls, a, h);
1772 [[mm, ml], [ml, ll]]
1773 }
1774
1775 fn path_point(
1776 base: [f64; 2],
1777 direction_a: [f64; 2],
1778 direction_b: [f64; 2],
1779 direction_ab: [f64; 2],
1780 s: f64,
1781 t: f64,
1782 ) -> [f64; 2] {
1783 [
1784 base[0] + s * direction_a[0] + t * direction_b[0] + s * t * direction_ab[0],
1785 base[1] + s * direction_a[1] + t * direction_b[1] + s * t * direction_ab[1],
1786 ]
1787 }
1788
1789 fn mixed_value_fd(
1790 y: f64,
1791 base: [f64; 2],
1792 a: f64,
1793 direction_a: [f64; 2],
1794 direction_b: [f64; 2],
1795 direction_ab: [f64; 2],
1796 h: f64,
1797 ) -> f64 {
1798 let pp = path_point(base, direction_a, direction_b, direction_ab, h, h);
1799 let pm = path_point(base, direction_a, direction_b, direction_ab, h, -h);
1800 let mp = path_point(base, direction_a, direction_b, direction_ab, -h, h);
1801 let mm = path_point(base, direction_a, direction_b, direction_ab, -h, -h);
1802 (row_nll(y, pp[0], pp[1], a) - row_nll(y, pm[0], pm[1], a) - row_nll(y, mp[0], mp[1], a)
1803 + row_nll(y, mm[0], mm[1], a))
1804 / (4.0 * h * h)
1805 }
1806
1807 fn mixed_gradient_fd(
1808 y: f64,
1809 base: [f64; 2],
1810 a: f64,
1811 direction_a: [f64; 2],
1812 direction_b: [f64; 2],
1813 direction_ab: [f64; 2],
1814 outer_h: f64,
1815 inner_h: f64,
1816 ) -> [f64; 2] {
1817 let pp = path_point(
1818 base,
1819 direction_a,
1820 direction_b,
1821 direction_ab,
1822 outer_h,
1823 outer_h,
1824 );
1825 let pm = path_point(
1826 base,
1827 direction_a,
1828 direction_b,
1829 direction_ab,
1830 outer_h,
1831 -outer_h,
1832 );
1833 let mp = path_point(
1834 base,
1835 direction_a,
1836 direction_b,
1837 direction_ab,
1838 -outer_h,
1839 outer_h,
1840 );
1841 let mm = path_point(
1842 base,
1843 direction_a,
1844 direction_b,
1845 direction_ab,
1846 -outer_h,
1847 -outer_h,
1848 );
1849 let gpp = gradient_fd(y, pp[0], pp[1], a, inner_h);
1850 let gpm = gradient_fd(y, pm[0], pm[1], a, inner_h);
1851 let gmp = gradient_fd(y, mp[0], mp[1], a, inner_h);
1852 let gmm = gradient_fd(y, mm[0], mm[1], a, inner_h);
1853 std::array::from_fn(|axis| {
1854 (gpp[axis] - gpm[axis] - gmp[axis] + gmm[axis]) / (4.0 * outer_h * outer_h)
1855 })
1856 }
1857
1858 fn mixed_hessian_fd(
1859 y: f64,
1860 base: [f64; 2],
1861 a: f64,
1862 direction_a: [f64; 2],
1863 direction_b: [f64; 2],
1864 direction_ab: [f64; 2],
1865 outer_h: f64,
1866 inner_h: f64,
1867 ) -> [[f64; 2]; 2] {
1868 let pp = path_point(
1869 base,
1870 direction_a,
1871 direction_b,
1872 direction_ab,
1873 outer_h,
1874 outer_h,
1875 );
1876 let pm = path_point(
1877 base,
1878 direction_a,
1879 direction_b,
1880 direction_ab,
1881 outer_h,
1882 -outer_h,
1883 );
1884 let mp = path_point(
1885 base,
1886 direction_a,
1887 direction_b,
1888 direction_ab,
1889 -outer_h,
1890 outer_h,
1891 );
1892 let mm = path_point(
1893 base,
1894 direction_a,
1895 direction_b,
1896 direction_ab,
1897 -outer_h,
1898 -outer_h,
1899 );
1900 let hpp = hessian_fd(y, pp[0], pp[1], a, inner_h);
1901 let hpm = hessian_fd(y, pm[0], pm[1], a, inner_h);
1902 let hmp = hessian_fd(y, mp[0], mp[1], a, inner_h);
1903 let hmm = hessian_fd(y, mm[0], mm[1], a, inner_h);
1904 std::array::from_fn(|row| {
1905 std::array::from_fn(|column| {
1906 (hpp[row][column] - hpm[row][column] - hmp[row][column] + hmm[row][column])
1907 / (4.0 * outer_h * outer_h)
1908 })
1909 })
1910 }
1911
1912 fn assert_close(actual: f64, expected: f64, tolerance: f64, label: &str) {
1913 let band = tolerance * actual.abs().max(expected.abs()).max(1.0);
1914 assert!(
1915 (actual - expected).abs() <= band,
1916 "{label}: actual={actual:+.15e} expected={expected:+.15e} band={band:.3e}"
1917 );
1918 }
1919
1920 fn assert_matrix_close(
1921 actual: &[[f64; 2]; 2],
1922 expected: &[[f64; 2]; 2],
1923 tolerance: f64,
1924 label: &str,
1925 ) {
1926 for row in 0..2 {
1927 for column in 0..2 {
1928 assert_close(
1929 actual[row][column],
1930 expected[row][column],
1931 tolerance,
1932 &format!("{label}[{row},{column}]"),
1933 );
1934 }
1935 }
1936 }
1937
1938 fn production_observed_row(y: f64, mu: f64, eta_ls: f64, a: f64) -> (f64, f64, f64) {
1940 let rows = gaussian_jointrow_scalars(&array![y], &array![mu], &array![eta_ls], &array![a])
1941 .expect("row scalars");
1942 let (mm, ml, ll) = gaussian_locscale_observed_joint_row_coeffs(&rows);
1943 (mm[0], ml[0], ll[0])
1944 }
1945
1946 #[test]
1947 fn generated_gaussian_psi_chain_matches_generic_nested_jet_all_channels_932() {
1948 use gam_math::jet_tower::{
1949 program_fourth_contracted, program_row_kernel, program_third_contracted,
1950 };
1951
1952 let rows =
1953 gaussian_jointrow_scalars(&array![0.55], &array![0.3], &array![-0.4], &array![1.7])
1954 .expect("row scalars");
1955 let program = GaussianJointRowProgram::new(&rows);
1956 let direction_a = [0.5, -0.7];
1957 let direction_b = [-0.3, 0.8];
1958 let direction_ab = [0.2, -0.15];
1959 let drift = [0.6, 0.25];
1960 let psi = [-0.4, 0.75];
1961 let drift_psi = [0.12, -0.18];
1962
1963 let atom = program.row_order2(0);
1964 let (jet_value, jet_score, jet_hessian) =
1965 program_row_kernel(&program, 0).expect("generic order2");
1966 assert_close(atom.value(), jet_value, 1e-12, "row value");
1967 for axis in 0..2 {
1968 assert_close(
1969 atom.gradient()[axis],
1970 jet_score[axis],
1971 1e-12,
1972 &format!("row score[{axis}]"),
1973 );
1974 }
1975 let generated_hessian = [
1976 [atom.hessian_at(0, 0), atom.hessian_at(0, 1)],
1977 [atom.hessian_at(1, 0), atom.hessian_at(1, 1)],
1978 ];
1979 assert_matrix_close(&generated_hessian, &jet_hessian, 1e-12, "row Hessian");
1980
1981 let jet_third_a =
1982 program_third_contracted(&program, 0, &direction_a).expect("generic third a");
1983 let jet_third_ab =
1984 program_third_contracted(&program, 0, &direction_ab).expect("generic third ab");
1985 let jet_fourth_ab = program_fourth_contracted(&program, 0, &direction_a, &direction_b)
1986 .expect("generic fourth ab");
1987 assert_matrix_close(
1988 &program.row_third_contracted(0, &direction_a),
1989 &jet_third_a,
1990 1e-9,
1991 "generated dH",
1992 );
1993 assert_matrix_close(
1994 &program.row_fourth_contracted(0, &direction_a, &direction_b),
1995 &jet_fourth_ab,
1996 1e-9,
1997 "generated d2H",
1998 );
1999
2000 let first = gaussian_joint_psi_firstweights(
2001 &rows,
2002 &array![direction_a[0]],
2003 &array![direction_a[1]],
2004 );
2005 let expected_score_a = matrix_vector_2(&jet_hessian, &direction_a);
2006 assert_close(
2007 first.objective_psirow[0],
2008 dot_2(&jet_score, &direction_a),
2009 1e-9,
2010 "first psi objective",
2011 );
2012 assert_close(first.scoremu[0], jet_score[0], 1e-12, "first score mu");
2013 assert_close(first.score_ls[0], jet_score[1], 1e-12, "first score ls");
2014 assert_close(
2015 first.dscoremu[0],
2016 expected_score_a[0],
2017 1e-9,
2018 "first dscore mu",
2019 );
2020 assert_close(
2021 first.dscore_ls[0],
2022 expected_score_a[1],
2023 1e-9,
2024 "first dscore ls",
2025 );
2026 assert_close(first.hmumu[0], jet_hessian[0][0], 1e-12, "first H mm");
2027 assert_close(first.hmu_ls[0], jet_hessian[0][1], 1e-12, "first H ml");
2028 assert_close(first.h_ls_ls[0], jet_hessian[1][1], 1e-12, "first H ll");
2029 assert_close(first.dhmumu[0], jet_third_a[0][0], 1e-9, "first dH mm");
2030 assert_close(first.dhmu_ls[0], jet_third_a[0][1], 1e-9, "first dH ml");
2031 assert_close(first.dh_ls_ls[0], jet_third_a[1][1], 1e-9, "first dH ll");
2032
2033 let second = gaussian_joint_psisecondweights(
2034 &rows,
2035 &array![direction_a[0]],
2036 &array![direction_a[1]],
2037 &array![direction_b[0]],
2038 &array![direction_b[1]],
2039 &array![direction_ab[0]],
2040 &array![direction_ab[1]],
2041 );
2042 let expected_second_score = add_vector_2(
2043 matrix_vector_2(&jet_third_a, &direction_b),
2044 matrix_vector_2(&jet_hessian, &direction_ab),
2045 );
2046 let expected_second_hessian = add_matrix_2(jet_fourth_ab, jet_third_ab);
2047 assert_close(
2048 second.objective_psi_psirow[0],
2049 dot_2(&direction_a, &matrix_vector_2(&jet_hessian, &direction_b))
2050 + dot_2(&jet_score, &direction_ab),
2051 1e-9,
2052 "second psi objective",
2053 );
2054 assert_close(
2055 second.d2scoremu[0],
2056 expected_second_score[0],
2057 1e-9,
2058 "second psi score mu",
2059 );
2060 assert_close(
2061 second.d2score_ls[0],
2062 expected_second_score[1],
2063 1e-9,
2064 "second psi score ls",
2065 );
2066 assert_close(
2067 second.d2hmumu[0],
2068 expected_second_hessian[0][0],
2069 1e-9,
2070 "second psi H mm",
2071 );
2072 assert_close(
2073 second.d2hmu_ls[0],
2074 expected_second_hessian[0][1],
2075 1e-9,
2076 "second psi H ml",
2077 );
2078 assert_close(
2079 second.d2h_ls_ls[0],
2080 expected_second_hessian[1][1],
2081 1e-9,
2082 "second psi H ll",
2083 );
2084
2085 let mixed = gaussian_joint_psi_mixed_driftweights(
2086 &rows,
2087 &array![drift[0]],
2088 &array![drift[1]],
2089 &array![psi[0]],
2090 &array![psi[1]],
2091 &array![drift_psi[0]],
2092 &array![drift_psi[1]],
2093 );
2094 let jet_third_drift =
2095 program_third_contracted(&program, 0, &drift).expect("generic third drift");
2096 let jet_mixed_hessian = add_matrix_2(
2097 program_fourth_contracted(&program, 0, &drift, &psi).expect("generic fourth mixed"),
2098 program_third_contracted(&program, 0, &drift_psi)
2099 .expect("generic third mixed direction"),
2100 );
2101 assert_close(
2102 mixed.dhmumu_u[0],
2103 jet_third_drift[0][0],
2104 1e-9,
2105 "mixed dH mm",
2106 );
2107 assert_close(
2108 mixed.dhmu_ls_u[0],
2109 jet_third_drift[0][1],
2110 1e-9,
2111 "mixed dH ml",
2112 );
2113 assert_close(
2114 mixed.dh_ls_ls_u[0],
2115 jet_third_drift[1][1],
2116 1e-9,
2117 "mixed dH ll",
2118 );
2119 assert_close(
2120 mixed.d2hmumu[0],
2121 jet_mixed_hessian[0][0],
2122 1e-9,
2123 "mixed d2H mm",
2124 );
2125 assert_close(
2126 mixed.d2hmu_ls[0],
2127 jet_mixed_hessian[0][1],
2128 1e-9,
2129 "mixed d2H ml",
2130 );
2131 assert_close(
2132 mixed.d2h_ls_ls[0],
2133 jet_mixed_hessian[1][1],
2134 1e-9,
2135 "mixed d2H ll",
2136 );
2137 }
2138
2139 #[test]
2140 fn generated_gaussian_psi_chain_matches_likelihood_finite_differences_932() {
2141 let y = 0.55;
2142 let base = [0.3, -0.4];
2143 let weight = 1.7;
2144 let direction_a = [0.5, -0.7];
2145 let direction_b = [-0.3, 0.8];
2146 let direction_ab = [0.2, -0.15];
2147 let drift = [0.6, 0.25];
2148 let psi = [-0.4, 0.75];
2149 let drift_psi = [0.12, -0.18];
2150 let rows = gaussian_jointrow_scalars(
2151 &array![y],
2152 &array![base[0]],
2153 &array![base[1]],
2154 &array![weight],
2155 )
2156 .expect("row scalars");
2157 let program = GaussianJointRowProgram::new(&rows);
2158 let atom = program.row_order2(0);
2159
2160 let normalized_value =
2161 0.5 * weight * rows.standardized_residual[0] * rows.standardized_residual[0];
2162 assert_close(
2163 atom.value(),
2164 normalized_value,
2165 1e-12,
2166 "normalized row value",
2167 );
2168 let score_fd = gradient_fd(y, base[0], base[1], weight, 2e-5);
2169 let hessian_fd0 = hessian_fd(y, base[0], base[1], weight, 1e-3);
2170 for axis in 0..2 {
2171 assert_close(
2172 atom.gradient()[axis],
2173 score_fd[axis],
2174 2e-8,
2175 &format!("score FD {axis}"),
2176 );
2177 }
2178 let atom_hessian = [
2179 [atom.hessian_at(0, 0), atom.hessian_at(0, 1)],
2180 [atom.hessian_at(1, 0), atom.hessian_at(1, 1)],
2181 ];
2182 assert_matrix_close(&atom_hessian, &hessian_fd0, 3e-6, "Hessian FD");
2183
2184 let first = gaussian_joint_psi_firstweights(
2185 &rows,
2186 &array![direction_a[0]],
2187 &array![direction_a[1]],
2188 );
2189 let first_step = 1e-2;
2190 let plus = [
2191 base[0] + first_step * direction_a[0],
2192 base[1] + first_step * direction_a[1],
2193 ];
2194 let minus = [
2195 base[0] - first_step * direction_a[0],
2196 base[1] - first_step * direction_a[1],
2197 ];
2198 let objective_first_fd = (row_nll(y, plus[0], plus[1], weight)
2199 - row_nll(y, minus[0], minus[1], weight))
2200 / (2.0 * first_step);
2201 let gradient_plus = gradient_fd(y, plus[0], plus[1], weight, 2e-5);
2202 let gradient_minus = gradient_fd(y, minus[0], minus[1], weight, 2e-5);
2203 let score_first_fd: [f64; 2] = std::array::from_fn(|axis| {
2204 (gradient_plus[axis] - gradient_minus[axis]) / (2.0 * first_step)
2205 });
2206 let hessian_plus = hessian_fd(y, plus[0], plus[1], weight, 1e-3);
2207 let hessian_minus = hessian_fd(y, minus[0], minus[1], weight, 1e-3);
2208 let hessian_first_fd: [[f64; 2]; 2] = std::array::from_fn(|row| {
2209 std::array::from_fn(|column| {
2210 (hessian_plus[row][column] - hessian_minus[row][column]) / (2.0 * first_step)
2211 })
2212 });
2213 assert_close(
2214 first.objective_psirow[0],
2215 objective_first_fd,
2216 2e-4,
2217 "first objective FD",
2218 );
2219 assert_close(
2220 first.dscoremu[0],
2221 score_first_fd[0],
2222 2e-4,
2223 "first score mu FD",
2224 );
2225 assert_close(
2226 first.dscore_ls[0],
2227 score_first_fd[1],
2228 2e-4,
2229 "first score ls FD",
2230 );
2231 assert_close(
2232 first.dhmumu[0],
2233 hessian_first_fd[0][0],
2234 2e-3,
2235 "first H mm FD",
2236 );
2237 assert_close(
2238 first.dhmu_ls[0],
2239 hessian_first_fd[0][1],
2240 2e-3,
2241 "first H ml FD",
2242 );
2243 assert_close(
2244 first.dh_ls_ls[0],
2245 hessian_first_fd[1][1],
2246 2e-3,
2247 "first H ll FD",
2248 );
2249
2250 let second = gaussian_joint_psisecondweights(
2251 &rows,
2252 &array![direction_a[0]],
2253 &array![direction_a[1]],
2254 &array![direction_b[0]],
2255 &array![direction_b[1]],
2256 &array![direction_ab[0]],
2257 &array![direction_ab[1]],
2258 );
2259 let objective_second_fd = mixed_value_fd(
2260 y,
2261 base,
2262 weight,
2263 direction_a,
2264 direction_b,
2265 direction_ab,
2266 5e-3,
2267 );
2268 let score_second_fd = mixed_gradient_fd(
2269 y,
2270 base,
2271 weight,
2272 direction_a,
2273 direction_b,
2274 direction_ab,
2275 1e-2,
2276 2e-5,
2277 );
2278 let hessian_second_fd = mixed_hessian_fd(
2279 y,
2280 base,
2281 weight,
2282 direction_a,
2283 direction_b,
2284 direction_ab,
2285 2e-2,
2286 1e-3,
2287 );
2288 assert_close(
2289 second.objective_psi_psirow[0],
2290 objective_second_fd,
2291 5e-4,
2292 "second objective FD",
2293 );
2294 assert_close(
2295 second.d2scoremu[0],
2296 score_second_fd[0],
2297 2e-3,
2298 "second score mu FD",
2299 );
2300 assert_close(
2301 second.d2score_ls[0],
2302 score_second_fd[1],
2303 2e-3,
2304 "second score ls FD",
2305 );
2306 assert_close(
2307 second.d2hmumu[0],
2308 hessian_second_fd[0][0],
2309 3e-2,
2310 "second H mm FD",
2311 );
2312 assert_close(
2313 second.d2hmu_ls[0],
2314 hessian_second_fd[0][1],
2315 3e-2,
2316 "second H ml FD",
2317 );
2318 assert_close(
2319 second.d2h_ls_ls[0],
2320 hessian_second_fd[1][1],
2321 3e-2,
2322 "second H ll FD",
2323 );
2324
2325 let mixed = gaussian_joint_psi_mixed_driftweights(
2326 &rows,
2327 &array![drift[0]],
2328 &array![drift[1]],
2329 &array![psi[0]],
2330 &array![psi[1]],
2331 &array![drift_psi[0]],
2332 &array![drift_psi[1]],
2333 );
2334 let drift_plus = [
2335 base[0] + first_step * drift[0],
2336 base[1] + first_step * drift[1],
2337 ];
2338 let drift_minus = [
2339 base[0] - first_step * drift[0],
2340 base[1] - first_step * drift[1],
2341 ];
2342 let drift_hessian_plus = hessian_fd(y, drift_plus[0], drift_plus[1], weight, 1e-3);
2343 let drift_hessian_minus = hessian_fd(y, drift_minus[0], drift_minus[1], weight, 1e-3);
2344 let drift_hessian_fd: [[f64; 2]; 2] = std::array::from_fn(|row| {
2345 std::array::from_fn(|column| {
2346 (drift_hessian_plus[row][column] - drift_hessian_minus[row][column])
2347 / (2.0 * first_step)
2348 })
2349 });
2350 let mixed_hessian = mixed_hessian_fd(y, base, weight, drift, psi, drift_psi, 2e-2, 1e-3);
2351 assert_close(
2352 mixed.dhmumu_u[0],
2353 drift_hessian_fd[0][0],
2354 2e-3,
2355 "mixed dH mm FD",
2356 );
2357 assert_close(
2358 mixed.dhmu_ls_u[0],
2359 drift_hessian_fd[0][1],
2360 2e-3,
2361 "mixed dH ml FD",
2362 );
2363 assert_close(
2364 mixed.dh_ls_ls_u[0],
2365 drift_hessian_fd[1][1],
2366 2e-3,
2367 "mixed dH ll FD",
2368 );
2369 assert_close(
2370 mixed.d2hmumu[0],
2371 mixed_hessian[0][0],
2372 3e-2,
2373 "mixed d2H mm FD",
2374 );
2375 assert_close(
2376 mixed.d2hmu_ls[0],
2377 mixed_hessian[0][1],
2378 3e-2,
2379 "mixed d2H ml FD",
2380 );
2381 assert_close(
2382 mixed.d2h_ls_ls[0],
2383 mixed_hessian[1][1],
2384 3e-2,
2385 "mixed d2H ll FD",
2386 );
2387 }
2388
2389 #[test]
2390 fn observed_joint_row_coeffs_match_likelihood_fd_single_source() {
2391 let cases = [
2394 (0.3_f64, -0.4_f64, 1.0_f64, 0.55_f64),
2395 (-1.2, 0.7, 2.5, -0.9),
2396 (0.0, 1.5, 0.4, 0.35),
2397 (2.4, -1.1, 0.8, 1.7),
2398 (-0.6, 0.2, 3.3, -1.1),
2399 ];
2400 let h = 1e-4;
2401 for &(mu, eta_ls, a, y) in &cases {
2402 let (mm_hand, ml_hand, ll_hand) = production_observed_row(y, mu, eta_ls, a);
2403 let (mm_fd, ml_fd, ll_fd) = observed_hessian_fd(y, mu, eta_ls, a, h);
2404 assert!(
2405 (mm_hand - mm_fd).abs() <= 1e-5 * mm_hand.abs().max(1.0),
2406 "H_μμ observed μ={mu} η={eta_ls} y={y}: hand={mm_hand} fd={mm_fd}"
2407 );
2408 assert!(
2409 (ml_hand - ml_fd).abs() <= 1e-5 * ml_hand.abs().max(1.0),
2410 "H_μls observed μ={mu} η={eta_ls} y={y}: hand={ml_hand} fd={ml_fd}"
2411 );
2412 assert!(
2413 (ll_hand - ll_fd).abs() <= 1e-5 * ll_hand.abs().max(1.0),
2414 "H_lsls observed μ={mu} η={eta_ls} y={y}: hand={ll_hand} fd={ll_fd}"
2415 );
2416 }
2417 }
2418
2419 #[test]
2420 fn first_directional_weights_match_observed_finite_difference() {
2421 let cases = [
2424 (0.3_f64, -0.4_f64, 1.0_f64, 0.5_f64, -0.7_f64, 0.55_f64),
2425 (-1.2, 0.7, 2.5, 1.1, 0.3, -0.9),
2426 (0.0, 1.5, 0.4, -0.2, 0.9, 0.35),
2427 (2.4, -1.1, 0.8, 0.6, -0.4, 1.7),
2428 ];
2429 let t = 1e-6;
2430 for &(mu, eta_ls, a, xi_mu, xi_ls, y) in &cases {
2431 let rows =
2432 gaussian_jointrow_scalars(&array![y], &array![mu], &array![eta_ls], &array![a])
2433 .expect("row scalars");
2434 let (w_u, c_u, d_u) =
2435 gaussian_joint_first_directionalweights(&rows, &array![xi_mu], &array![xi_ls]);
2436 let coeffs_at =
2437 |m: f64, e: f64| -> (f64, f64, f64) { production_observed_row(y, m, e, a) };
2438 let (mmp, mlp, llp) = coeffs_at(mu + t * xi_mu, eta_ls + t * xi_ls);
2439 let (mmm, mlm, llm) = coeffs_at(mu - t * xi_mu, eta_ls - t * xi_ls);
2440 let fd_w = (mmp - mmm) / (2.0 * t);
2441 let fd_c = (mlp - mlm) / (2.0 * t);
2442 let fd_d = (llp - llm) / (2.0 * t);
2443 assert!(
2444 (w_u[0] - fd_w).abs() <= 1e-5 * fd_w.abs().max(1.0),
2445 "dH_μμ drift μ={mu} η={eta_ls}: hand={} fd={fd_w}",
2446 w_u[0]
2447 );
2448 assert!(
2449 (c_u[0] - fd_c).abs() <= 1e-5 * fd_c.abs().max(1.0),
2450 "dH_μls drift μ={mu} η={eta_ls}: hand={} fd={fd_c}",
2451 c_u[0]
2452 );
2453 assert!(
2454 (d_u[0] - fd_d).abs() <= 1e-5 * fd_d.abs().max(1.0),
2455 "dH_lsls drift μ={mu} η={eta_ls}: hand={} fd={fd_d}",
2456 d_u[0]
2457 );
2458 }
2459 }
2460
2461 #[test]
2462 fn second_directional_weights_match_first_directional_finite_difference() {
2463 let cases = [
2466 (
2467 0.3_f64, -0.4_f64, 1.0_f64, 0.5_f64, -0.7_f64, 0.8_f64, 0.2_f64, 0.55_f64,
2468 ),
2469 (-1.2, 0.7, 2.5, 1.1, 0.3, -0.6, 0.9, -0.9),
2470 (0.0, 1.5, 0.4, -0.2, 0.9, 0.4, -0.5, 0.35),
2471 ];
2472 let t = 1e-5;
2473 for &(mu, eta_ls, a, xi_mu_u, xi_ls_u, xi_mu_v, xi_ls_v, y) in &cases {
2474 let rows =
2475 gaussian_jointrow_scalars(&array![y], &array![mu], &array![eta_ls], &array![a])
2476 .expect("row scalars");
2477 let (w_uv, c_uv, d_uv) = gaussian_jointsecond_directionalweights(
2478 &rows,
2479 &array![xi_mu_u],
2480 &array![xi_ls_u],
2481 &array![xi_mu_v],
2482 &array![xi_ls_v],
2483 );
2484 let first_at = |m: f64, e: f64| -> (f64, f64, f64) {
2485 let r = gaussian_jointrow_scalars(&array![y], &array![m], &array![e], &array![a])
2486 .expect("row scalars");
2487 let (w_u, c_u, d_u) =
2488 gaussian_joint_first_directionalweights(&r, &array![xi_mu_u], &array![xi_ls_u]);
2489 (w_u[0], c_u[0], d_u[0])
2490 };
2491 let (wp, cp, dp) = first_at(mu + t * xi_mu_v, eta_ls + t * xi_ls_v);
2492 let (wm, cm, dm) = first_at(mu - t * xi_mu_v, eta_ls - t * xi_ls_v);
2493 let fd_w = (wp - wm) / (2.0 * t);
2494 let fd_c = (cp - cm) / (2.0 * t);
2495 let fd_d = (dp - dm) / (2.0 * t);
2496 assert!(
2497 (w_uv[0] - fd_w).abs() <= 1e-4 * fd_w.abs().max(1.0),
2498 "d²H_μμ drift μ={mu} η={eta_ls}: hand={} fd={fd_w}",
2499 w_uv[0]
2500 );
2501 assert!(
2502 (c_uv[0] - fd_c).abs() <= 1e-4 * fd_c.abs().max(1.0),
2503 "d²H_μls drift μ={mu} η={eta_ls}: hand={} fd={fd_c}",
2504 c_uv[0]
2505 );
2506 assert!(
2507 (d_uv[0] - fd_d).abs() <= 1e-4 * fd_d.abs().max(1.0),
2508 "d²H_lsls drift μ={mu} η={eta_ls}: hand={} fd={fd_d}",
2509 d_uv[0]
2510 );
2511 }
2512 }
2513
2514 mod jet_third_fourth_oracle {
2522 use super::*;
2523 use gam_math::jet_tower::{program_fourth_contracted, program_third_contracted};
2524
2525 fn close(hand: f64, jet: f64, label: &str) {
2526 let band = 1e-9 + 1e-9 * hand.abs().max(jet.abs());
2527 assert!(
2528 (hand - jet).abs() <= band,
2529 "{label}: hand {hand:+.15e} vs jet {jet:+.15e} (band {band:.3e})"
2530 );
2531 }
2532
2533 #[test]
2536 fn first_directional_weights_match_jet_third() {
2537 let cases = [
2538 (0.3_f64, -0.4_f64, 1.0_f64, 0.5_f64, -0.7_f64, 0.55_f64),
2539 (-1.2, 0.7, 2.5, 1.1, 0.3, -0.9),
2540 (0.0, 1.5, 0.4, -0.2, 0.9, 0.35),
2541 (2.4, -1.1, 0.8, 0.6, -0.4, 1.7),
2542 (-0.6, 0.2, 3.3, 0.8, -1.0, -1.1),
2543 ];
2544 for &(mu, eta_ls, a, xi_mu, xi_ls, y) in &cases {
2545 let rows =
2546 gaussian_jointrow_scalars(&array![y], &array![mu], &array![eta_ls], &array![a])
2547 .expect("row scalars");
2548 let (w_u, c_u, d_u) =
2549 gaussian_joint_first_directionalweights(&rows, &array![xi_mu], &array![xi_ls]);
2550 let prog = crate::gamlss::GaussianJointRowProgram::new(&rows);
2551 let jt = program_third_contracted(&prog, 0, &[xi_mu, xi_ls]).expect("jet third");
2552 close(w_u[0], jt[0][0], &format!("dH_μμ μ={mu} η={eta_ls}"));
2553 close(c_u[0], jt[0][1], &format!("dH_μls μ={mu} η={eta_ls}"));
2554 close(c_u[0], jt[1][0], &format!("dH_lsμ μ={mu} η={eta_ls}"));
2555 close(d_u[0], jt[1][1], &format!("dH_lsls μ={mu} η={eta_ls}"));
2556 }
2557 }
2558
2559 #[test]
2563 fn second_directional_weights_match_jet_fourth() {
2564 let cases = [
2565 (
2566 0.3_f64, -0.4_f64, 1.0_f64, 0.5_f64, -0.7_f64, 0.8_f64, 0.2_f64, 0.55_f64,
2567 ),
2568 (-1.2, 0.7, 2.5, 1.1, 0.3, -0.6, 0.9, -0.9),
2569 (0.0, 1.5, 0.4, -0.2, 0.9, 0.4, -0.5, 0.35),
2570 (2.4, -1.1, 0.8, 0.6, -0.4, -0.3, 0.7, 1.7),
2571 ];
2572 for &(mu, eta_ls, a, xi_mu_u, xi_ls_u, xi_mu_v, xi_ls_v, y) in &cases {
2573 let rows =
2574 gaussian_jointrow_scalars(&array![y], &array![mu], &array![eta_ls], &array![a])
2575 .expect("row scalars");
2576 let (w_uv, c_uv, d_uv) = gaussian_jointsecond_directionalweights(
2577 &rows,
2578 &array![xi_mu_u],
2579 &array![xi_ls_u],
2580 &array![xi_mu_v],
2581 &array![xi_ls_v],
2582 );
2583 let prog = crate::gamlss::GaussianJointRowProgram::new(&rows);
2584 let jt =
2585 program_fourth_contracted(&prog, 0, &[xi_mu_u, xi_ls_u], &[xi_mu_v, xi_ls_v])
2586 .expect("jet fourth");
2587 close(w_uv[0], jt[0][0], &format!("d²H_μμ μ={mu} η={eta_ls}"));
2588 close(c_uv[0], jt[0][1], &format!("d²H_μls μ={mu} η={eta_ls}"));
2589 close(c_uv[0], jt[1][0], &format!("d²H_lsμ μ={mu} η={eta_ls}"));
2590 close(d_uv[0], jt[1][1], &format!("d²H_lsls μ={mu} η={eta_ls}"));
2591 }
2592 }
2593
2594 #[test]
2614 fn release_measure_gaussian_joint_specialized_vs_generic_tower_932() {
2615 use gam_math::jet_tower::program_row_kernel;
2616 use std::time::Instant;
2617
2618 const ROWS: usize = 512;
2619 let y = Array1::from_shape_fn(ROWS, |i| {
2620 let f = i as f64;
2621 1.4 * (f * 0.17 + 0.3).sin() - 0.5 * (f * 0.09).cos()
2622 });
2623 let mu = Array1::from_shape_fn(ROWS, |i| {
2624 let f = i as f64;
2625 0.9 * (f * 0.13 + 0.7).cos() + 0.3 * (f * 0.05).sin()
2626 });
2627 let eta_ls = Array1::from_shape_fn(ROWS, |i| {
2628 let f = i as f64;
2629 0.8 * (f * 0.11 + 0.2).sin() - 0.25 * (f * 0.07).cos()
2630 });
2631 let weight = Array1::from_shape_fn(ROWS, |i| {
2632 let f = i as f64;
2633 0.6 + 0.4 * (f * 0.19 + 1.0).sin().abs()
2634 });
2635 let rows = gaussian_jointrow_scalars(&y, &mu, &eta_ls, &weight)
2636 .expect("certified release-measure Gaussian joint rows");
2637 let program = crate::gamlss::GaussianJointRowProgram::new(&rows);
2638 let dir_u: Vec<[f64; 2]> = (0..ROWS)
2639 .map(|i| {
2640 let f = i as f64;
2641 [
2642 0.7 * (f * 0.23 + 0.4).cos() - 0.2 * (f * 0.03).sin(),
2643 -0.6 * (f * 0.29 + 0.1).sin() + 0.25 * (f * 0.15).cos(),
2644 ]
2645 })
2646 .collect();
2647 let dir_v: Vec<[f64; 2]> = (0..ROWS)
2648 .map(|i| {
2649 let f = i as f64;
2650 [
2651 -0.5 * (f * 0.21 + 0.9).sin() + 0.3 * (f * 0.06).cos(),
2652 0.8 * (f * 0.27 + 0.5).cos() - 0.15 * (f * 0.04).sin(),
2653 ]
2654 })
2655 .collect();
2656
2657 for row in 0..ROWS {
2660 let atom = program.row_order2(row);
2661 let (tower_value, tower_gradient, tower_hessian) =
2662 program_row_kernel(&program, row).expect("tower warm kernel");
2663 close(atom.value(), tower_value, "release-measure value parity");
2664 let production_gradient = atom.gradient();
2665 for a in 0..2 {
2666 close(
2667 production_gradient[a],
2668 tower_gradient[a],
2669 "release-measure gradient parity",
2670 );
2671 for b in 0..2 {
2672 close(
2673 atom.hessian_at(a, b),
2674 tower_hessian[a][b],
2675 "release-measure hessian parity",
2676 );
2677 }
2678 }
2679 let production_third = program.row_third_contracted(row, &dir_u[row]);
2680 let tower_third = program_third_contracted(&program, row, &dir_u[row])
2681 .expect("tower warm third");
2682 let production_fourth =
2683 program.row_fourth_contracted(row, &dir_u[row], &dir_v[row]);
2684 let tower_fourth =
2685 program_fourth_contracted(&program, row, &dir_u[row], &dir_v[row])
2686 .expect("tower warm fourth");
2687 for a in 0..2 {
2688 for b in 0..2 {
2689 close(
2690 production_third[a][b],
2691 tower_third[a][b],
2692 "release-measure third parity",
2693 );
2694 close(
2695 production_fourth[a][b],
2696 tower_fourth[a][b],
2697 "release-measure fourth parity",
2698 );
2699 }
2700 }
2701 }
2702
2703 let best_secs = |sweep: &mut dyn FnMut() -> f64| -> f64 {
2704 let mut best = f64::INFINITY;
2705 for _ in 0..5 {
2706 let started = Instant::now();
2707 let checksum = sweep();
2708 assert!(
2709 checksum.is_finite(),
2710 "Gaussian joint release-measure checksum must stay finite"
2711 );
2712 best = best.min(started.elapsed().as_secs_f64());
2713 }
2714 best
2715 };
2716
2717 let mut production_order2_sweep = || {
2718 let mut checksum = 0.0_f64;
2719 for row in 0..ROWS {
2720 let atom = program.row_order2(row);
2721 checksum += atom.value() + atom.gradient()[0] + atom.hessian_at(0, 0);
2722 }
2723 checksum
2724 };
2725 let production_order2_secs = best_secs(&mut production_order2_sweep);
2726 let mut tower_order2_sweep = || {
2727 let mut checksum = 0.0_f64;
2728 for row in 0..ROWS {
2729 let (value, gradient, hessian) =
2730 program_row_kernel(&program, row).expect("tower kernel");
2731 checksum += value + gradient[0] + hessian[0][0];
2732 }
2733 checksum
2734 };
2735 let tower_order2_secs = best_secs(&mut tower_order2_sweep);
2736
2737 let mut production_third_sweep = || {
2738 let mut checksum = 0.0_f64;
2739 for row in 0..ROWS {
2740 let third = program.row_third_contracted(row, &dir_u[row]);
2741 checksum += third[0][0] + third[0][1] + third[1][1];
2742 }
2743 checksum
2744 };
2745 let production_third_secs = best_secs(&mut production_third_sweep);
2746 let mut tower_third_sweep = || {
2747 let mut checksum = 0.0_f64;
2748 for row in 0..ROWS {
2749 let third = program_third_contracted(&program, row, &dir_u[row])
2750 .expect("tower third");
2751 checksum += third[0][0] + third[0][1] + third[1][1];
2752 }
2753 checksum
2754 };
2755 let tower_third_secs = best_secs(&mut tower_third_sweep);
2756
2757 let mut production_fourth_sweep = || {
2758 let mut checksum = 0.0_f64;
2759 for row in 0..ROWS {
2760 let fourth = program.row_fourth_contracted(row, &dir_u[row], &dir_v[row]);
2761 checksum += fourth[0][0] + fourth[0][1] + fourth[1][1];
2762 }
2763 checksum
2764 };
2765 let production_fourth_secs = best_secs(&mut production_fourth_sweep);
2766 let mut tower_fourth_sweep = || {
2767 let mut checksum = 0.0_f64;
2768 for row in 0..ROWS {
2769 let fourth =
2770 program_fourth_contracted(&program, row, &dir_u[row], &dir_v[row])
2771 .expect("tower fourth");
2772 checksum += fourth[0][0] + fourth[0][1] + fourth[1][1];
2773 }
2774 checksum
2775 };
2776 let tower_fourth_secs = best_secs(&mut tower_fourth_sweep);
2777
2778 for (channel, production_secs, tower_secs) in [
2779 ("order2", production_order2_secs, tower_order2_secs),
2780 ("third", production_third_secs, tower_third_secs),
2781 ("fourth", production_fourth_secs, tower_fourth_secs),
2782 ] {
2783 let production_ns = production_secs * 1e9 / ROWS as f64;
2784 let tower_ns = tower_secs * 1e9 / ROWS as f64;
2785 eprintln!(
2786 "GAUSSIAN-JOINT-RELEASE-932 channel={channel} rows={ROWS} \
2787 production_ns={production_ns:.3} generic_tower_ns={tower_ns:.3} \
2788 generic_tower_over_production={:.6}",
2789 tower_ns / production_ns,
2790 );
2791 }
2792 }
2793 }
2794}
2795
2796macro_rules! impl_location_scale_joint_psi_custom_family_hooks {
2822 ($label:literal) => {
2823 fn exact_newton_joint_psi_terms(
2824 &self,
2825 block_states: &[ParameterBlockState],
2826 specs: &[ParameterBlockSpec],
2827 hyper_layout: &crate::custom_family::CustomFamilyHyperLayout,
2828 psi_index: usize,
2829 ) -> Result<Option<gam_problem::ExactNewtonJointPsiTerms>, String> {
2830 if hyper_layout.family_axis_count() != 0 {
2831 return Err(concat!($label, " does not declare family-owned hyper axes").to_string());
2832 }
2833 self.exact_newton_joint_psi_terms_for_specs(
2834 block_states,
2835 specs,
2836 hyper_layout.design_derivative_blocks(),
2837 psi_index,
2838 )
2839 }
2840
2841 fn exact_newton_joint_psisecond_order_terms(
2842 &self,
2843 block_states: &[ParameterBlockState],
2844 specs: &[ParameterBlockSpec],
2845 hyper_layout: &crate::custom_family::CustomFamilyHyperLayout,
2846 psi_i: usize,
2847 psi_j: usize,
2848 ) -> Result<Option<gam_problem::ExactNewtonJointPsiSecondOrderTerms>, String> {
2849 if hyper_layout.family_axis_count() != 0 {
2850 return Err(concat!($label, " does not declare family-owned hyper axes").to_string());
2851 }
2852 self.exact_newton_joint_psisecond_order_terms_for_specs(
2853 block_states,
2854 specs,
2855 hyper_layout.design_derivative_blocks(),
2856 psi_i,
2857 psi_j,
2858 )
2859 }
2860
2861 fn exact_newton_joint_psihessian_directional_derivative(
2862 &self,
2863 block_states: &[ParameterBlockState],
2864 specs: &[ParameterBlockSpec],
2865 hyper_layout: &crate::custom_family::CustomFamilyHyperLayout,
2866 psi_index: usize,
2867 d_beta_flat: &Array1<f64>,
2868 ) -> Result<Option<Array2<f64>>, String> {
2869 if hyper_layout.family_axis_count() != 0 {
2870 return Err(concat!($label, " does not declare family-owned hyper axes").to_string());
2871 }
2872 self.exact_newton_joint_psihessian_directional_derivative_for_specs(
2873 block_states,
2874 specs,
2875 hyper_layout.design_derivative_blocks(),
2876 psi_index,
2877 d_beta_flat,
2878 )
2879 }
2880 };
2881}
2882
2883pub(crate) use impl_location_scale_joint_psi_custom_family_hooks;