1use super::*;
6
7#[derive(Debug)]
13pub enum GamlssError {
14 DimensionMismatch { reason: String },
17 InvalidInput { reason: String },
21 NonFinite { reason: String },
24 UnsupportedConfiguration { reason: String },
28 ConstraintViolation { reason: String },
31 NumericalFailure { reason: String },
35 RowGeometryUnrepresentable {
40 row: usize,
41 quantity: &'static str,
42 eta: f64,
43 value: f64,
44 },
45}
46
47impl std::fmt::Display for GamlssError {
48 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49 match self {
50 Self::DimensionMismatch { reason }
51 | Self::InvalidInput { reason }
52 | Self::NonFinite { reason }
53 | Self::UnsupportedConfiguration { reason }
54 | Self::ConstraintViolation { reason }
55 | Self::NumericalFailure { reason } => f.write_str(reason),
56 Self::RowGeometryUnrepresentable {
57 row,
58 quantity,
59 eta,
60 value,
61 } => write!(
62 f,
63 "GAMLSS row geometry is not representable at row {row}: {quantity} evaluated from eta={eta:?} produced {value:?}"
64 ),
65 }
66 }
67}
68
69impl std::error::Error for GamlssError {}
70
71impl From<GamlssError> for String {
72 fn from(err: GamlssError) -> String {
73 err.to_string()
74 }
75}
76
77impl From<crate::block_layout::block_count::BlockCountMismatch> for GamlssError {
78 fn from(err: crate::block_layout::block_count::BlockCountMismatch) -> GamlssError {
79 GamlssError::DimensionMismatch {
80 reason: err.message(),
81 }
82 }
83}
84
85pub(crate) const EXACT_DENSE_BLOCK_BUDGET_BYTES: usize = 512 * 1024 * 1024;
86
87pub(crate) const EXACT_DENSE_TOTAL_BUDGET_BYTES: usize = 2 * 1024 * 1024 * 1024;
88
89pub(crate) const GAMLSS_ROWWISE_PAR_MIN_N: usize = 4096;
90
91pub(crate) const GAMLSS_PROJECTED_TRACE_TARGET_BYTES: usize = 32 * 1024 * 1024;
92
93pub(crate) const GAMLSS_PROJECTED_TRACE_MIN_CHUNK_ROWS: usize = 64;
94
95pub(crate) const GAMLSS_PROJECTED_TRACE_MAX_CHUNK_ROWS: usize = 8192;
96
97pub(crate) fn gamlss_projected_trace_chunk_rows(
98 rank: usize,
99 projected_channel_count: usize,
100 gram_column_count: usize,
101) -> usize {
102 let per_row_values = rank
103 .saturating_mul(projected_channel_count.max(1))
104 .saturating_add(gram_column_count.max(1))
105 .max(1);
106 let per_row_bytes = per_row_values.saturating_mul(std::mem::size_of::<f64>());
107 let rows = GAMLSS_PROJECTED_TRACE_TARGET_BYTES / per_row_bytes.max(1);
108 rows.clamp(
109 GAMLSS_PROJECTED_TRACE_MIN_CHUNK_ROWS,
110 GAMLSS_PROJECTED_TRACE_MAX_CHUNK_ROWS,
111 )
112}
113
114pub(crate) fn gamlss_rowwise_map<F>(n: usize, f: F) -> Array1<f64>
115where
116 F: Fn(usize) -> f64 + Sync,
117{
118 if n >= GAMLSS_ROWWISE_PAR_MIN_N {
119 Array1::from((0..n).into_par_iter().map(&f).collect::<Vec<f64>>())
120 } else {
121 Array1::from_iter((0..n).map(f))
122 }
123}
124
125pub(crate) fn gamlss_rowwise_map_result<F>(n: usize, f: F) -> Result<Array1<f64>, String>
126where
127 F: Fn(usize) -> Result<f64, String> + Sync,
128{
129 if n >= GAMLSS_ROWWISE_PAR_MIN_N {
130 let values: Result<Vec<f64>, String> = (0..n).into_par_iter().map(&f).collect();
131 Ok(Array1::from(values?))
132 } else {
133 let mut out = Array1::<f64>::zeros(n);
134 for i in 0..n {
135 out[i] = f(i)?;
136 }
137 Ok(out)
138 }
139}
140
141pub(crate) enum DenseOrOperator<'a> {
142 Borrowed(&'a Array2<f64>),
143 Owned(Array2<f64>),
144 Operator(DesignMatrix),
145}
146
147impl DenseOrOperator<'_> {
148 pub(crate) fn nrows(&self) -> usize {
149 match self {
150 Self::Borrowed(dense) => dense.nrows(),
151 Self::Owned(dense) => dense.nrows(),
152 Self::Operator(design) => design.nrows(),
153 }
154 }
155
156 pub(crate) fn ncols(&self) -> usize {
157 match self {
158 Self::Borrowed(dense) => dense.ncols(),
159 Self::Owned(dense) => dense.ncols(),
160 Self::Operator(design) => design.ncols(),
161 }
162 }
163
164 pub(crate) fn row_chunk(&self, rows: std::ops::Range<usize>) -> Result<Array2<f64>, String> {
165 match self {
166 Self::Borrowed(dense) => Ok(dense.slice(s![rows, ..]).to_owned()),
167 Self::Owned(dense) => Ok(dense.slice(s![rows, ..]).to_owned()),
168 Self::Operator(design) => design.try_row_chunk(rows).map_err(|e| e.to_string()),
169 }
170 }
171
172 pub(crate) fn dot(&self, beta: ArrayView1<'_, f64>) -> Array1<f64> {
173 let n = self.nrows();
174 let p = self.ncols();
175 assert_eq!(beta.len(), p);
176 match self {
177 Self::Borrowed(dense) => fast_av(*dense, &beta),
178 Self::Owned(dense) => fast_av(dense, &beta),
179 Self::Operator(design) => {
180 let mut out = Array1::<f64>::zeros(n);
181 for rows in exact_design_row_chunks(n, p) {
182 let chunk = design
183 .try_row_chunk(rows.clone())
184 .expect("gamlss DesignSlot::dot: design row chunk materialization failed");
185 out.slice_mut(s![rows]).assign(&fast_av(&chunk, &beta));
186 }
187 out
188 }
189 }
190 }
191}
192
193pub(crate) fn dense_block_from_spec<'a>(
200 spec: &'a ParameterBlockSpec,
201 material_policy: &gam_runtime::resource::MaterializationPolicy,
202 materialization_label: &str,
203) -> Result<Cow<'a, Array2<f64>>, String> {
204 match spec.design.as_dense_ref() {
205 Some(d) => Ok(Cow::Borrowed(d)),
206 None => Ok(Cow::Owned(
207 spec.design
208 .try_to_dense_with_policy(material_policy, "gamlss dense_block_from_spec")
209 .map_err(|e| format!("{materialization_label}: {e}"))?
210 .as_ref()
211 .clone(),
212 )),
213 }
214}
215
216pub(crate) fn dense_locscale_block_designs_fromspecs<'a>(
223 specs: &'a [ParameterBlockSpec],
224 expected_count: usize,
225 family_name: &str,
226 short_family_name: &str,
227 primary_block_idx: usize,
228 log_sigma_block_idx: usize,
229 primary_label: &str,
230 material_policy: &gam_runtime::resource::MaterializationPolicy,
231) -> Result<(Cow<'a, Array2<f64>>, Cow<'a, Array2<f64>>), String> {
232 if specs.len() != expected_count {
233 return Err(GamlssError::DimensionMismatch {
234 reason: format!(
235 "{family_name} expects {expected_count} specs, got {}",
236 specs.len()
237 ),
238 }
239 .into());
240 }
241 let primary = dense_block_from_spec(
242 &specs[primary_block_idx],
243 material_policy,
244 &format!("{short_family_name} dense_block_designs_fromspecs {primary_label}"),
245 )?;
246 let log_sigma = dense_block_from_spec(
247 &specs[log_sigma_block_idx],
248 material_policy,
249 &format!("{short_family_name} dense_block_designs_fromspecs log_sigma"),
250 )?;
251 Ok((primary, log_sigma))
252}
253
254pub(crate) fn gamlss_joint_gradient_from_working_sets(
270 eval: &FamilyEvaluation,
271 specs: &[ParameterBlockSpec],
272 states: &[ParameterBlockState],
273) -> Result<ExactNewtonJointGradientEvaluation, String> {
274 if eval.blockworking_sets.len() != specs.len() || states.len() != specs.len() {
275 return Err(GamlssError::DimensionMismatch { reason: format!(
276 "gamlss joint gradient: block/spec/state count mismatch (working_sets={}, specs={}, states={})",
277 eval.blockworking_sets.len(),
278 specs.len(),
279 states.len()
280 ) }
281 .into());
282 }
283 let total: usize = specs.iter().map(|spec| spec.design.ncols()).sum();
284 let mut gradient = Array1::<f64>::zeros(total);
285 let mut offset = 0usize;
286 for ((spec, work), state) in specs
287 .iter()
288 .zip(eval.blockworking_sets.iter())
289 .zip(states.iter())
290 {
291 let width = spec.design.ncols();
292 let block_grad = match work {
293 BlockWorkingSet::Diagonal {
294 working_response,
295 working_weights,
296 } => {
297 let n = working_response.len();
298 if working_weights.len() != n || state.eta.len() != n || spec.design.nrows() != n {
299 return Err(GamlssError::DimensionMismatch { reason: format!(
300 "gamlss joint gradient: diagonal working-set length mismatch (z={}, w={}, η={}, X_rows={})",
301 n,
302 working_weights.len(),
303 state.eta.len(),
304 spec.design.nrows()
305 ) }
306 .into());
307 }
308 let mut weighted = Array1::<f64>::zeros(n);
309 for i in 0..n {
310 weighted[i] = working_weights[i] * (working_response[i] - state.eta[i]);
311 }
312 spec.design.transpose_vector_multiply(&weighted)
313 }
314 BlockWorkingSet::ExactNewton {
315 gradient: block_gradient,
316 ..
317 } => block_gradient.clone(),
318 };
319 if block_grad.len() != width {
320 return Err(GamlssError::DimensionMismatch { reason: format!(
321 "gamlss joint gradient: assembled block gradient length {} != design cols {width}",
322 block_grad.len()
323 ) }
324 .into());
325 }
326 gradient
327 .slice_mut(s![offset..offset + width])
328 .assign(&block_grad);
329 offset += width;
330 }
331 Ok(ExactNewtonJointGradientEvaluation {
332 log_likelihood: eval.log_likelihood,
333 gradient,
334 })
335}
336
337pub(crate) fn dense_locscale_block_designs_cached<'a>(
344 primary_design: Option<&'a DesignMatrix>,
345 log_sigma_design: Option<&'a DesignMatrix>,
346 family_name: &str,
347 short_family_name: &str,
348 primary_label: &str,
349 material_policy: &gam_runtime::resource::MaterializationPolicy,
350) -> Result<(Cow<'a, Array2<f64>>, Cow<'a, Array2<f64>>), String> {
351 let primary_design = primary_design
352 .ok_or_else(|| format!("{family_name} exact path is missing {primary_label} design"))?;
353 let log_sigma_design = log_sigma_design
354 .ok_or_else(|| format!("{family_name} exact path is missing log-sigma design"))?;
355 let primary = match primary_design.as_dense_ref() {
356 Some(d) => Cow::Borrowed(d),
357 None => Cow::Owned(
358 primary_design
359 .try_to_dense_with_policy(material_policy, "gamlss dense_locscale_block_designs")
360 .map_err(|e| {
361 format!("{short_family_name} dense_block_designs {primary_label}: {e}")
362 })?
363 .as_ref()
364 .clone(),
365 ),
366 };
367 let log_sigma = match log_sigma_design.as_dense_ref() {
368 Some(d) => Cow::Borrowed(d),
369 None => Cow::Owned(
370 log_sigma_design
371 .try_to_dense_with_policy(material_policy, "gamlss dense_locscale_block_designs")
372 .map_err(|e| format!("{short_family_name} dense_block_designs log_sigma: {e}"))?
373 .as_ref()
374 .clone(),
375 ),
376 };
377 Ok((primary, log_sigma))
378}
379
380pub(crate) struct LocScalePsiDirectionParts {
385 pub(crate) block_idx: usize,
386 pub(crate) local_idx: usize,
387 pub(crate) primary_psi: PsiDesignMap,
388 pub(crate) log_sigma_psi: PsiDesignMap,
389 pub(crate) primary_z: Array1<f64>,
390 pub(crate) log_sigma_z: Array1<f64>,
391}
392
393pub(crate) fn locscale_joint_psi_direction_parts(
403 block_states: &[ParameterBlockState],
404 derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
405 psi_index: usize,
406 n: usize,
407 p_primary: usize,
408 p_log_sigma: usize,
409 primary_block_idx: usize,
410 log_sigma_block_idx: usize,
411 expected_blocks: usize,
412 family_name: &str,
413 primary_label: &str,
414 policy: &gam_runtime::resource::ResourcePolicy,
415) -> Result<Option<LocScalePsiDirectionParts>, String> {
416 validate_block_count::<GamlssError>(family_name, expected_blocks, block_states.len())?;
417 if derivative_blocks.len() != expected_blocks {
418 return Err(GamlssError::DimensionMismatch {
419 reason: format!(
420 "{family_name} joint psi direction expects {expected_blocks} derivative block lists, got {}",
421 derivative_blocks.len()
422 ),
423 }
424 .into());
425 }
426 let beta_primary = &block_states[primary_block_idx].beta;
427 let beta_log_sigma = &block_states[log_sigma_block_idx].beta;
428
429 let mut global = 0usize;
430 for (block_idx, block_derivs) in derivative_blocks.iter().enumerate() {
431 for (local_idx, deriv) in block_derivs.iter().enumerate() {
432 if global == psi_index {
433 let primary_psi;
434 let log_sigma_psi;
435 let primary_z;
436 let log_sigma_z;
437 if block_idx == primary_block_idx {
438 primary_psi = resolve_custom_family_x_psi_map(
439 deriv,
440 n,
441 p_primary,
442 0..n,
443 &format!("{family_name} {primary_label}"),
444 policy,
445 )?;
446 primary_z = primary_psi
447 .forward_mul(beta_primary.view())
448 .map_err(|e| format!("{family_name} {primary_label} forward_mul: {e}"))?;
449 log_sigma_psi = PsiDesignMap::Zero {
450 nrows: n,
451 ncols: p_log_sigma,
452 };
453 log_sigma_z = Array1::<f64>::zeros(n);
454 } else if block_idx == log_sigma_block_idx {
455 log_sigma_psi = resolve_custom_family_x_psi_map(
456 deriv,
457 n,
458 p_log_sigma,
459 0..n,
460 &format!("{family_name} log-sigma"),
461 policy,
462 )?;
463 log_sigma_z = log_sigma_psi
464 .forward_mul(beta_log_sigma.view())
465 .map_err(|e| format!("{family_name} log-sigma forward_mul: {e}"))?;
466 primary_psi = PsiDesignMap::Zero {
467 nrows: n,
468 ncols: p_primary,
469 };
470 primary_z = Array1::<f64>::zeros(n);
471 } else {
472 return Ok(None);
473 }
474 return Ok(Some(LocScalePsiDirectionParts {
475 block_idx,
476 local_idx,
477 primary_psi,
478 log_sigma_psi,
479 primary_z,
480 log_sigma_z,
481 }));
482 }
483 global += 1;
484 }
485 }
486 Ok(None)
487}
488
489pub(crate) struct LocScalePsiDriftConfig<'a> {
494 pub(crate) n: usize,
495 pub(crate) p_primary: usize,
496 pub(crate) p_log_sigma: usize,
497 pub(crate) primary_block_idx: usize,
498 pub(crate) log_sigma_block_idx: usize,
499 pub(crate) family_name: &'a str,
500 pub(crate) primary_label: &'a str,
501 pub(crate) policy: &'a gam_runtime::resource::ResourcePolicy,
502}
503
504pub(crate) fn locscale_joint_psisecond_design_drifts(
505 block_states: &[ParameterBlockState],
506 derivative_blocks: &[Vec<crate::custom_family::CustomFamilyBlockPsiDerivative>],
507 psi_a: &LocationScaleJointPsiDirection,
508 psi_b: &LocationScaleJointPsiDirection,
509 cfg: LocScalePsiDriftConfig<'_>,
510) -> Result<LocationScaleJointPsiSecondDrifts, String> {
511 let beta_primary = &block_states[cfg.primary_block_idx].beta;
512 let beta_log_sigma = &block_states[cfg.log_sigma_block_idx].beta;
513 let mut primary_ab_action = None;
514 let mut log_sigma_ab_action = None;
515 let mut primary_ab = None;
516 let mut log_sigma_ab = None;
517
518 if psi_a.block_idx == psi_b.block_idx {
522 let deriv = &derivative_blocks[psi_a.block_idx][psi_a.local_idx];
523 let deriv_b = &derivative_blocks[psi_b.block_idx][psi_b.local_idx];
524 if psi_a.block_idx == cfg.primary_block_idx {
525 let (action, matrix) = psi_psi_map_to_drift_slots(
526 deriv,
527 deriv_b,
528 psi_b.local_idx,
529 cfg.n,
530 cfg.p_primary,
531 &format!("{} {}", cfg.family_name, cfg.primary_label),
532 cfg.policy,
533 )?;
534 primary_ab_action = action;
535 primary_ab = matrix;
536 } else if psi_a.block_idx == cfg.log_sigma_block_idx {
537 let (action, matrix) = psi_psi_map_to_drift_slots(
538 deriv,
539 deriv_b,
540 psi_b.local_idx,
541 cfg.n,
542 cfg.p_log_sigma,
543 &format!("{} log-sigma", cfg.family_name),
544 cfg.policy,
545 )?;
546 log_sigma_ab_action = action;
547 log_sigma_ab = matrix;
548 }
549 }
550
551 let z_primary_ab = second_psi_linear_map(
552 primary_ab_action.as_ref(),
553 primary_ab.as_ref(),
554 cfg.n,
555 cfg.p_primary,
556 )
557 .forward_mul(beta_primary.view());
558 let z_ls_ab = second_psi_linear_map(
559 log_sigma_ab_action.as_ref(),
560 log_sigma_ab.as_ref(),
561 cfg.n,
562 cfg.p_log_sigma,
563 )
564 .forward_mul(beta_log_sigma.view());
565
566 Ok(LocationScaleJointPsiSecondDrifts {
567 x_primary_ab_action: primary_ab_action,
568 x_ls_ab_action: log_sigma_ab_action,
569 x_primary_ab: primary_ab,
570 x_ls_ab: log_sigma_ab,
571 z_primary_ab,
572 z_ls_ab,
573 })
574}
575
576pub(crate) fn psi_psi_map_to_drift_slots(
577 deriv: &crate::custom_family::CustomFamilyBlockPsiDerivative,
578 deriv_b: &crate::custom_family::CustomFamilyBlockPsiDerivative,
579 local_idx_b: usize,
580 n: usize,
581 p: usize,
582 label: &str,
583 policy: &gam_runtime::resource::ResourcePolicy,
584) -> Result<
585 (
586 Option<crate::custom_family::CustomFamilyPsiSecondDesignAction>,
587 Option<Array2<f64>>,
588 ),
589 String,
590> {
591 match resolve_custom_family_x_psi_psi_map(
592 deriv,
593 deriv_b,
594 local_idx_b,
595 n,
596 p,
597 0..n,
598 label,
599 policy,
600 )? {
601 crate::custom_family::PsiDesignMap::Second { action } => Ok((Some(action), None)),
602 crate::custom_family::PsiDesignMap::Dense { matrix } => Ok((None, Some((*matrix).clone()))),
603 crate::custom_family::PsiDesignMap::Zero { .. } => Ok((None, None)),
604 crate::custom_family::PsiDesignMap::First { .. } => {
605 Err(GamlssError::UnsupportedConfiguration {
606 reason: format!("{label}: unexpected First variant from _psi_psi_map"),
607 }
608 .into())
609 }
610 }
611}
612
613pub(crate) fn dense_block_or_operator<'a>(
614 design: &'a DesignMatrix,
615 n: usize,
616 p: usize,
617 budget_bytes: usize,
618 policy: &gam_runtime::resource::ResourcePolicy,
619) -> DenseOrOperator<'a> {
620 if let Some(dense) = design.as_dense_ref() {
621 return DenseOrOperator::Borrowed(dense);
622 }
623
624 let dense_bytes = 8usize.saturating_mul(n).saturating_mul(p);
625 if dense_bytes <= budget_bytes
626 && let Ok(arc) = design
627 .try_to_dense_with_policy(&policy.material_policy(), "gamlss dense_block_or_operator")
628 {
629 return DenseOrOperator::Owned(arc.as_ref().clone());
630 }
631
632 DenseOrOperator::Operator(design.clone())
633}
634
635pub(crate) fn dense_blocks_planned_budget(blocks: &[&DesignMatrix]) -> Vec<usize> {
636 let mut planned = vec![0; blocks.len()];
637 let mut total = 0usize;
638 for (idx, design) in blocks.iter().enumerate() {
639 if design.as_dense_ref().is_some() {
640 continue;
641 }
642 let bytes = 8usize
643 .saturating_mul(design.nrows())
644 .saturating_mul(design.ncols());
645 if bytes <= EXACT_DENSE_BLOCK_BUDGET_BYTES
646 && total.saturating_add(bytes) <= EXACT_DENSE_TOTAL_BUDGET_BYTES
647 {
648 planned[idx] = bytes;
649 total += bytes;
650 }
651 }
652 planned
653}
654
655pub(crate) fn exact_design_row_chunks(
656 n: usize,
657 p: usize,
658) -> impl Iterator<Item = std::ops::Range<usize>> {
659 const TARGET_BYTES: usize = 8 * 1024 * 1024;
660 const MIN_ROWS: usize = 512;
661 const MAX_ROWS: usize = 131_072;
662 let rows = (TARGET_BYTES / (p.max(1) * 8))
663 .clamp(MIN_ROWS, MAX_ROWS)
664 .min(n.max(1));
665 (0..n)
666 .step_by(rows)
667 .map(move |start| start..(start + rows).min(n))
668}
669
670pub(crate) fn design_weighted_column_squares(
671 design: &DesignMatrix,
672 weights: &Array1<f64>,
673) -> Result<Array1<f64>, String> {
674 let n = design.nrows();
675 let p = design.ncols();
676 if weights.len() != n {
677 return Err(GamlssError::DimensionMismatch {
678 reason: format!(
679 "design weighted column squares dimension mismatch: weights={}, rows={}",
680 weights.len(),
681 n
682 ),
683 }
684 .into());
685 }
686 let mut out = Array1::<f64>::zeros(p);
687 for rows in exact_design_row_chunks(n, p) {
688 let chunk = design.try_row_chunk(rows.clone()).map_err(|e| {
689 format!("design weighted column squares row chunk materialization failed: {e}")
690 })?;
691 for (local_i, row) in chunk.outer_iter().enumerate() {
692 let w = weights[rows.start + local_i];
693 if w == 0.0 {
694 continue;
695 }
696 for j in 0..p {
697 let x = row[j];
698 out[j] += w * x * x;
699 }
700 }
701 }
702 Ok(out)
703}
704
705#[inline]
706pub(crate) fn logb_dlog_sigma_deta(sigma: f64, d_sigma_deta: f64) -> f64 {
707 d_sigma_deta / sigma
708}
709
710#[inline]
713pub(crate) fn positive_frexp(x: f64) -> (f64, i32) {
714 assert!(x.is_finite() && x > 0.0);
715 let bits = x.to_bits();
716 let raw_exp = ((bits >> 52) & 0x7ff) as i32;
717 let fraction = bits & ((1_u64 << 52) - 1);
718 if raw_exp != 0 {
719 let mantissa = f64::from_bits((1023_u64 << 52) | fraction);
720 (mantissa, raw_exp - 1023)
721 } else {
722 let leading = 63_i32 - fraction.leading_zeros() as i32;
723 let shift = 52_i32 - leading;
724 let normalized = fraction << shift;
725 let mantissa = f64::from_bits((1023_u64 << 52) | (normalized & ((1_u64 << 52) - 1)));
726 (mantissa, -1022 - shift)
727 }
728}
729
730#[inline]
731pub(crate) fn scale_normalized_power_of_two(mut mantissa: f64, mut exponent: i32) -> f64 {
732 while mantissa >= 2.0 {
733 mantissa *= 0.5;
734 exponent += 1;
735 }
736 while mantissa < 1.0 {
737 mantissa *= 2.0;
738 exponent -= 1;
739 }
740 if exponent > 1023 {
741 return f64::INFINITY;
742 }
743 if exponent >= -1022 {
744 let power = f64::from_bits(((exponent + 1023) as u64) << 52);
745 return mantissa * power;
746 }
747 if exponent < -1075 {
748 return 0.0;
749 }
750 let units = mantissa * 2.0_f64.powi(exponent + 1074);
751 units * f64::from_bits(1)
752}
753
754#[inline]
758pub(crate) fn scaled_positive_product_quotient(a: f64, b: f64, c: f64, d: f64) -> f64 {
759 assert!(a.is_finite() && a > 0.0);
760 assert!(b.is_finite() && b > 0.0);
761 assert!(c.is_finite() && c > 0.0);
762 assert!(d.is_finite() && d > 0.0);
763 let (ma, ea) = positive_frexp(a);
764 let (mb, eb) = positive_frexp(b);
765 let (mc, ec) = positive_frexp(c);
766 let (md, ed) = positive_frexp(d);
767 scale_normalized_power_of_two((ma * mb) * (mc / md), ea + eb + ec - ed)
768}
769
770#[inline]
771pub(crate) fn scaled_signed_product3(a: f64, b: f64, c: f64) -> f64 {
772 if a == 0.0 || b == 0.0 || c == 0.0 {
773 return 0.0;
774 }
775 let sign = a.signum() * b.signum() * c.signum();
776 sign * scaled_positive_product_quotient(a.abs(), b.abs(), c.abs(), 1.0)
777}
778
779#[inline]
780pub(crate) fn gaussian_log_sigma_irlsinfo_directional_derivative(
781 row: usize,
782 eta: f64,
783 weight: f64,
784 sigma: f64,
785 d_sigma_deta: f64,
786 d_eta: f64,
787) -> Result<f64, String> {
788 if weight == 0.0 || d_eta == 0.0 {
789 return Ok(0.0);
790 }
791 let g = logb_dlog_sigma_deta(sigma, d_sigma_deta);
792 if !g.is_finite() || g <= 0.0 || g > 1.0 {
793 return Err(GamlssError::RowGeometryUnrepresentable {
794 row,
795 quantity: "Gaussian log-scale link derivative",
796 eta,
797 value: g,
798 }
799 .into());
800 }
801 let info = scaled_positive_product_quotient(weight, g, g, 0.5);
802 if !info.is_finite() || info <= 0.0 {
803 return Err(GamlssError::RowGeometryUnrepresentable {
804 row,
805 quantity: "Gaussian log-scale Fisher information",
806 eta,
807 value: info,
808 }
809 .into());
810 }
811 let dw = scaled_signed_product3(info, 2.0 * (1.0 - g), d_eta);
812 if !dw.is_finite() {
813 return Err(GamlssError::RowGeometryUnrepresentable {
814 row,
815 quantity: "Gaussian log-scale Fisher-information directional derivative",
816 eta,
817 value: dw,
818 }
819 .into());
820 }
821 Ok(dw)
822}
823
824#[derive(Clone, Copy)]
825pub(crate) struct GaussianDiagonalRowKernel {
826 pub(crate) log_likelihood: f64,
827 pub(crate) location_working_weight: f64,
828 pub(crate) log_sigma_working_weight: f64,
829 pub(crate) log_sigma_working_response: f64,
830 pub(crate) joint_w: f64,
831 pub(crate) joint_m: f64,
832 pub(crate) joint_n: f64,
833 pub(crate) standardized_residual: f64,
834 pub(crate) inv_sigma: f64,
835 pub(crate) kappa: f64,
836 pub(crate) kappa_prime: f64,
837}
838
839#[inline]
840pub(crate) fn gaussian_diagonal_row_kernel(
841 row: usize,
842 y: f64,
843 location_eta: f64,
844 eta_log_sigma: f64,
845 obs_weight: f64,
846 ln2pi: f64,
847) -> Result<GaussianDiagonalRowKernel, String> {
848 if !y.is_finite() || !location_eta.is_finite() || !eta_log_sigma.is_finite() {
849 return Err(GamlssError::NonFinite {
850 reason: format!(
851 "Gaussian location-scale requires finite row inputs at row {row}: y={y}, eta_mu={location_eta}, eta_log_sigma={eta_log_sigma}"
852 ),
853 }
854 .into());
855 }
856 if !obs_weight.is_finite() || obs_weight < 0.0 {
857 return Err(GamlssError::InvalidInput {
858 reason: format!(
859 "Gaussian location-scale requires finite non-negative weights; weight[{row}]={obs_weight}"
860 ),
861 }
862 .into());
863 }
864 if obs_weight == 0.0 {
865 return Ok(GaussianDiagonalRowKernel {
866 log_likelihood: 0.0,
867 location_working_weight: 0.0,
868 log_sigma_working_weight: 0.0,
869 log_sigma_working_response: eta_log_sigma,
870 joint_w: 0.0,
871 joint_m: 0.0,
872 joint_n: 0.0,
873 standardized_residual: 0.0,
874 inv_sigma: 0.0,
875 kappa: 0.0,
876 kappa_prime: 0.0,
877 });
878 }
879
880 let SigmaJet1 { sigma, d1 } = logb_sigma_jet1_scalar(eta_log_sigma);
888 if !sigma.is_finite() || sigma <= 0.0 {
889 return Err(GamlssError::RowGeometryUnrepresentable {
890 row,
891 quantity: "Gaussian scale link",
892 eta: eta_log_sigma,
893 value: sigma,
894 }
895 .into());
896 }
897 let kappa = logb_dlog_sigma_deta(sigma, d1);
898 if !kappa.is_finite() || kappa <= 0.0 || kappa > 1.0 {
899 return Err(GamlssError::RowGeometryUnrepresentable {
900 row,
901 quantity: "Gaussian log-scale link derivative",
902 eta: eta_log_sigma,
903 value: kappa,
904 }
905 .into());
906 }
907 let inv_sigma = sigma.recip();
908 let location_working_weight =
909 scaled_positive_product_quotient(obs_weight, inv_sigma, inv_sigma, 1.0);
910 if !location_working_weight.is_finite() || location_working_weight <= 0.0 {
911 return Err(GamlssError::RowGeometryUnrepresentable {
912 row,
913 quantity: "Gaussian location Fisher information",
914 eta: location_eta,
915 value: location_working_weight,
916 }
917 .into());
918 }
919
920 let residual = y - location_eta;
925 let standardized_residual = if residual.is_finite() {
926 residual / sigma
927 } else {
928 y / sigma - location_eta / sigma
929 };
930 let standardized_residual_sq = standardized_residual * standardized_residual;
931 if !standardized_residual.is_finite() || !standardized_residual_sq.is_finite() {
932 return Err(GamlssError::RowGeometryUnrepresentable {
933 row,
934 quantity: "Gaussian standardized residual squared",
935 eta: location_eta,
936 value: standardized_residual_sq,
937 }
938 .into());
939 }
940 let joint_n = if standardized_residual == 0.0 {
941 0.0
942 } else {
943 scaled_positive_product_quotient(
944 obs_weight,
945 standardized_residual.abs(),
946 standardized_residual.abs(),
947 1.0,
948 )
949 };
950 let joint_m = if standardized_residual == 0.0 {
951 0.0
952 } else {
953 scaled_signed_product3(obs_weight, standardized_residual, inv_sigma)
954 };
955 let log_sigma_working_weight = scaled_positive_product_quotient(obs_weight, kappa, kappa, 0.5);
956 if !log_sigma_working_weight.is_finite() || log_sigma_working_weight <= 0.0 {
957 return Err(GamlssError::RowGeometryUnrepresentable {
958 row,
959 quantity: "Gaussian log-scale Fisher information",
960 eta: eta_log_sigma,
961 value: log_sigma_working_weight,
962 }
963 .into());
964 }
965 let log_sigma_step = (standardized_residual_sq - 1.0) / (2.0 * kappa);
966 let log_sigma_working_response = eta_log_sigma + log_sigma_step;
967 if !log_sigma_working_response.is_finite() {
968 return Err(GamlssError::RowGeometryUnrepresentable {
969 row,
970 quantity: "Gaussian log-scale working response",
971 eta: eta_log_sigma,
972 value: log_sigma_working_response,
973 }
974 .into());
975 }
976 let likelihood_core = standardized_residual_sq + ln2pi + 2.0 * sigma.ln();
977 let log_likelihood = if likelihood_core == 0.0 {
978 0.0
979 } else {
980 -scaled_signed_product3(0.5, obs_weight, likelihood_core)
981 };
982 if !log_likelihood.is_finite() || !joint_m.is_finite() || !joint_n.is_finite() {
983 let (quantity, value) = if !log_likelihood.is_finite() {
984 ("Gaussian row log likelihood", log_likelihood)
985 } else if !joint_m.is_finite() {
986 ("Gaussian location score", joint_m)
987 } else {
988 ("Gaussian squared standardized residual weight", joint_n)
989 };
990 return Err(GamlssError::RowGeometryUnrepresentable {
991 row,
992 quantity,
993 eta: eta_log_sigma,
994 value,
995 }
996 .into());
997 }
998 let kappa_prime = kappa * (1.0 - kappa);
999
1000 Ok(GaussianDiagonalRowKernel {
1001 log_likelihood,
1002 location_working_weight,
1003 log_sigma_working_weight,
1004 log_sigma_working_response,
1005 joint_w: location_working_weight,
1006 joint_m,
1007 joint_n,
1008 standardized_residual,
1009 inv_sigma,
1010 kappa,
1011 kappa_prime,
1012 })
1013}
1014
1015#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1017pub enum ParameterLink {
1018 Identity,
1019 Log,
1020 Logit,
1021 Probit,
1022 InverseLink,
1023 Wiggle,
1025}