1use super::*;
6
7pub struct VarianceJet {
8 pub v: f64,
9 pub v1: f64,
10 pub v2: f64,
11 pub v3: f64,
12 pub v4: f64,
13}
14
15impl VarianceJet {
16 #[inline]
18 pub fn bernoulli(mu: f64) -> Self {
19 Self {
20 v: mu * (1.0 - mu),
21 v1: 1.0 - 2.0 * mu,
22 v2: -2.0,
23 v3: 0.0,
24 v4: 0.0,
25 }
26 }
27
28 #[inline]
30 pub fn poisson(mu: f64) -> Self {
31 Self {
32 v: mu,
33 v1: 1.0,
34 v2: 0.0,
35 v3: 0.0,
36 v4: 0.0,
37 }
38 }
39
40 #[inline]
42 pub fn gamma(mu: f64) -> Self {
43 Self {
44 v: mu * mu,
45 v1: 2.0 * mu,
46 v2: 2.0,
47 v3: 0.0,
48 v4: 0.0,
49 }
50 }
51
52 #[inline]
54 pub fn tweedie(mu: f64, p: f64) -> Self {
55 Self {
56 v: mu.powf(p),
57 v1: p * mu.powf(p - 1.0),
58 v2: p * (p - 1.0) * mu.powf(p - 2.0),
59 v3: p * (p - 1.0) * (p - 2.0) * mu.powf(p - 3.0),
60 v4: p * (p - 1.0) * (p - 2.0) * (p - 3.0) * mu.powf(p - 4.0),
61 }
62 }
63
64 #[inline]
66 pub fn negative_binomial(mu: f64, theta: f64) -> Self {
67 let inv_theta = if valid_negbin_theta(theta) {
68 1.0 / theta
69 } else {
70 f64::NAN
71 };
72 Self {
73 v: mu + mu * mu * inv_theta,
74 v1: 1.0 + 2.0 * mu * inv_theta,
75 v2: 2.0 * inv_theta,
76 v3: 0.0,
77 v4: 0.0,
78 }
79 }
80
81 #[inline]
83 pub fn gaussian() -> Self {
84 Self {
85 v: 1.0,
86 v1: 0.0,
87 v2: 0.0,
88 v3: 0.0,
89 v4: 0.0,
90 }
91 }
92
93 #[inline]
98 pub fn binomial_n(mu: f64) -> Self {
99 Self::bernoulli(mu)
101 }
102
103 #[inline]
105 pub fn beta(mu: f64, phi: f64) -> Self {
106 let scale = 1.0 / (1.0 + phi);
107 let base = Self::bernoulli(mu);
108 Self {
109 v: base.v * scale,
110 v1: base.v1 * scale,
111 v2: base.v2 * scale,
112 v3: 0.0,
113 v4: 0.0,
114 }
115 }
116}
117
118pub fn exact_hessian_surface_arrays(
122 hessian_weights: gam_linalg::matrix::SignedWeightsView<'_>,
123 c_array: &Array1<f64>,
124 d_array: &Array1<f64>,
125 eta: &Array1<f64>,
126) -> Result<(Array1<f64>, Array1<f64>, Array1<f64>), EstimationError> {
127 let hessian_view = hessian_weights.view();
128 let n = hessian_view.len();
129 if c_array.len() != n || d_array.len() != n || eta.len() != n {
130 crate::bail_invalid_estim!(
131 "exact Hessian surface length mismatch: W={}, c={}, d={}, eta={}",
132 n,
133 c_array.len(),
134 d_array.len(),
135 eta.len()
136 );
137 }
138 for i in 0..n {
139 for (quantity, value) in [
140 ("observed Hessian weight", hessian_view[i]),
141 ("observed Hessian dW/deta", c_array[i]),
142 ("observed Hessian d2W/deta2", d_array[i]),
143 ] {
144 if !value.is_finite() {
145 return Err(EstimationError::PirlsRowGeometryUnrepresentable {
146 row: i,
147 quantity,
148 eta: eta[i],
149 value,
150 });
151 }
152 }
153 }
154 Ok((
155 hessian_view.to_owned(),
156 c_array.to_owned(),
157 d_array.to_owned(),
158 ))
159}
160
161#[inline]
162pub(crate) fn fixed_glm_dispersion(
163 likelihood: &GlmLikelihoodSpec,
164) -> Result<f64, EstimationError> {
165 use gam_problem::ResolvedLikelihoodScale as Scale;
166
167 let scale = likelihood
168 .resolved_scale()
169 .map_err(|error| EstimationError::InvalidInput(error.to_string()))?;
170 match scale {
171 Scale::ProfiledGaussian | Scale::Unit | Scale::NegativeBinomial { .. } => Ok(1.0),
173 Scale::FixedGaussian { phi } | Scale::Tweedie { phi, .. } => Ok(phi.value()),
174 Scale::Gamma { .. } => scale
175 .gamma_phi()
176 .map_err(|error| EstimationError::InvalidInput(error.to_string())),
177 Scale::BetaPrecision { .. } => Ok(1.0),
180 Scale::Unspecified => Err(EstimationError::InvalidInput(
181 "family has no fixed GLM dispersion".to_string(),
182 )),
183 }
184}
185
186#[inline]
213pub(crate) fn penalized_objective_deviance_scale(
214 likelihood: &GlmLikelihoodSpec,
215) -> Result<f64, EstimationError> {
216 let resolved = likelihood
217 .resolved_scale()
218 .map_err(|error| EstimationError::InvalidInput(error.to_string()))?;
219 let k = match likelihood.spec.response {
220 ResponseFamily::Gamma => resolved
221 .gamma_shape()
222 .map_err(|error| EstimationError::InvalidInput(error.to_string()))?,
223 ResponseFamily::Tweedie { .. } => {
224 let phi = resolved
225 .tweedie_phi()
226 .map_err(|error| EstimationError::InvalidInput(error.to_string()))?;
227 1.0 / phi
228 }
229 ResponseFamily::Gaussian => match resolved {
230 gam_problem::ResolvedLikelihoodScale::ProfiledGaussian => 1.0,
231 gam_problem::ResolvedLikelihoodScale::FixedGaussian { phi } => 1.0 / phi.value(),
232 _ => {
233 return Err(EstimationError::InvalidInput(
234 "resolved Gaussian scale has the wrong family variant".to_string(),
235 ));
236 }
237 },
238 _ => 1.0,
239 };
240 if k.is_finite() && k > 0.0 {
241 Ok(k)
242 } else {
243 Err(EstimationError::InvalidInput(format!(
244 "penalized objective deviance scale is not representable: {k:?}"
245 )))
246 }
247}
248
249#[inline]
250pub fn weight_family_for_glm_likelihood(
251 likelihood: &GlmLikelihoodSpec,
252) -> Result<WeightFamily, EstimationError> {
253 let resolved = likelihood
254 .resolved_scale()
255 .map_err(|error| EstimationError::InvalidInput(error.to_string()))?;
256 match &likelihood.spec.response {
257 ResponseFamily::Gaussian => Ok(WeightFamily::Gaussian),
258 ResponseFamily::Poisson => Ok(WeightFamily::Poisson),
259 ResponseFamily::Tweedie { p } => Ok(WeightFamily::Tweedie { p: *p }),
260 ResponseFamily::NegativeBinomial { .. } => Ok(WeightFamily::NegativeBinomial {
261 theta: resolved
262 .negative_binomial_theta()
263 .map_err(|error| EstimationError::InvalidInput(error.to_string()))?,
264 }),
265 ResponseFamily::Beta { .. } => Ok(WeightFamily::Beta {
266 phi: resolved
267 .beta_precision()
268 .map_err(|error| EstimationError::InvalidInput(error.to_string()))?,
269 }),
270 ResponseFamily::Gamma => Ok(WeightFamily::Gamma),
271 ResponseFamily::Binomial => Ok(WeightFamily::Binomial),
272 ResponseFamily::RoystonParmar => Err(EstimationError::InvalidInput(
273 "Royston-Parmar is not a GLM weight family".to_string(),
274 )),
275 }
276}
277
278#[inline]
279pub(crate) fn weight_link_for_inverse_link(inverse_link: &InverseLink) -> WeightLink {
280 match inverse_link {
281 InverseLink::Standard(StandardLink::Identity) => WeightLink::Identity,
282 InverseLink::Standard(StandardLink::Log) => WeightLink::Log,
283 InverseLink::Standard(StandardLink::Logit) => WeightLink::Logit,
284 InverseLink::Standard(StandardLink::Probit)
285 | InverseLink::Standard(StandardLink::CLogLog)
286 | InverseLink::Standard(StandardLink::LogLog)
287 | InverseLink::Standard(StandardLink::Cauchit)
288 | InverseLink::LatentCLogLog(_)
289 | InverseLink::Sas(_)
290 | InverseLink::BetaLogistic(_)
291 | InverseLink::Mixture(_) => WeightLink::Other,
292 }
293}
294
295#[inline]
296pub(crate) fn supports_observed_hessian_curvature_for_likelihood(
297 likelihood: &GlmLikelihoodSpec,
298 inverse_link: &InverseLink,
299) -> bool {
300 let spec = &likelihood.spec;
301 if matches!(spec.response, ResponseFamily::NegativeBinomial { .. }) {
302 return matches!(inverse_link, InverseLink::Standard(StandardLink::Log));
303 }
304 if matches!(spec.response, ResponseFamily::Gamma) {
305 return true;
306 }
307 if !matches!(spec.response, ResponseFamily::Binomial) {
308 return false;
309 }
310 matches!(
311 spec.link,
312 InverseLink::Standard(StandardLink::Probit)
313 | InverseLink::Standard(StandardLink::CLogLog)
314 | InverseLink::Standard(StandardLink::LogLog)
315 | InverseLink::Standard(StandardLink::Cauchit)
316 | InverseLink::Sas(_)
317 | InverseLink::BetaLogistic(_)
318 | InverseLink::Mixture(_)
319 )
320}
321
322pub(crate) fn compute_observed_hessian_curvature_arrays_into(
340 likelihood: &GlmLikelihoodSpec,
341 inverse_link: &InverseLink,
342 eta: &Array1<f64>,
343 y: ArrayView1<'_, f64>,
344 fisher_weights: &Array1<f64>,
345 priorweights: ArrayView1<'_, f64>,
346 hessian_weights: &mut Array1<f64>,
347 hessian_c: &mut Array1<f64>,
348 hessian_d: &mut Array1<f64>,
349) -> Result<(), EstimationError> {
350 assert!(supports_observed_hessian_curvature_for_likelihood(
351 likelihood,
352 inverse_link
353 ));
354 let n = eta.len();
355 if hessian_weights.len() != n {
356 *hessian_weights = Array1::<f64>::zeros(n);
357 }
358 if hessian_c.len() != n {
359 *hessian_c = Array1::<f64>::zeros(n);
360 }
361 if hessian_d.len() != n {
362 *hessian_d = Array1::<f64>::zeros(n);
363 }
364
365 let weight_family = weight_family_for_glm_likelihood(likelihood)?;
366 let weight_link = weight_link_for_inverse_link(inverse_link);
367 let phi = fixed_glm_dispersion(likelihood)?;
368
369 let certified: Vec<Result<(f64, f64, f64), EstimationError>> = (0..n)
374 .into_par_iter()
375 .map(|i| -> Result<(f64, f64, f64), EstimationError> {
376 let eta_used = eta[i];
377 if !(priorweights[i].is_finite() && priorweights[i] >= 0.0) {
378 return Err(EstimationError::PirlsRowGeometryUnrepresentable {
379 row: i,
380 quantity: "prior weight",
381 eta: eta_used,
382 value: priorweights[i],
383 });
384 }
385 if priorweights[i] == 0.0 {
386 return Ok((0.0, 0.0, 0.0));
387 }
388 let jet =
392 crate::mixture_link::inverse_link_jet_for_inverse_link(inverse_link, eta_used)?;
393 let h4 = crate::mixture_link::inverse_link_pdfthird_derivative_for_inverse_link(
394 inverse_link,
395 eta_used,
396 )?;
397 let (w_obs, c_obs, d_obs) = observed_weight_dispatch(
398 weight_family,
399 weight_link,
400 eta_used,
401 y[i],
402 jet.mu,
403 phi,
404 priorweights[i],
405 jet,
406 h4,
407 );
408 if !w_obs.is_finite() {
416 return Err(EstimationError::PirlsRowGeometryUnrepresentable {
417 row: i,
418 quantity: "observed Hessian weight",
419 eta: eta_used,
420 value: w_obs,
421 });
422 }
423 if !c_obs.is_finite() {
424 return Err(EstimationError::PirlsRowGeometryUnrepresentable {
425 row: i,
426 quantity: "observed Hessian dW/deta",
427 eta: eta_used,
428 value: c_obs,
429 });
430 }
431 if !d_obs.is_finite() {
432 return Err(EstimationError::PirlsRowGeometryUnrepresentable {
433 row: i,
434 quantity: "observed Hessian d2W/deta2",
435 eta: eta_used,
436 value: d_obs,
437 });
438 }
439 Ok((w_obs, c_obs, d_obs))
440 })
441 .collect();
442 let certified: Vec<(f64, f64, f64)> = certified.into_iter().collect::<Result<_, _>>()?;
443 for (i, &(w, c, d)) in certified.iter().enumerate() {
444 hessian_weights[i] = w;
445 hessian_c[i] = c;
446 hessian_d[i] = d;
447 }
448 if fisher_weights.len() != n {
451 crate::bail_invalid_estim!(
452 "observed Hessian Fisher-weight length mismatch: expected {n}, got {}",
453 fisher_weights.len()
454 );
455 }
456 Ok(())
457}
458
459pub(crate) fn compute_observed_hessian_curvature_arrays(
460 likelihood: &GlmLikelihoodSpec,
461 inverse_link: &InverseLink,
462 eta: &Array1<f64>,
463 y: ArrayView1<'_, f64>,
464 fisher_weights: &Array1<f64>,
465 priorweights: ArrayView1<'_, f64>,
466) -> Result<(Array1<f64>, Array1<f64>, Array1<f64>), EstimationError> {
467 let n = eta.len();
468 let mut hessian_weights = Array1::<f64>::zeros(n);
469 let mut hessian_c = Array1::<f64>::zeros(n);
470 let mut hessian_d = Array1::<f64>::zeros(n);
471 compute_observed_hessian_curvature_arrays_into(
472 likelihood,
473 inverse_link,
474 eta,
475 y,
476 fisher_weights,
477 priorweights,
478 &mut hessian_weights,
479 &mut hessian_c,
480 &mut hessian_d,
481 )?;
482 Ok((hessian_weights, hessian_c, hessian_d))
483}
484
485#[inline]
521pub fn observed_weight_noncanonical(
522 y: f64,
523 mu: f64,
524 h1: f64,
525 h2: f64,
526 h3: f64,
527 h4: f64,
528 vj: VarianceJet,
529 phi: f64,
530 pw: f64,
531) -> (f64, f64, f64) {
532 let VarianceJet {
533 v,
534 v1,
535 v2,
536 v3,
537 v4: _,
538 } = vj;
539 let phi_v = phi * v;
540 let phi_v2 = phi * v * v;
541 let phi_v3 = phi * v * v * v;
542
543 let h1_sq = h1 * h1;
545 let w_f = h1_sq / phi_v;
546
547 let n0 = h1_sq; let n1 = 2.0 * h1 * h2; let n2 = 2.0 * (h2 * h2 + h1 * h3); let vd1 = h1 * v1; let vd2 = h2 * v1 + h1_sq * v2; let c_f = (n1 * v - n0 * vd1) / phi_v2;
555
556 let numer_cf = n1 * v - n0 * vd1;
559 let dnumer_cf = n2 * v - n0 * vd2;
560 let d_f = (dnumer_cf * v - 2.0 * numer_cf * vd1) / (phi_v3);
561
562 let b_num = h2 * v - h1_sq * v1;
565 let b = b_num / phi_v2;
566
567 let b_eta_num =
569 h3 * v * v - 3.0 * h1 * h2 * v * v1 - h1_sq * h1 * v * v2 + 2.0 * h1_sq * h1 * v1 * v1;
570 let b_eta = b_eta_num / phi_v3;
571
572 let h1_cu = h1_sq * h1;
588 let h1_qu = h1_sq * h1_sq;
589
590 let db_eta_num = h4 * v * v + 2.0 * h3 * v * h1 * v1
591 - 3.0 * (h2 * h2 + h1 * h3) * v * v1
592 - 3.0 * h1 * h2 * (h1 * v1 * v1 + v * h1 * v2)
593 - 3.0 * h1_sq * h2 * v * v2
594 - h1_cu * (h1 * v1 * v2 + v * h1 * v3)
595 + 6.0 * h1_sq * h2 * v1 * v1
596 + 4.0 * h1_qu * v1 * v2;
597
598 let phi_v4 = phi_v3 * v;
599 let b_etaeta = (db_eta_num * v - 3.0 * b_eta_num * h1 * v1) / phi_v4;
600
601 let resid = y - mu;
603
604 let w_obs = w_f - resid * b;
605 let c_obs = c_f + h1 * b - resid * b_eta;
606 let d_obs = d_f + h2 * b + 2.0 * h1 * b_eta - resid * b_etaeta;
607
608 (pw * w_obs, pw * c_obs, pw * d_obs)
609}
610
611#[inline]
635pub fn e_obs_from_jets(
636 y: f64,
637 mu: f64,
638 h1: f64,
639 h2: f64,
640 h3: f64,
641 h4: f64,
642 h5: f64,
643 vj: VarianceJet,
644 phi: f64,
645 pw: f64,
646) -> f64 {
647 let VarianceJet { v, v1, v2, v3, v4 } = vj;
648 let q = phi * v;
649
650 let h1_sq = h1 * h1;
656 let h1_cu = h1_sq * h1;
657 let h1_qu = h1_sq * h1_sq;
658
659 let q1 = phi * v1 * h1;
660 let q2 = phi * (v1 * h2 + v2 * h1_sq);
661 let q3 = phi * (v1 * h3 + 3.0 * v2 * h1 * h2 + v3 * h1_cu);
662 let q4 = phi
663 * (v1 * h4 + 4.0 * v2 * h1 * h3 + 3.0 * v2 * h2 * h2 + 6.0 * v3 * h1_sq * h2 + v4 * h1_qu);
664
665 let t0 = h1 / q;
671 let t1 = (h2 - t0 * q1) / q;
672 let t2 = (h3 - 2.0 * t1 * q1 - t0 * q2) / q;
673 let t3 = (h4 - 3.0 * t2 * q1 - 3.0 * t1 * q2 - t0 * q3) / q;
674 let t4 = (h5 - 4.0 * t3 * q1 - 6.0 * t2 * q2 - 4.0 * t1 * q3 - t0 * q4) / q;
675
676 let w_f3 = h1 * t3 + 3.0 * h2 * t2 + 3.0 * h3 * t1 + h4 * t0;
682
683 let resid = y - mu;
687 let e_obs = w_f3 + h3 * t1 + 3.0 * h2 * t2 + 3.0 * h1 * t3 - resid * t4;
688
689 pw * e_obs
690}
691
692#[inline]
706pub fn observed_weight_gaussian_log(y: f64, mu: f64, phi: f64, pw: f64) -> (f64, f64, f64) {
707 let inv_phi = pw / phi;
708 let w = inv_phi * mu * (2.0 * mu - y);
709 let c = inv_phi * mu * (4.0 * mu - y);
710 let d = inv_phi * mu * (8.0 * mu - y);
711 (w, c, d)
712}
713
714#[inline]
724pub fn observed_weight_gaussian_inverse(y: f64, eta: f64, phi: f64, pw: f64) -> (f64, f64, f64) {
725 let eta2 = eta * eta;
726 let eta4 = eta2 * eta2;
727 let eta5 = eta4 * eta;
728 let eta6 = eta4 * eta2;
729 let ey = eta * y;
730 let inv_phi = pw / phi;
731 let w = inv_phi * (3.0 - 2.0 * ey) / eta4;
732 let c = inv_phi * 6.0 * (ey - 2.0) / eta5;
733 let d = inv_phi * 12.0 * (5.0 - 2.0 * ey) / eta6;
734 (w, c, d)
735}
736
737#[inline]
753pub fn observed_weight_gamma_log(y: f64, mu: f64, phi: f64, pw: f64) -> (f64, f64, f64) {
754 let w = (pw / phi) * (y / mu);
755 (w, -w, w)
756}
757
758#[inline]
763pub fn observed_weight_negative_binomial_log(
764 y: f64,
765 mu: f64,
766 theta: f64,
767 prior_weight: f64,
768) -> (f64, f64, f64) {
769 let r = if theta >= mu {
770 1.0 / (1.0 + mu / theta)
771 } else {
772 let theta_over_mu = theta / mu;
773 theta_over_mu / (1.0 + theta_over_mu)
774 };
775 let s = 1.0 - r;
776 let w = prior_weight * (y + theta) * r * s;
777 let c = w * (r - s);
778 let d = w * ((r - s) * (r - s) - 2.0 * r * s);
779 (w, c, d)
780}
781
782#[inline]
783pub(crate) fn observed_weight_binomial_logit_from_jet(
784 n_trials: f64,
785 jet: MixtureInverseLinkJet,
786 pw: f64,
787) -> (f64, f64, f64) {
788 let scale = pw * n_trials;
789 (scale * jet.d1, scale * jet.d2, scale * jet.d3)
790}
791
792#[derive(Debug, Clone, Copy, PartialEq)]
798pub enum WeightFamily {
799 Gaussian,
800 Binomial,
801 Poisson,
802 Tweedie { p: f64 },
803 NegativeBinomial { theta: f64 },
804 Beta { phi: f64 },
805 Gamma,
806}
807
808#[derive(Debug, Clone, Copy, PartialEq, Eq)]
813pub enum WeightLink {
814 Identity,
815 Log,
816 Logit,
817 Inverse,
818 Other,
820}
821
822#[inline]
823pub fn variance_jet_for_weight_family(family: WeightFamily, mu: f64) -> VarianceJet {
824 match family {
825 WeightFamily::Gaussian => VarianceJet::gaussian(),
826 WeightFamily::Binomial => VarianceJet::binomial_n(mu),
827 WeightFamily::Poisson => VarianceJet::poisson(mu),
828 WeightFamily::Tweedie { p } => VarianceJet::tweedie(mu, p),
829 WeightFamily::NegativeBinomial { theta } => VarianceJet::negative_binomial(mu, theta),
830 WeightFamily::Beta { phi } => VarianceJet::beta(mu, phi),
831 WeightFamily::Gamma => VarianceJet::gamma(mu),
832 }
833}
834
835pub fn observed_weight_dispatch(
848 family: WeightFamily,
849 link: WeightLink,
850 eta: f64,
851 y: f64,
852 mu: f64,
853 phi: f64,
854 prior_weight: f64,
855 jet: MixtureInverseLinkJet,
856 h4: f64,
857) -> (f64, f64, f64) {
858 match (family, link) {
859 (WeightFamily::Gaussian, WeightLink::Log) => {
860 observed_weight_gaussian_log(y, mu, phi, prior_weight)
861 }
862 (WeightFamily::Gaussian, WeightLink::Inverse) => {
863 observed_weight_gaussian_inverse(y, eta, phi, prior_weight)
864 }
865 (WeightFamily::Gamma, WeightLink::Log) => {
866 observed_weight_gamma_log(y, mu, phi, prior_weight)
867 }
868 (WeightFamily::NegativeBinomial { theta }, WeightLink::Log) => {
869 observed_weight_negative_binomial_log(y, mu, theta, prior_weight)
870 }
871 (WeightFamily::Binomial, WeightLink::Logit) => {
872 observed_weight_binomial_logit_from_jet(1.0, jet, prior_weight)
873 }
874 _ => {
875 let vj = variance_jet_for_weight_family(family, mu);
877 observed_weight_noncanonical(y, mu, jet.d1, jet.d2, jet.d3, h4, vj, phi, prior_weight)
878 }
879 }
880}
881
882#[derive(Clone)]
883pub enum DirectionalWorkingCurvature {
884 Diagonal(Array1<f64>),
888}
889
890pub fn directionalworking_curvature_from_c_array(
891 c_array: &Array1<f64>,
892 eta_direction: &Array1<f64>,
893) -> DirectionalWorkingCurvature {
894 DirectionalWorkingCurvature::Diagonal(c_array * eta_direction)
895}