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 ).map_err(|error| error.to_string())?;
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 ).map_err(|error| error.to_string())?;
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 ).map_err(|error| error.to_string())? {
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 = gam_runtime::resource::LIBRARY_ROW_CHUNK_TARGET_BYTES;
661 const MIN_ROWS: usize = 512;
662 const MAX_ROWS: usize = 131_072;
663 let rows = (TARGET_BYTES / (p.max(1) * 8))
664 .clamp(MIN_ROWS, MAX_ROWS)
665 .min(n.max(1));
666 (0..n)
667 .step_by(rows)
668 .map(move |start| start..(start + rows).min(n))
669}
670
671pub(crate) fn design_weighted_column_squares(
672 design: &DesignMatrix,
673 weights: &Array1<f64>,
674) -> Result<Array1<f64>, String> {
675 let n = design.nrows();
676 let p = design.ncols();
677 if weights.len() != n {
678 return Err(GamlssError::DimensionMismatch {
679 reason: format!(
680 "design weighted column squares dimension mismatch: weights={}, rows={}",
681 weights.len(),
682 n
683 ),
684 }
685 .into());
686 }
687 let mut out = Array1::<f64>::zeros(p);
688 for rows in exact_design_row_chunks(n, p) {
689 let chunk = design.try_row_chunk(rows.clone()).map_err(|e| {
690 format!("design weighted column squares row chunk materialization failed: {e}")
691 })?;
692 for (local_i, row) in chunk.outer_iter().enumerate() {
693 let w = weights[rows.start + local_i];
694 if w == 0.0 {
695 continue;
696 }
697 for j in 0..p {
698 let x = row[j];
699 out[j] += w * x * x;
700 }
701 }
702 }
703 Ok(out)
704}
705
706#[inline]
707pub(crate) fn logb_dlog_sigma_deta(sigma: f64, d_sigma_deta: f64) -> f64 {
708 d_sigma_deta / sigma
709}
710
711#[inline]
714pub(crate) fn positive_frexp(x: f64) -> (f64, i32) {
715 assert!(x.is_finite() && x > 0.0);
716 let bits = x.to_bits();
717 let raw_exp = ((bits >> 52) & 0x7ff) as i32;
718 let fraction = bits & ((1_u64 << 52) - 1);
719 if raw_exp != 0 {
720 let mantissa = f64::from_bits((1023_u64 << 52) | fraction);
721 (mantissa, raw_exp - 1023)
722 } else {
723 let leading = 63_i32 - fraction.leading_zeros() as i32;
724 let shift = 52_i32 - leading;
725 let normalized = fraction << shift;
726 let mantissa = f64::from_bits((1023_u64 << 52) | (normalized & ((1_u64 << 52) - 1)));
727 (mantissa, -1022 - shift)
728 }
729}
730
731#[inline]
732pub(crate) fn scale_normalized_power_of_two(mut mantissa: f64, mut exponent: i32) -> f64 {
733 while mantissa >= 2.0 {
734 mantissa *= 0.5;
735 exponent += 1;
736 }
737 while mantissa < 1.0 {
738 mantissa *= 2.0;
739 exponent -= 1;
740 }
741 if exponent > 1023 {
742 return f64::INFINITY;
743 }
744 if exponent >= -1022 {
745 let power = f64::from_bits(((exponent + 1023) as u64) << 52);
746 return mantissa * power;
747 }
748 if exponent < -1075 {
749 return 0.0;
750 }
751 let units = mantissa * 2.0_f64.powi(exponent + 1074);
752 units * f64::from_bits(1)
753}
754
755#[inline]
759pub(crate) fn scaled_positive_product_quotient(a: f64, b: f64, c: f64, d: f64) -> f64 {
760 assert!(a.is_finite() && a > 0.0);
761 assert!(b.is_finite() && b > 0.0);
762 assert!(c.is_finite() && c > 0.0);
763 assert!(d.is_finite() && d > 0.0);
764 let (ma, ea) = positive_frexp(a);
765 let (mb, eb) = positive_frexp(b);
766 let (mc, ec) = positive_frexp(c);
767 let (md, ed) = positive_frexp(d);
768 scale_normalized_power_of_two((ma * mb) * (mc / md), ea + eb + ec - ed)
769}
770
771#[inline]
772pub(crate) fn scaled_signed_product3(a: f64, b: f64, c: f64) -> f64 {
773 if a == 0.0 || b == 0.0 || c == 0.0 {
774 return 0.0;
775 }
776 let sign = a.signum() * b.signum() * c.signum();
777 sign * scaled_positive_product_quotient(a.abs(), b.abs(), c.abs(), 1.0)
778}
779
780#[inline]
781pub(crate) fn gaussian_log_sigma_irlsinfo_directional_derivative(
782 row: usize,
783 eta: f64,
784 weight: f64,
785 sigma: f64,
786 d_sigma_deta: f64,
787 d_eta: f64,
788) -> Result<f64, String> {
789 if weight == 0.0 || d_eta == 0.0 {
790 return Ok(0.0);
791 }
792 let g = logb_dlog_sigma_deta(sigma, d_sigma_deta);
793 if !g.is_finite() || g <= 0.0 || g > 1.0 {
794 return Err(GamlssError::RowGeometryUnrepresentable {
795 row,
796 quantity: "Gaussian log-scale link derivative",
797 eta,
798 value: g,
799 }
800 .into());
801 }
802 let info = scaled_positive_product_quotient(weight, g, g, 0.5);
803 if !info.is_finite() || info <= 0.0 {
804 return Err(GamlssError::RowGeometryUnrepresentable {
805 row,
806 quantity: "Gaussian log-scale Fisher information",
807 eta,
808 value: info,
809 }
810 .into());
811 }
812 let dw = scaled_signed_product3(info, 2.0 * (1.0 - g), d_eta);
813 if !dw.is_finite() {
814 return Err(GamlssError::RowGeometryUnrepresentable {
815 row,
816 quantity: "Gaussian log-scale Fisher-information directional derivative",
817 eta,
818 value: dw,
819 }
820 .into());
821 }
822 Ok(dw)
823}
824
825#[derive(Clone, Copy)]
826pub(crate) struct GaussianDiagonalRowKernel {
827 pub(crate) log_likelihood: f64,
828 pub(crate) location_working_weight: f64,
829 pub(crate) location_working_response: f64,
836 pub(crate) log_sigma_working_weight: f64,
837 pub(crate) log_sigma_working_response: f64,
838 pub(crate) joint_w: f64,
839 pub(crate) joint_m: f64,
840 pub(crate) joint_n: f64,
841 pub(crate) standardized_residual: f64,
842 pub(crate) inv_sigma: f64,
843 pub(crate) kappa: f64,
844 pub(crate) kappa_prime: f64,
845}
846
847#[inline]
848pub(crate) fn gaussian_diagonal_row_kernel(
849 row: usize,
850 y: f64,
851 location_eta: f64,
852 eta_log_sigma: f64,
853 obs_weight: f64,
854 ln2pi: f64,
855) -> Result<GaussianDiagonalRowKernel, String> {
856 if !y.is_finite() || !location_eta.is_finite() || !eta_log_sigma.is_finite() {
857 return Err(GamlssError::NonFinite {
858 reason: format!(
859 "Gaussian location-scale requires finite row inputs at row {row}: y={y}, eta_mu={location_eta}, eta_log_sigma={eta_log_sigma}"
860 ),
861 }
862 .into());
863 }
864 if !obs_weight.is_finite() || obs_weight < 0.0 {
865 return Err(GamlssError::InvalidInput {
866 reason: format!(
867 "Gaussian location-scale requires finite non-negative weights; weight[{row}]={obs_weight}"
868 ),
869 }
870 .into());
871 }
872 if obs_weight == 0.0 {
873 return Ok(GaussianDiagonalRowKernel {
874 log_likelihood: 0.0,
875 location_working_weight: 0.0,
876 location_working_response: location_eta,
877 log_sigma_working_weight: 0.0,
878 log_sigma_working_response: eta_log_sigma,
879 joint_w: 0.0,
880 joint_m: 0.0,
881 joint_n: 0.0,
882 standardized_residual: 0.0,
883 inv_sigma: 0.0,
884 kappa: 0.0,
885 kappa_prime: 0.0,
886 });
887 }
888
889 let SigmaJet1 { sigma, d1 } = logb_sigma_jet1_scalar(eta_log_sigma);
897 if !sigma.is_finite() || sigma <= 0.0 {
898 return Err(GamlssError::RowGeometryUnrepresentable {
899 row,
900 quantity: "Gaussian scale link",
901 eta: eta_log_sigma,
902 value: sigma,
903 }
904 .into());
905 }
906 let kappa = logb_dlog_sigma_deta(sigma, d1);
907 if !kappa.is_finite() || kappa <= 0.0 || kappa > 1.0 {
908 return Err(GamlssError::RowGeometryUnrepresentable {
909 row,
910 quantity: "Gaussian log-scale link derivative",
911 eta: eta_log_sigma,
912 value: kappa,
913 }
914 .into());
915 }
916 let inv_sigma = sigma.recip();
917 let location_working_weight =
918 scaled_positive_product_quotient(obs_weight, inv_sigma, inv_sigma, 1.0);
919 if !location_working_weight.is_finite() || location_working_weight <= 0.0 {
920 return Err(GamlssError::RowGeometryUnrepresentable {
921 row,
922 quantity: "Gaussian location Fisher information",
923 eta: location_eta,
924 value: location_working_weight,
925 }
926 .into());
927 }
928
929 let residual = y - location_eta;
934 let standardized_residual = if residual.is_finite() {
935 residual / sigma
936 } else {
937 y / sigma - location_eta / sigma
938 };
939 let standardized_residual_sq = standardized_residual * standardized_residual;
940 if !standardized_residual.is_finite() || !standardized_residual_sq.is_finite() {
941 return Err(GamlssError::RowGeometryUnrepresentable {
942 row,
943 quantity: "Gaussian standardized residual squared",
944 eta: location_eta,
945 value: standardized_residual_sq,
946 }
947 .into());
948 }
949 let joint_n = if standardized_residual == 0.0 {
950 0.0
951 } else {
952 scaled_positive_product_quotient(
953 obs_weight,
954 standardized_residual.abs(),
955 standardized_residual.abs(),
956 1.0,
957 )
958 };
959 let joint_m = if standardized_residual == 0.0 {
960 0.0
961 } else {
962 scaled_signed_product3(obs_weight, standardized_residual, inv_sigma)
963 };
964 let log_sigma_working_weight = scaled_positive_product_quotient(obs_weight, kappa, kappa, 0.5);
965 if !log_sigma_working_weight.is_finite() || log_sigma_working_weight <= 0.0 {
966 return Err(GamlssError::RowGeometryUnrepresentable {
967 row,
968 quantity: "Gaussian log-scale Fisher information",
969 eta: eta_log_sigma,
970 value: log_sigma_working_weight,
971 }
972 .into());
973 }
974 let log_sigma_step = (standardized_residual_sq - 1.0) / (2.0 * kappa);
975 let log_sigma_working_response = eta_log_sigma + log_sigma_step;
976 if !log_sigma_working_response.is_finite() {
977 return Err(GamlssError::RowGeometryUnrepresentable {
978 row,
979 quantity: "Gaussian log-scale working response",
980 eta: eta_log_sigma,
981 value: log_sigma_working_response,
982 }
983 .into());
984 }
985 let likelihood_core = standardized_residual_sq + ln2pi + 2.0 * sigma.ln();
986 let log_likelihood = if likelihood_core == 0.0 {
987 0.0
988 } else {
989 -scaled_signed_product3(0.5, obs_weight, likelihood_core)
990 };
991 if !log_likelihood.is_finite() || !joint_m.is_finite() || !joint_n.is_finite() {
992 let (quantity, value) = if !log_likelihood.is_finite() {
993 ("Gaussian row log likelihood", log_likelihood)
994 } else if !joint_m.is_finite() {
995 ("Gaussian location score", joint_m)
996 } else {
997 ("Gaussian squared standardized residual weight", joint_n)
998 };
999 return Err(GamlssError::RowGeometryUnrepresentable {
1000 row,
1001 quantity,
1002 eta: eta_log_sigma,
1003 value,
1004 }
1005 .into());
1006 }
1007 let kappa_prime = kappa * (1.0 - kappa);
1008
1009 Ok(GaussianDiagonalRowKernel {
1010 log_likelihood,
1011 location_working_weight,
1012 location_working_response: y,
1014 log_sigma_working_weight,
1015 log_sigma_working_response,
1016 joint_w: location_working_weight,
1017 joint_m,
1018 joint_n,
1019 standardized_residual,
1020 inv_sigma,
1021 kappa,
1022 kappa_prime,
1023 })
1024}
1025
1026#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1028pub enum ParameterLink {
1029 Identity,
1030 Log,
1031 Logit,
1032 Probit,
1033 InverseLink,
1034 Wiggle,
1036}