1use super::*;
6
7pub struct PoissonLogFamily {
8 pub y: Array1<f64>,
9 pub weights: Array1<f64>,
10}
11
12impl PoissonLogFamily {
13 pub const BLOCK_ETA: usize = 0;
14
15 pub fn parameternames() -> &'static [&'static str] {
16 &["eta"]
17 }
18
19 pub fn parameter_links() -> &'static [ParameterLink] {
20 &[ParameterLink::Log]
21 }
22
23 pub fn metadata() -> FamilyMetadata {
24 FamilyMetadata {
25 name: "poisson_log",
26 parameternames: Self::parameternames(),
27 parameter_links: Self::parameter_links(),
28 }
29 }
30}
31
32pub(crate) struct DiagonalIrlsRow {
36 pub(crate) log_lik_increment: f64,
38 pub(crate) observed_weight: f64,
40 pub(crate) working_response: f64,
42}
43
44trait LogLinkDiagonalIrlsFamily {
49 fn family_label(&self) -> &'static str;
51
52 fn y(&self) -> &Array1<f64>;
54 fn prior_weights(&self) -> &Array1<f64>;
55
56 fn validate_self(&self) -> Result<(), String> {
59 Ok(())
60 }
61
62 fn validate_yi(&self, yi: f64, idx: usize) -> Result<(), String>;
66
67 fn row_kernel(
70 &self,
71 row: usize,
72 yi: f64,
73 eta: f64,
74 prior_w: f64,
75 ) -> Result<DiagonalIrlsRow, String>;
76}
77
78fn evaluate_log_link_diagonal_irls<F: LogLinkDiagonalIrlsFamily + ?Sized>(
83 family: &F,
84 block_states: &[ParameterBlockState],
85) -> Result<FamilyEvaluation, String> {
86 let label = family.family_label();
87 let eta = &expect_single_block(block_states, label)?.eta;
88 let y = family.y();
89 let prior_weights = family.prior_weights();
90 let n = y.len();
91 if eta.len() != n || prior_weights.len() != n {
92 return Err(GamlssError::DimensionMismatch {
93 reason: format!("{label} input size mismatch"),
94 }
95 .into());
96 }
97 family.validate_self()?;
98
99 let mut rows = Vec::with_capacity(n);
100 for i in 0..n {
101 let yi = y[i];
102 family.validate_yi(yi, i)?;
103 let e = eta[i];
104 if !e.is_finite() {
105 return Err(GamlssError::NonFinite {
106 reason: format!("{label} requires finite eta; found eta[{i}]={e}"),
107 }
108 .into());
109 }
110 let prior_w = prior_weights[i];
111 if !prior_w.is_finite() || prior_w < 0.0 {
112 return Err(GamlssError::InvalidInput {
113 reason: format!(
114 "{label} requires finite non-negative prior weights; found weight[{i}]={prior_w}"
115 ),
116 }
117 .into());
118 }
119 rows.push(family.row_kernel(i, yi, e, prior_w)?);
120 }
121
122 let mut ll = 0.0;
123 for (i, row) in rows.iter().enumerate() {
124 ll += row.log_lik_increment;
125 if !ll.is_finite() {
126 return Err(GamlssError::RowGeometryUnrepresentable {
127 row: i,
128 quantity: "cumulative log likelihood",
129 eta: eta[i],
130 value: ll,
131 }
132 .into());
133 }
134 }
135 let z = Array1::from_iter(rows.iter().map(|row| row.working_response));
136 let w = Array1::from_iter(rows.iter().map(|row| row.observed_weight));
137
138 Ok(FamilyEvaluation {
139 log_likelihood: ll,
140 blockworking_sets: vec![BlockWorkingSet::diagonal_checked(z, w)?],
141 })
142}
143
144impl LogLinkDiagonalIrlsFamily for PoissonLogFamily {
145 fn family_label(&self) -> &'static str {
146 "PoissonLogFamily"
147 }
148 fn y(&self) -> &Array1<f64> {
149 &self.y
150 }
151 fn prior_weights(&self) -> &Array1<f64> {
152 &self.weights
153 }
154 fn validate_yi(&self, yi: f64, idx: usize) -> Result<(), String> {
155 if !yi.is_finite() || yi < 0.0 {
156 return Err(GamlssError::InvalidInput {
157 reason: format!(
158 "PoissonLogFamily requires non-negative finite y; found y[{idx}]={yi}"
159 ),
160 }
161 .into());
162 }
163 Ok::<(), _>(())
164 }
165 #[inline]
166 fn row_kernel(
167 &self,
168 row: usize,
169 yi: f64,
170 eta: f64,
171 prior_w: f64,
172 ) -> Result<DiagonalIrlsRow, String> {
173 if prior_w == 0.0 {
174 return Ok(DiagonalIrlsRow {
175 log_lik_increment: 0.0,
176 observed_weight: 0.0,
177 working_response: eta,
178 });
179 }
180 let m = eta.exp();
181 if !m.is_finite() || m <= 0.0 {
182 return Err(row_geometry_error(row, "Poisson mean exp(eta)", eta, m));
183 }
184 let observed_weight = scaled_positive_product_quotient(prior_w, 1.0, m, 1.0);
185 if !observed_weight.is_finite() || observed_weight <= 0.0 {
186 return Err(row_geometry_error(
187 row,
188 "Poisson observed information",
189 eta,
190 observed_weight,
191 ));
192 }
193 let weighted_y_eta = if yi == 0.0 || eta == 0.0 {
197 0.0
198 } else {
199 scaled_positive_product_quotient(prior_w, yi, eta.abs(), 1.0).copysign(eta)
200 };
201 let log_lik_increment = weighted_y_eta - observed_weight;
202 if !log_lik_increment.is_finite() {
203 return Err(row_geometry_error(
204 row,
205 "Poisson log-likelihood contribution",
206 eta,
207 log_lik_increment,
208 ));
209 }
210 let working_response = eta + (yi / m - 1.0);
211 if !working_response.is_finite() {
212 return Err(row_geometry_error(
213 row,
214 "Poisson working response",
215 eta,
216 working_response,
217 ));
218 }
219 Ok(DiagonalIrlsRow {
220 log_lik_increment,
221 observed_weight,
222 working_response,
223 })
224 }
225}
226
227impl CustomFamily for PoissonLogFamily {
228 fn joint_jeffreys_term_required(&self) -> bool {
232 true
233 }
234
235 fn evaluate(&self, block_states: &[ParameterBlockState]) -> Result<FamilyEvaluation, String> {
236 evaluate_log_link_diagonal_irls(self, block_states)
237 }
238
239 fn exact_newton_joint_gradient_evaluation(
240 &self,
241 block_states: &[ParameterBlockState],
242 specs: &[ParameterBlockSpec],
243 ) -> Result<Option<ExactNewtonJointGradientEvaluation>, String> {
244 let eval = self.evaluate(block_states)?;
249 gamlss_joint_gradient_from_working_sets(&eval, specs, block_states).map(Some)
250 }
251}
252
253impl CustomFamilyGenerative for PoissonLogFamily {
254 fn generativespec(
255 &self,
256 block_states: &[ParameterBlockState],
257 ) -> Result<GenerativeSpec, String> {
258 let eta = &expect_single_block(block_states, "PoissonLogFamily")?.eta;
259 let mean = gamlss_rowwise_map(eta.len(), |i| eta[i].exp());
264 Ok(GenerativeSpec {
265 mean,
266 noise: NoiseModel::Poisson,
267 })
268 }
269}
270
271#[derive(Clone)]
273pub struct GammaLogFamily {
274 pub y: Array1<f64>,
275 pub weights: Array1<f64>,
276 pub shape: f64,
277}
278
279impl GammaLogFamily {
280 pub const BLOCK_ETA: usize = 0;
281
282 pub fn parameternames() -> &'static [&'static str] {
283 &["eta"]
284 }
285
286 pub fn parameter_links() -> &'static [ParameterLink] {
287 &[ParameterLink::Log]
288 }
289
290 pub fn metadata() -> FamilyMetadata {
291 FamilyMetadata {
292 name: "gamma_log",
293 parameternames: Self::parameternames(),
294 parameter_links: Self::parameter_links(),
295 }
296 }
297}
298
299impl LogLinkDiagonalIrlsFamily for GammaLogFamily {
300 fn family_label(&self) -> &'static str {
301 "GammaLogFamily"
302 }
303 fn y(&self) -> &Array1<f64> {
304 &self.y
305 }
306 fn prior_weights(&self) -> &Array1<f64> {
307 &self.weights
308 }
309 fn validate_self(&self) -> Result<(), String> {
310 if !self.shape.is_finite() || self.shape <= 0.0 {
311 return Err(GamlssError::NonFinite {
312 reason: "GammaLogFamily shape must be finite and > 0".to_string(),
313 }
314 .into());
315 }
316 Ok(())
317 }
318 fn validate_yi(&self, yi: f64, idx: usize) -> Result<(), String> {
319 if !yi.is_finite() || yi <= 0.0 {
320 return Err(GamlssError::InvalidInput {
321 reason: format!("GammaLogFamily requires positive finite y; found y[{idx}]={yi}"),
322 }
323 .into());
324 }
325 Ok::<(), _>(())
326 }
327 #[inline]
328 fn row_kernel(
329 &self,
330 row: usize,
331 yi: f64,
332 eta: f64,
333 prior_w: f64,
334 ) -> Result<DiagonalIrlsRow, String> {
335 if prior_w == 0.0 {
336 return Ok(DiagonalIrlsRow {
337 log_lik_increment: 0.0,
338 observed_weight: 0.0,
339 working_response: eta,
340 });
341 }
342 let m = eta.exp();
343 if !m.is_finite() || m <= 0.0 {
344 return Err(row_geometry_error(row, "Gamma mean exp(eta)", eta, m));
345 }
346 let observed_weight = scaled_positive_product_quotient(prior_w, self.shape, yi, m);
352 if !observed_weight.is_finite() || observed_weight <= 0.0 {
353 return Err(row_geometry_error(
354 row,
355 "Gamma observed information",
356 eta,
357 observed_weight,
358 ));
359 }
360 let eta_term = if eta == 0.0 {
361 0.0
362 } else {
363 scaled_positive_product_quotient(prior_w, self.shape, eta.abs(), 1.0).copysign(eta)
364 };
365 let log_lik_increment = -observed_weight - eta_term;
366 if !log_lik_increment.is_finite() {
367 return Err(row_geometry_error(
368 row,
369 "Gamma log-likelihood contribution",
370 eta,
371 log_lik_increment,
372 ));
373 }
374 let working_step = if yi >= m {
382 1.0 - m / yi
383 } else {
384 let ratio = yi / m;
385 (ratio - 1.0) / ratio
386 };
387 let working_response = eta + working_step;
388 if !working_response.is_finite() {
389 return Err(row_geometry_error(
390 row,
391 "Gamma working response",
392 eta,
393 working_response,
394 ));
395 }
396 Ok(DiagonalIrlsRow {
397 log_lik_increment,
398 observed_weight,
399 working_response,
400 })
401 }
402}
403
404impl CustomFamily for GammaLogFamily {
405 fn joint_jeffreys_term_required(&self) -> bool {
409 true
410 }
411
412 fn evaluate(&self, block_states: &[ParameterBlockState]) -> Result<FamilyEvaluation, String> {
413 evaluate_log_link_diagonal_irls(self, block_states)
414 }
415
416 fn exact_newton_joint_gradient_evaluation(
417 &self,
418 block_states: &[ParameterBlockState],
419 specs: &[ParameterBlockSpec],
420 ) -> Result<Option<ExactNewtonJointGradientEvaluation>, String> {
421 let eval = self.evaluate(block_states)?;
426 gamlss_joint_gradient_from_working_sets(&eval, specs, block_states).map(Some)
427 }
428
429 fn diagonalworking_weights_directional_derivative(
430 &self,
431 block_states: &[ParameterBlockState],
432 block_idx: usize,
433 d_eta: &Array1<f64>,
434 ) -> Result<Option<Array1<f64>>, String> {
435 if block_idx != Self::BLOCK_ETA {
436 return Ok(None);
437 }
438 let eta = &expect_single_block(block_states, "GammaLogFamily")?.eta;
439 let n = self.y.len();
440 if eta.len() != n || self.weights.len() != n || d_eta.len() != n {
441 return Err(GamlssError::DimensionMismatch {
442 reason: "GammaLogFamily input size mismatch".to_string(),
443 }
444 .into());
445 }
446 if !self.shape.is_finite() || self.shape <= 0.0 {
447 return Err(GamlssError::NonFinite {
448 reason: "GammaLogFamily shape must be finite and > 0".to_string(),
449 }
450 .into());
451 }
452
453 let mut values = Vec::with_capacity(n);
454 for i in 0..n {
455 let yi = self.y[i];
456 if !yi.is_finite() || yi <= 0.0 {
457 return Err(GamlssError::InvalidInput {
458 reason: format!("GammaLogFamily requires positive finite y; found y[{i}]={yi}"),
459 }
460 .into());
461 }
462 let e = eta[i];
463 if !e.is_finite() || !d_eta[i].is_finite() {
464 return Err(GamlssError::NonFinite {
465 reason: format!(
466 "GammaLogFamily directional geometry requires finite eta and direction at row {i}"
467 ),
468 }
469 .into());
470 }
471 let prior_w = self.weights[i];
472 if !prior_w.is_finite() || prior_w < 0.0 {
473 return Err(GamlssError::InvalidInput {
474 reason: format!(
475 "GammaLogFamily requires finite non-negative prior weights; found weight[{i}]={prior_w}"
476 ),
477 }
478 .into());
479 }
480 if prior_w == 0.0 {
481 values.push(0.0);
482 continue;
483 }
484 let row = self.row_kernel(i, yi, e, prior_w)?;
485 let observed_weight = row.observed_weight;
486 let derivative = -observed_weight * d_eta[i];
488 if !derivative.is_finite() {
489 return Err(row_geometry_error(
490 i,
491 "Gamma observed-information directional derivative",
492 e,
493 derivative,
494 ));
495 }
496 values.push(derivative);
497 }
498 Ok(Some(Array1::from_vec(values)))
499 }
500}
501
502impl CustomFamilyGenerative for GammaLogFamily {
503 fn generativespec(
504 &self,
505 block_states: &[ParameterBlockState],
506 ) -> Result<GenerativeSpec, String> {
507 let eta = &expect_single_block(block_states, "GammaLogFamily")?.eta;
508 let mean = gamlss_rowwise_map(eta.len(), |i| eta[i].exp());
509 let shape = ndarray::Array1::from_elem(mean.len(), self.shape);
510 Ok(GenerativeSpec {
511 mean,
512 noise: NoiseModel::Gamma { shape },
513 })
514 }
515}
516
517#[inline]
518fn row_geometry_error(row: usize, quantity: &'static str, eta: f64, value: f64) -> String {
519 GamlssError::RowGeometryUnrepresentable {
520 row,
521 quantity,
522 eta,
523 value,
524 }
525 .into()
526}
527
528#[inline]
531fn positive_frexp(x: f64) -> (f64, i32) {
532 assert!(x.is_finite() && x > 0.0);
533 let bits = x.to_bits();
534 let raw_exp = ((bits >> 52) & 0x7ff) as i32;
535 let fraction = bits & ((1_u64 << 52) - 1);
536 if raw_exp != 0 {
537 let mantissa = f64::from_bits((1023_u64 << 52) | fraction);
538 (mantissa, raw_exp - 1023)
539 } else {
540 let leading = 63_i32 - fraction.leading_zeros() as i32;
541 let shift = 52_i32 - leading;
542 let normalized = fraction << shift;
543 let mantissa = f64::from_bits((1023_u64 << 52) | (normalized & ((1_u64 << 52) - 1)));
544 (mantissa, -1022 - shift)
545 }
546}
547
548#[inline]
549fn scale_normalized_power_of_two(mut mantissa: f64, mut exponent: i32) -> f64 {
550 while mantissa >= 2.0 {
551 mantissa *= 0.5;
552 exponent += 1;
553 }
554 while mantissa < 1.0 {
555 mantissa *= 2.0;
556 exponent -= 1;
557 }
558 if exponent > 1023 {
559 return f64::INFINITY;
560 }
561 if exponent >= -1022 {
562 let power = f64::from_bits(((exponent + 1023) as u64) << 52);
563 return mantissa * power;
564 }
565 if exponent < -1075 {
566 return 0.0;
567 }
568 let units = mantissa * 2.0_f64.powi(exponent + 1074);
572 units * f64::from_bits(1)
573}
574
575#[inline]
579fn scaled_positive_product_quotient(a: f64, b: f64, c: f64, d: f64) -> f64 {
580 assert!(a.is_finite() && a > 0.0);
581 assert!(b.is_finite() && b > 0.0);
582 assert!(c.is_finite() && c > 0.0);
583 assert!(d.is_finite() && d > 0.0);
584 let (ma, ea) = positive_frexp(a);
585 let (mb, eb) = positive_frexp(b);
586 let (mc, ec) = positive_frexp(c);
587 let (md, ed) = positive_frexp(d);
588 scale_normalized_power_of_two((ma * mb) * (mc / md), ea + eb + ec - ed)
589}
590
591#[cfg(test)]
592mod exact_domain_tests {
593 use super::*;
594
595 #[test]
596 fn scaled_product_quotient_avoids_false_intermediate_overflow_and_underflow() {
597 let got = scaled_positive_product_quotient(1.0e-300, 1.0, 1.0e308, 1.0);
598 assert!((got - 1.0e8).abs() <= 4.0 * f64::EPSILON * 1.0e8);
599 let got = scaled_positive_product_quotient(1.0e-300, 1.0e-200, 1.0, 1.0e-300);
600 assert!((got - 1.0e-200).abs() <= 4.0 * f64::EPSILON * 1.0e-200);
601 }
602
603 #[test]
604 fn gamma_row_accepts_overflowing_raw_ratio_when_final_geometry_is_finite() {
605 let family = GammaLogFamily {
606 y: Array1::from_vec(vec![1.0e308]),
607 weights: Array1::from_vec(vec![1.0e-300]),
608 shape: 1.0,
609 };
610 let row = family
611 .row_kernel(0, 1.0e308, 0.0, 1.0e-300)
612 .expect("final Gamma geometry is representable");
613 assert!(row.log_lik_increment.is_finite());
614 assert!((row.observed_weight - 1.0e8).abs() <= 4.0 * f64::EPSILON * 1.0e8);
615 assert_eq!(row.working_response, 1.0);
616 }
617
618 #[test]
619 fn poisson_row_scales_y_eta_before_multiplication() {
620 let family = PoissonLogFamily {
621 y: Array1::from_vec(vec![1.0e308]),
622 weights: Array1::from_vec(vec![1.0e-300]),
623 };
624 let row = family
625 .row_kernel(0, 1.0e308, 2.0, 1.0e-300)
626 .expect("weighted Poisson objective is representable");
627 assert!(row.log_lik_increment.is_finite());
628 assert!(row.observed_weight.is_normal());
629 }
630}