1use crate::estimate::EstimationError;
2use crate::quadrature::latent_cloglog_jet5;
3use gam_math::{
4 jet_tower::trigamma,
5 probability::{normal_cdf, normal_pdf},
6};
7use gam_math::special::stable_polynomial_times_exp_neg as stable_nonnegative_poly_times_exp_neg;
8use gam_problem::{
9 InverseLink, LatentCLogLogState, LikelihoodSpec, LinkComponent, LinkFunction, MixtureLinkSpec,
10 MixtureLinkState, ResponseFamily, SasLinkSpec, SasLinkState, StandardLink,
11};
12use ndarray::{Array1, Array2};
13use statrs::function::beta::{beta_reg, ln_beta};
14use statrs::function::gamma::digamma;
15use std::ops::Neg;
16use std::sync::OnceLock;
17
18const SAS_U_CLAMP: f64 = 50.0;
19pub const LOG_LINK_SOLVER_ETA_MIN: f64 = -700.0;
25pub const LOG_LINK_SOLVER_ETA_MAX: f64 = 700.0;
27pub(crate) const SAS_LOG_DELTA_BOUND: f64 = 12.0;
32
33pub(crate) const BETA_LOGISTIC_LOG_SHAPE_BOUND: f64 = 1.5;
61
62#[inline]
63fn latent_cloglog_quadctx() -> &'static crate::quadrature::QuadratureContext {
64 static QUADCTX: OnceLock<crate::quadrature::QuadratureContext> = OnceLock::new();
65 QUADCTX.get_or_init(crate::quadrature::QuadratureContext::new)
66}
67
68#[inline]
69fn latent_cloglog_point_jet(
70 state: &LatentCLogLogState,
71 eta: f64,
72) -> Result<InverseLinkJet, EstimationError> {
73 let jet = latent_cloglog_jet5(latent_cloglog_quadctx(), eta, state.latent_sd)?;
74 Ok(InverseLinkJet {
75 mu: jet.mean,
76 d1: jet.d1,
77 d2: jet.d2,
78 d3: jet.d3,
79 })
80}
81
82#[inline]
83pub(crate) fn log_link_solver_exp(eta: f64) -> Result<f64, EstimationError> {
84 if !(LOG_LINK_SOLVER_ETA_MIN..=LOG_LINK_SOLVER_ETA_MAX).contains(&eta) {
85 return Err(EstimationError::InverseLinkDomainViolation {
86 link: "standard log inverse link",
87 eta,
88 lower: LOG_LINK_SOLVER_ETA_MIN,
89 upper: LOG_LINK_SOLVER_ETA_MAX,
90 });
91 }
92 Ok(eta.exp())
93}
94
95#[inline]
96fn finite_inverse_link_eta(link: &'static str, eta: f64) -> Result<f64, EstimationError> {
97 if !eta.is_finite() {
98 return Err(EstimationError::InverseLinkDomainViolation {
99 link,
100 eta,
101 lower: -f64::MAX,
102 upper: f64::MAX,
103 });
104 }
105 Ok(eta)
106}
107
108#[derive(Clone, Copy)]
109struct AsinhJet5 {
110 value: f64,
111 d1: f64,
112 d2: f64,
113 d3: f64,
114 d4: f64,
115 d5: f64,
116}
117
118#[inline]
121fn asinh_jet5(eta: f64) -> AsinhJet5 {
122 let q = eta.hypot(1.0);
123 let inv_q = q.recip();
124 let inv_q2 = inv_q * inv_q;
125 let inv_q3 = inv_q2 * inv_q;
126 let inv_q4 = inv_q2 * inv_q2;
127 let inv_q5 = inv_q4 * inv_q;
128 let t = eta / q;
129 let t2 = t * t;
130 let t4 = t2 * t2;
131 let value = {
139 let v = eta.asinh();
140 if v.is_finite() {
141 v
142 } else {
143 eta.signum() * (eta.abs().ln() + std::f64::consts::LN_2)
144 }
145 };
146 AsinhJet5 {
147 value,
148 d1: inv_q,
149 d2: -t * inv_q2,
150 d3: (2.0 * t2 - inv_q2) * inv_q3,
151 d4: t * (9.0 * inv_q2 - 6.0 * t2) * inv_q4,
152 d5: (9.0 * inv_q4 - 72.0 * t2 * inv_q2 + 24.0 * t4) * inv_q5,
153 }
154}
155
156#[derive(Clone, Copy, Debug, PartialEq)]
157pub struct InverseLinkJet {
158 pub mu: f64,
159 pub d1: f64,
160 pub d2: f64,
161 pub d3: f64,
162}
163
164#[derive(Clone, Copy, Debug, PartialEq)]
165pub struct LogitJet5 {
166 pub mu: f64,
167 pub d1: f64,
168 pub d2: f64,
169 pub d3: f64,
170 pub d4: f64,
171 pub d5: f64,
172}
173
174#[inline]
175fn canonicalzero(v: f64) -> f64 {
176 if v == 0.0 { 0.0 } else { v }
181}
182
183#[inline]
184fn canonicalize_jet(mut jet: InverseLinkJet) -> InverseLinkJet {
185 jet.d1 = canonicalzero(jet.d1);
186 jet.d2 = canonicalzero(jet.d2);
187 jet.d3 = canonicalzero(jet.d3);
188 jet
189}
190
191#[inline]
192pub fn logit_inverse_link_jet5(eta: f64) -> LogitJet5 {
193 if eta.is_nan() {
194 return LogitJet5 {
195 mu: f64::NAN,
196 d1: f64::NAN,
197 d2: f64::NAN,
198 d3: f64::NAN,
199 d4: f64::NAN,
200 d5: f64::NAN,
201 };
202 }
203 if eta == f64::INFINITY {
204 return LogitJet5 {
205 mu: 1.0,
206 d1: 0.0,
207 d2: 0.0,
208 d3: 0.0,
209 d4: 0.0,
210 d5: 0.0,
211 };
212 }
213 if eta == f64::NEG_INFINITY {
214 return LogitJet5 {
215 mu: 0.0,
216 d1: 0.0,
217 d2: 0.0,
218 d3: 0.0,
219 d4: 0.0,
220 d5: 0.0,
221 };
222 }
223
224 let jet = if eta >= 0.0 {
225 let z = (-eta).exp();
226 let opz = 1.0 + z;
227 let opz2 = opz * opz;
228 let opz3 = opz2 * opz;
229 let opz4 = opz3 * opz;
230 let opz5 = opz4 * opz;
231 let opz6 = opz5 * opz;
232 let z2 = z * z;
233 let z3 = z2 * z;
234 let z4 = z3 * z;
235 LogitJet5 {
236 mu: 1.0 / opz,
237 d1: z / opz2,
238 d2: z * (z - 1.0) / opz3,
239 d3: z * (z2 - 4.0 * z + 1.0) / opz4,
240 d4: z * (z3 - 11.0 * z2 + 11.0 * z - 1.0) / opz5,
241 d5: z * (z4 - 26.0 * z3 + 66.0 * z2 - 26.0 * z + 1.0) / opz6,
242 }
243 } else {
244 let z = eta.exp();
245 let opz = 1.0 + z;
246 let opz2 = opz * opz;
247 let opz3 = opz2 * opz;
248 let opz4 = opz3 * opz;
249 let opz5 = opz4 * opz;
250 let opz6 = opz5 * opz;
251 let z2 = z * z;
252 let z3 = z2 * z;
253 let z4 = z3 * z;
254 LogitJet5 {
255 mu: z / opz,
256 d1: z / opz2,
257 d2: z * (1.0 - z) / opz3,
258 d3: z * (1.0 - 4.0 * z + z2) / opz4,
259 d4: z * (1.0 - 11.0 * z + 11.0 * z2 - z3) / opz5,
260 d5: z * (1.0 - 26.0 * z + 66.0 * z2 - 26.0 * z3 + z4) / opz6,
261 }
262 };
263 LogitJet5 {
264 mu: jet.mu,
265 d1: canonicalzero(jet.d1),
266 d2: canonicalzero(jet.d2),
267 d3: canonicalzero(jet.d3),
268 d4: canonicalzero(jet.d4),
269 d5: canonicalzero(jet.d5),
270 }
271}
272
273#[inline]
274fn probit_jet(eta: f64) -> InverseLinkJet {
275 if eta.is_nan() {
286 return InverseLinkJet {
287 mu: f64::NAN,
288 d1: f64::NAN,
289 d2: f64::NAN,
290 d3: f64::NAN,
291 };
292 }
293 if eta == f64::INFINITY {
294 return InverseLinkJet {
295 mu: 1.0,
296 d1: 0.0,
297 d2: 0.0,
298 d3: 0.0,
299 };
300 }
301 if eta == f64::NEG_INFINITY {
302 return InverseLinkJet {
303 mu: 0.0,
304 d1: 0.0,
305 d2: 0.0,
306 d3: 0.0,
307 };
308 }
309 let x = eta;
310 let phi = normal_pdf(x);
311 if phi == 0.0 {
312 return InverseLinkJet {
313 mu: normal_cdf(x),
314 d1: 0.0,
315 d2: 0.0,
316 d3: 0.0,
317 };
318 }
319 InverseLinkJet {
320 mu: normal_cdf(x),
321 d1: phi,
322 d2: -x * phi,
323 d3: (x * x - 1.0) * phi,
324 }
325}
326
327#[inline]
328fn probit_pdfthird_derivative(eta: f64) -> f64 {
329 if eta.is_nan() {
333 return f64::NAN;
334 }
335 if !eta.is_finite() {
336 return 0.0;
337 }
338 let x = eta;
339 let phi = normal_pdf(x);
340 if phi == 0.0 {
341 return 0.0;
342 }
343 canonicalzero(-(x * x * x - 3.0 * x) * phi)
344}
345
346#[inline]
347fn probit_pdffourth_derivative(eta: f64) -> f64 {
348 if eta.is_nan() {
350 return f64::NAN;
351 }
352 if !eta.is_finite() {
353 return 0.0;
354 }
355 let x = eta;
356 let phi = normal_pdf(x);
357 if phi == 0.0 {
358 return 0.0;
359 }
360 canonicalzero((x * x * x * x - 6.0 * x * x + 3.0) * phi)
361}
362
363#[inline]
366fn taylor5_mul(a: &[f64; 5], b: &[f64; 5]) -> [f64; 5] {
367 let mut c = [0.0_f64; 5];
368 for i in 0..5 {
369 let ai = a[i];
370 if ai == 0.0 {
371 continue;
372 }
373 for j in 0..(5 - i) {
374 c[i + j] += ai * b[j];
375 }
376 }
377 c
378}
379
380#[inline]
382fn taylor5_inv(a: &[f64; 5]) -> [f64; 5] {
383 let mut b = [0.0_f64; 5];
384 b[0] = 1.0 / a[0];
385 for k in 1..5 {
386 let mut s = 0.0_f64;
387 for j in 1..=k {
388 s += a[j] * b[k - j];
389 }
390 b[k] = -s * b[0];
391 }
392 b
393}
394
395pub(crate) fn fisher_weight_jet5(link: StandardLink, eta: f64) -> (f64, f64, f64, f64, f64) {
411 match link {
412 StandardLink::Logit => {
413 let jet = logit_inverse_link_jet5(eta);
414 (jet.d1, jet.d2, jet.d3, jet.d4, jet.d5)
415 }
416 StandardLink::Probit => probit_fisher_weight_jet5(eta),
417 StandardLink::CLogLog => component_fisher_weight_jet5(LinkComponent::CLogLog, eta),
418 StandardLink::LogLog => component_fisher_weight_jet5(LinkComponent::LogLog, eta),
419 StandardLink::Cauchit => component_fisher_weight_jet5(LinkComponent::Cauchit, eta),
420 StandardLink::Identity | StandardLink::Log => (0.0, 0.0, 0.0, 0.0, 0.0),
421 }
422}
423
424pub(crate) fn fisher_weight_jet5_for_inverse_link(
425 link: &InverseLink,
426 eta: f64,
427) -> Result<(f64, f64, f64, f64, f64), EstimationError> {
428 match link {
429 InverseLink::Standard(link) => Ok(fisher_weight_jet5(*link, eta)),
430 InverseLink::LatentCLogLog(_)
431 | InverseLink::Sas(_)
432 | InverseLink::BetaLogistic(_)
433 | InverseLink::Mixture(_) => {
434 let jet = link.jet(eta)?;
435 let d4 = inverse_link_pdfthird_derivative_for_inverse_link(link, eta)?;
436 let d5 = inverse_link_pdffourth_derivative_for_inverse_link(link, eta)?;
437 Ok(fisher_weight_jet5_from_inverse_link_derivatives(
438 jet.mu, jet.d1, jet.d2, jet.d3, d4, d5,
439 ))
440 }
441 }
442}
443
444#[inline]
445fn component_fisher_weight_jet5(component: LinkComponent, eta: f64) -> (f64, f64, f64, f64, f64) {
446 let jet = component_inverse_link_jet(component, eta);
447 let d4 = component_inverse_link_pdfthird_derivative(component, eta);
448 let d5 = component_inverse_link_pdffourth_derivative(component, eta);
449 fisher_weight_jet5_from_inverse_link_derivatives(jet.mu, jet.d1, jet.d2, jet.d3, d4, d5)
450}
451
452#[inline]
453fn fisher_weight_jet5_from_inverse_link_derivatives(
454 mu: f64,
455 d1: f64,
456 d2: f64,
457 d3: f64,
458 d4: f64,
459 d5: f64,
460) -> (f64, f64, f64, f64, f64) {
461 if [mu, d1, d2, d3, d4, d5].iter().any(|v| v.is_nan()) {
462 return (f64::NAN, f64::NAN, f64::NAN, f64::NAN, f64::NAN);
463 }
464 let variance = mu * (1.0 - mu);
465 if !(variance > 0.0) || !variance.is_finite() {
466 return (0.0, 0.0, 0.0, 0.0, 0.0);
467 }
468
469 let factorial = [1.0_f64, 1.0, 2.0, 6.0, 24.0];
470 let mu_d = [mu, d1, d2, d3, d4];
471 let one_minus_mu_d = [1.0 - mu, -d1, -d2, -d3, -d4];
472 let dmu_d = [d1, d2, d3, d4, d5];
473 let mut mu_t = [0.0_f64; 5];
474 let mut one_minus_mu_t = [0.0_f64; 5];
475 let mut dmu_t = [0.0_f64; 5];
476 for k in 0..5 {
477 let inv_fact = 1.0 / factorial[k];
478 mu_t[k] = mu_d[k] * inv_fact;
479 one_minus_mu_t[k] = one_minus_mu_d[k] * inv_fact;
480 dmu_t[k] = dmu_d[k] * inv_fact;
481 }
482 let num_t = taylor5_mul(&dmu_t, &dmu_t);
483 let den_t = taylor5_mul(&mu_t, &one_minus_mu_t);
484 if !(den_t[0] > 0.0) || !den_t[0].is_finite() {
485 return (0.0, 0.0, 0.0, 0.0, 0.0);
486 }
487 let w_t = taylor5_mul(&num_t, &taylor5_inv(&den_t));
488 (
489 canonicalzero(w_t[0] * factorial[0]),
490 canonicalzero(w_t[1] * factorial[1]),
491 canonicalzero(w_t[2] * factorial[2]),
492 canonicalzero(w_t[3] * factorial[3]),
493 canonicalzero(w_t[4] * factorial[4]),
494 )
495}
496
497#[inline]
500fn probit_fisher_weight_jet5(eta: f64) -> (f64, f64, f64, f64, f64) {
501 if eta.is_nan() {
502 return (f64::NAN, f64::NAN, f64::NAN, f64::NAN, f64::NAN);
503 }
504 if !eta.is_finite() {
505 return (0.0, 0.0, 0.0, 0.0, 0.0);
506 }
507 let x = eta;
508 let p = normal_cdf(x);
509 let q = normal_cdf(-x);
513 let phi = normal_pdf(x);
514 if !(p > 0.0) || !(q > 0.0) || p * q <= 0.0 {
517 return (0.0, 0.0, 0.0, 0.0, 0.0);
518 }
519 let phi1 = -x * phi;
521 let phi2 = (x * x - 1.0) * phi;
522 let phi3 = -(x * x * x - 3.0 * x) * phi;
523 let phi4 = (x * x * x * x - 6.0 * x * x + 3.0) * phi;
524 let f_d = [phi, phi1, phi2, phi3, phi4];
527 let p_d = [p, phi, phi1, phi2, phi3];
528 let q_d = [q, -phi, -phi1, -phi2, -phi3];
529 let factorial = [1.0_f64, 1.0, 2.0, 6.0, 24.0];
531 let mut f_t = [0.0_f64; 5];
532 let mut p_t = [0.0_f64; 5];
533 let mut q_t = [0.0_f64; 5];
534 for k in 0..5 {
535 let inv_fact = 1.0 / factorial[k];
536 f_t[k] = f_d[k] * inv_fact;
537 p_t[k] = p_d[k] * inv_fact;
538 q_t[k] = q_d[k] * inv_fact;
539 }
540 let num_t = taylor5_mul(&f_t, &f_t);
541 let den_t = taylor5_mul(&p_t, &q_t);
542 let w_t = taylor5_mul(&num_t, &taylor5_inv(&den_t));
543 (
545 canonicalzero(w_t[0] * factorial[0]),
546 canonicalzero(w_t[1] * factorial[1]),
547 canonicalzero(w_t[2] * factorial[2]),
548 canonicalzero(w_t[3] * factorial[3]),
549 canonicalzero(w_t[4] * factorial[4]),
550 )
551}
552
553#[inline]
554fn chain_inverse_link_jet(base: InverseLinkJet, z1: f64, z2: f64, z3: f64) -> InverseLinkJet {
555 InverseLinkJet {
556 mu: base.mu,
557 d1: base.d1 * z1,
558 d2: base.d2 * z1 * z1 + base.d1 * z2,
559 d3: base.d3 * z1 * z1 * z1 + 3.0 * base.d2 * z1 * z2 + base.d1 * z3,
560 }
561}
562
563#[inline]
564fn component_inverse_link_pdfthird_derivative(component: LinkComponent, eta: f64) -> f64 {
565 match component {
566 LinkComponent::Probit => probit_pdfthird_derivative(eta),
567 LinkComponent::Logit => logit_inverse_link_jet5(eta).d4,
568 LinkComponent::CLogLog => {
569 if eta.is_nan() {
577 return f64::NAN;
578 }
579 if !eta.is_finite() {
580 return 0.0;
581 }
582 let t = eta.exp();
583 canonicalzero(stable_nonnegative_poly_times_exp_neg(
584 t,
585 &[0.0, 1.0, -7.0, 6.0, -1.0],
586 ))
587 }
588 LinkComponent::LogLog => {
589 if eta.is_nan() {
596 return f64::NAN;
597 }
598 if !eta.is_finite() {
599 return 0.0;
600 }
601 let r = (-eta).exp();
602 canonicalzero(stable_nonnegative_poly_times_exp_neg(
603 r,
604 &[0.0, -1.0, 7.0, -6.0, 1.0],
605 ))
606 }
607 LinkComponent::Cauchit => {
608 if eta.is_nan() {
616 return f64::NAN;
617 }
618 if !eta.is_finite() {
619 return 0.0;
620 }
621 let denom = 1.0 + eta * eta;
622 24.0 * eta * (1.0 - eta * eta) / (std::f64::consts::PI * denom.powi(4))
623 }
624 }
625}
626
627#[inline]
630fn component_inverse_link_pdffourth_derivative(component: LinkComponent, eta: f64) -> f64 {
631 match component {
632 LinkComponent::Probit => probit_pdffourth_derivative(eta),
633 LinkComponent::Logit => logit_inverse_link_jet5(eta).d5,
634 LinkComponent::CLogLog => {
635 if eta.is_nan() {
640 return f64::NAN;
641 }
642 if !eta.is_finite() {
643 return 0.0;
644 }
645 let t = eta.exp();
646 canonicalzero(stable_nonnegative_poly_times_exp_neg(
647 t,
648 &[0.0, 1.0, -15.0, 25.0, -10.0, 1.0],
649 ))
650 }
651 LinkComponent::LogLog => {
652 if eta.is_nan() {
657 return f64::NAN;
658 }
659 if !eta.is_finite() {
660 return 0.0;
661 }
662 let r = (-eta).exp();
663 canonicalzero(stable_nonnegative_poly_times_exp_neg(
664 r,
665 &[0.0, 1.0, -15.0, 25.0, -10.0, 1.0],
666 ))
667 }
668 LinkComponent::Cauchit => {
669 if eta.is_nan() {
671 return f64::NAN;
672 }
673 if !eta.is_finite() {
674 return 0.0;
675 }
676 let e2 = eta * eta;
677 let denom = 1.0 + e2;
678 24.0 * (1.0 - 10.0 * e2 + 5.0 * e2 * e2) / (std::f64::consts::PI * denom.powi(5))
679 }
680 }
681}
682
683#[derive(Clone, Debug, PartialEq)]
684pub struct MixtureJetWithRhoPartials {
685 pub jet: InverseLinkJet,
686 pub djet_drho: Vec<InverseLinkJet>,
689 pub d2mu_drho2: Array2<f64>,
691 pub d2d1_drho2: Array2<f64>,
693 pub d2d2_drho2: Array2<f64>,
698}
699
700#[derive(Clone, Debug, PartialEq)]
701pub struct SasJetWithParamPartials {
702 pub jet: InverseLinkJet,
703 pub djet_depsilon: InverseLinkJet,
704 pub djet_dlog_delta: InverseLinkJet,
705 pub d2mu_dparams2: Array2<f64>,
709 pub d2d1_dparams2: Array2<f64>,
711 pub d2d2_dparams2: Array2<f64>,
716}
717
718#[derive(Clone, Debug, PartialEq)]
719pub enum LinkParamPartials {
720 Mixture(MixtureJetWithRhoPartials),
721 Sas(SasJetWithParamPartials),
722}
723
724pub trait InverseLinkKernel {
730 fn jet(&self, eta: f64) -> Result<InverseLinkJet, EstimationError>;
731
732 fn param_partials(&self, eta: f64) -> Result<Option<LinkParamPartials>, EstimationError> {
733 assert!(eta.is_finite(), "eta must be finite");
734 Ok(None)
735 }
736}
737
738#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
739pub struct ProbitLinkKernel;
740
741#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
742pub struct LogitLinkKernel;
743
744#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
745pub struct CLogLogLinkKernel;
746
747#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
748pub struct LogLogLinkKernel;
749
750#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
751pub struct CauchitLinkKernel;
752
753pub fn sas_link_state_from_raw(
761 raw_epsilon: f64,
762 raw_log_delta: f64,
763) -> Result<SasLinkState, String> {
764 if !raw_epsilon.is_finite() || !raw_log_delta.is_finite() {
765 return Err("SAS link parameters must be finite".to_string());
766 }
767 Ok(SasLinkState {
768 epsilon: raw_epsilon,
769 log_delta: raw_log_delta,
770 delta: sas_delta_from_raw_log_delta(raw_log_delta),
771 })
772}
773
774pub fn state_from_sasspec(spec: SasLinkSpec) -> Result<SasLinkState, String> {
775 sas_link_state_from_raw(spec.initial_epsilon, spec.initial_log_delta)
776}
777
778pub fn state_from_beta_logisticspec(spec: SasLinkSpec) -> Result<SasLinkState, String> {
779 if !spec.initial_epsilon.is_finite() || !spec.initial_log_delta.is_finite() {
780 return Err("Beta-Logistic link parameters must be finite".to_string());
781 }
782 let log_shape_center = spec.initial_log_delta;
788 Ok(SasLinkState {
789 epsilon: spec.initial_epsilon,
790 log_delta: log_shape_center,
791 delta: sas_delta_from_raw_log_delta(log_shape_center),
792 })
793}
794
795const SPLICE_INTERIOR_FRAC: f64 = 0.8;
799
800#[derive(Clone, Copy, Debug)]
802struct SmoothBoundJet {
803 g: f64,
804 d1: f64,
805 d2: f64,
806 d3: f64,
807 d4: f64,
808 d5: f64,
809}
810
811#[inline]
840fn smooth_bound_jet(value: f64, bound: f64) -> SmoothBoundJet {
841 let b = bound.max(f64::EPSILON);
842 let a = SPLICE_INTERIOR_FRAC * b; let l = 2.0 * (b - a); let ax = value.abs();
845 if ax <= a {
846 return SmoothBoundJet {
848 g: value,
849 d1: 1.0,
850 d2: 0.0,
851 d3: 0.0,
852 d4: 0.0,
853 d5: 0.0,
854 };
855 }
856 let sign = if value < 0.0 { -1.0 } else { 1.0 };
857 if ax >= a + l {
858 return SmoothBoundJet {
860 g: sign * b,
861 d1: 0.0,
862 d2: 0.0,
863 d3: 0.0,
864 d4: 0.0,
865 d5: 0.0,
866 };
867 }
868 let w = (ax - a) / l;
871 let w2 = w * w;
872 let w3 = w2 * w;
873 let w4 = w3 * w;
874 let w5 = w4 * w;
875 let w6 = w5 * w;
876 let w7 = w6 * w;
877 let w8 = w7 * w;
878 let w9 = w8 * w;
879 let w10 = w9 * w;
880 let s = 70.0 * w9 - 315.0 * w8 + 540.0 * w7 - 420.0 * w6 + 126.0 * w5;
881 let sp = 630.0 * w8 - 2520.0 * w7 + 3780.0 * w6 - 2520.0 * w5 + 630.0 * w4;
882 let spp = 5040.0 * w7 - 17640.0 * w6 + 22680.0 * w5 - 12600.0 * w4 + 2520.0 * w3;
883 let sppp = 35280.0 * w6 - 105840.0 * w5 + 113400.0 * w4 - 50400.0 * w3 + 7560.0 * w2;
884 let spppp = 211680.0 * w5 - 529200.0 * w4 + 453600.0 * w3 - 151200.0 * w2 + 15120.0 * w;
885 let iw = w - 7.0 * w10 + 35.0 * w9 - 67.5 * w8 + 60.0 * w7 - 21.0 * w6;
887 let g0 = a + l * iw;
888 let l2 = l * l;
891 SmoothBoundJet {
892 g: sign * g0,
893 d1: 1.0 - s,
894 d2: sign * (-sp / l),
895 d3: -spp / l2,
896 d4: sign * (-sppp / (l2 * l)),
897 d5: -spppp / (l2 * l2),
898 }
899}
900
901#[inline]
902fn sas_effective_log_delta(raw_log_delta: f64) -> (f64, f64) {
903 let sb = smooth_bound_jet(raw_log_delta, SAS_LOG_DELTA_BOUND);
904 (sb.g, sb.d1)
905}
906
907#[inline]
908fn sas_delta_from_raw_log_delta(raw_log_delta: f64) -> f64 {
909 let (ld_eff, _) = sas_effective_log_delta(raw_log_delta);
910 ld_eff.exp()
911}
912
913pub fn validate_mixturespec(spec: &MixtureLinkSpec) -> Result<(), String> {
914 if spec.components.is_empty() {
915 return Err("mixture link requires at least 1 component".to_string());
916 }
917 if spec.initial_rho.len() + 1 != spec.components.len() {
918 return Err(format!(
919 "mixture link rho length mismatch: expected {}, got {}",
920 spec.components.len() - 1,
921 spec.initial_rho.len()
922 ));
923 }
924 for i in 0..spec.components.len() {
925 for j in (i + 1)..spec.components.len() {
926 if spec.components[i] == spec.components[j] {
927 return Err("mixture link components must be unique".to_string());
928 }
929 }
930 }
931 let has_anchor = spec.components.iter().any(|component| {
946 matches!(
947 component,
948 LinkComponent::Logit | LinkComponent::Probit | LinkComponent::CLogLog
949 )
950 });
951 if !has_anchor && spec.components.len() > 1 {
952 let unsupported: Vec<&str> = spec
953 .components
954 .iter()
955 .map(|component| component.name())
956 .collect();
957 return Err(format!(
958 "mixture link components {{{}}} are unsupported: at least one component \
959 must map to a LinkFunction variant (logit/probit/cloglog) so the mixture's \
960 projected LinkFunction is well defined; cauchit and loglog have no \
961 LinkFunction representative",
962 unsupported.join(", ")
963 ));
964 }
965 Ok(())
966}
967
968pub fn softmax_last_fixedzero(rho: &Array1<f64>) -> Array1<f64> {
969 let k = rho.len() + 1;
970 let mut logits = Vec::with_capacity(k);
971 let mut maxv = 0.0_f64;
972 for &v in rho {
973 maxv = maxv.max(v);
974 logits.push(v);
975 }
976 maxv = maxv.max(0.0);
977 logits.push(0.0);
978
979 let mut sum = 0.0_f64;
980 let mut exps = vec![0.0_f64; k];
981 for i in 0..k {
982 let e = (logits[i] - maxv).exp();
983 exps[i] = e;
984 sum += e;
985 }
986 if !sum.is_finite() || sum <= 0.0 {
987 return Array1::from_elem(k, 1.0 / k as f64);
988 }
989 let inv = 1.0 / sum;
990 Array1::from_iter(exps.into_iter().map(|v| v * inv))
991}
992
993pub fn state_fromspec(spec: &MixtureLinkSpec) -> Result<MixtureLinkState, String> {
994 validate_mixturespec(spec)?;
995 let pi = softmax_last_fixedzero(&spec.initial_rho);
996 Ok(MixtureLinkState {
997 components: spec.components.clone(),
998 rho: spec.initial_rho.clone(),
999 pi,
1000 })
1001}
1002
1003#[inline]
1004pub fn component_inverse_link_jet(component: LinkComponent, eta: f64) -> InverseLinkJet {
1005 canonicalize_jet(match component {
1006 LinkComponent::Logit => {
1007 let jet = logit_inverse_link_jet5(eta);
1008 InverseLinkJet {
1009 mu: jet.mu,
1010 d1: jet.d1,
1011 d2: jet.d2,
1012 d3: jet.d3,
1013 }
1014 }
1015 LinkComponent::Probit => probit_jet(eta),
1016 LinkComponent::CLogLog => {
1017 if eta.is_nan() {
1018 return InverseLinkJet {
1019 mu: f64::NAN,
1020 d1: f64::NAN,
1021 d2: f64::NAN,
1022 d3: f64::NAN,
1023 };
1024 }
1025 let t = eta.exp();
1026 if !t.is_finite() {
1027 return InverseLinkJet {
1028 mu: 1.0,
1029 d1: 0.0,
1030 d2: 0.0,
1031 d3: 0.0,
1032 };
1033 }
1034 InverseLinkJet {
1035 mu: -(-t).exp_m1(),
1036 d1: stable_nonnegative_poly_times_exp_neg(t, &[0.0, 1.0]),
1037 d2: stable_nonnegative_poly_times_exp_neg(t, &[0.0, 1.0, -1.0]),
1038 d3: stable_nonnegative_poly_times_exp_neg(t, &[0.0, 1.0, -3.0, 1.0]),
1039 }
1040 }
1041 LinkComponent::LogLog => {
1042 if eta.is_nan() {
1043 return InverseLinkJet {
1044 mu: f64::NAN,
1045 d1: f64::NAN,
1046 d2: f64::NAN,
1047 d3: f64::NAN,
1048 };
1049 }
1050 let r = (-eta).exp();
1051 if !r.is_finite() {
1052 return InverseLinkJet {
1053 mu: 0.0,
1054 d1: 0.0,
1055 d2: 0.0,
1056 d3: 0.0,
1057 };
1058 }
1059 InverseLinkJet {
1060 mu: (-r).exp(),
1061 d1: stable_nonnegative_poly_times_exp_neg(r, &[0.0, 1.0]),
1062 d2: stable_nonnegative_poly_times_exp_neg(r, &[0.0, -1.0, 1.0]),
1063 d3: stable_nonnegative_poly_times_exp_neg(r, &[0.0, 1.0, -3.0, 1.0]),
1064 }
1065 }
1066 LinkComponent::Cauchit => {
1067 if eta.is_nan() {
1068 return InverseLinkJet {
1069 mu: f64::NAN,
1070 d1: f64::NAN,
1071 d2: f64::NAN,
1072 d3: f64::NAN,
1073 };
1074 }
1075 let den = 1.0 + eta * eta;
1076 let d1 = if eta.is_finite() {
1077 1.0 / (std::f64::consts::PI * den)
1078 } else {
1079 0.0
1080 };
1081 let d2 = if eta.is_finite() {
1082 -2.0 * eta / (std::f64::consts::PI * den * den)
1083 } else {
1084 0.0
1085 };
1086 let d3 = if eta.is_finite() {
1087 (6.0 * eta * eta - 2.0) / (std::f64::consts::PI * den * den * den)
1088 } else {
1089 0.0
1090 };
1091 InverseLinkJet {
1092 mu: 0.5 + eta.atan() / std::f64::consts::PI,
1093 d1,
1094 d2,
1095 d3,
1096 }
1097 }
1098 })
1099}
1100
1101impl InverseLinkKernel for ProbitLinkKernel {
1102 #[inline]
1103 fn jet(&self, eta: f64) -> Result<InverseLinkJet, EstimationError> {
1104 Ok(component_inverse_link_jet(LinkComponent::Probit, eta))
1105 }
1106}
1107
1108impl InverseLinkKernel for LogitLinkKernel {
1109 #[inline]
1110 fn jet(&self, eta: f64) -> Result<InverseLinkJet, EstimationError> {
1111 Ok(component_inverse_link_jet(LinkComponent::Logit, eta))
1112 }
1113}
1114
1115impl InverseLinkKernel for CLogLogLinkKernel {
1116 #[inline]
1117 fn jet(&self, eta: f64) -> Result<InverseLinkJet, EstimationError> {
1118 Ok(component_inverse_link_jet(LinkComponent::CLogLog, eta))
1119 }
1120}
1121
1122impl InverseLinkKernel for LogLogLinkKernel {
1123 #[inline]
1124 fn jet(&self, eta: f64) -> Result<InverseLinkJet, EstimationError> {
1125 Ok(component_inverse_link_jet(LinkComponent::LogLog, eta))
1126 }
1127}
1128
1129impl InverseLinkKernel for CauchitLinkKernel {
1130 #[inline]
1131 fn jet(&self, eta: f64) -> Result<InverseLinkJet, EstimationError> {
1132 Ok(component_inverse_link_jet(LinkComponent::Cauchit, eta))
1133 }
1134}
1135
1136impl InverseLinkKernel for LinkComponent {
1137 #[inline]
1138 fn jet(&self, eta: f64) -> Result<InverseLinkJet, EstimationError> {
1139 Ok(component_inverse_link_jet(*self, eta))
1140 }
1141}
1142
1143impl InverseLinkKernel for LinkFunction {
1144 fn jet(&self, eta: f64) -> Result<InverseLinkJet, EstimationError> {
1145 match self {
1146 LinkFunction::Logit => LogitLinkKernel.jet(eta),
1147 LinkFunction::Probit => ProbitLinkKernel.jet(eta),
1148 LinkFunction::CLogLog => CLogLogLinkKernel.jet(eta),
1149 LinkFunction::LogLog => LogLogLinkKernel.jet(eta),
1150 LinkFunction::Cauchit => CauchitLinkKernel.jet(eta),
1151 LinkFunction::Identity => Ok(InverseLinkJet {
1152 mu: eta,
1153 d1: 1.0,
1154 d2: 0.0,
1155 d3: 0.0,
1156 }),
1157 LinkFunction::Log => {
1158 let e = log_link_solver_exp(eta)?;
1166 Ok(InverseLinkJet {
1167 mu: e,
1168 d1: e,
1169 d2: e,
1170 d3: e,
1171 })
1172 }
1173 LinkFunction::Sas => Err(EstimationError::InvalidInput(
1174 "LinkFunction::Sas inverse-link requires explicit SAS link state".to_string(),
1175 )),
1176 LinkFunction::BetaLogistic => Err(EstimationError::InvalidInput(
1177 "LinkFunction::BetaLogistic inverse-link requires explicit Beta-Logistic link state"
1178 .to_string(),
1179 )),
1180 }
1181 }
1182}
1183
1184impl InverseLinkKernel for SasLinkState {
1185 fn jet(&self, eta: f64) -> Result<InverseLinkJet, EstimationError> {
1186 sas_inverse_link_jet(eta, self.epsilon, self.log_delta)
1187 }
1188
1189 fn param_partials(&self, eta: f64) -> Result<Option<LinkParamPartials>, EstimationError> {
1190 Ok(Some(LinkParamPartials::Sas(
1191 sas_inverse_link_jetwith_param_partials(eta, self.epsilon, self.log_delta)?,
1192 )))
1193 }
1194}
1195
1196#[derive(Clone, Copy, Debug)]
1197pub struct BetaLogisticKernel {
1198 pub log_shape_center: f64,
1201 pub epsilon: f64,
1202}
1203
1204impl InverseLinkKernel for BetaLogisticKernel {
1205 fn jet(&self, eta: f64) -> Result<InverseLinkJet, EstimationError> {
1206 Ok(beta_logistic_inverse_link_jet(
1207 eta,
1208 self.log_shape_center,
1209 self.epsilon,
1210 ))
1211 }
1212
1213 fn param_partials(&self, eta: f64) -> Result<Option<LinkParamPartials>, EstimationError> {
1214 Ok(Some(LinkParamPartials::Sas(
1215 beta_logistic_inverse_link_jetwith_param_partials(
1216 eta,
1217 self.log_shape_center,
1218 self.epsilon,
1219 ),
1220 )))
1221 }
1222}
1223
1224impl InverseLinkKernel for MixtureLinkState {
1225 fn jet(&self, eta: f64) -> Result<InverseLinkJet, EstimationError> {
1226 Ok(mixture_inverse_link_jet(self, eta))
1227 }
1228
1229 fn param_partials(&self, eta: f64) -> Result<Option<LinkParamPartials>, EstimationError> {
1230 Ok(Some(LinkParamPartials::Mixture(
1231 mixture_inverse_link_jetwith_rho_partials(self, eta),
1232 )))
1233 }
1234}
1235
1236impl InverseLinkKernel for InverseLink {
1237 fn jet(&self, eta: f64) -> Result<InverseLinkJet, EstimationError> {
1238 match self {
1239 InverseLink::Standard(StandardLink::Logit) => LogitLinkKernel.jet(eta),
1240 InverseLink::Standard(StandardLink::Probit) => ProbitLinkKernel.jet(eta),
1241 InverseLink::Standard(StandardLink::CLogLog) => CLogLogLinkKernel.jet(eta),
1242 InverseLink::Standard(StandardLink::LogLog) => LogLogLinkKernel.jet(eta),
1243 InverseLink::Standard(StandardLink::Cauchit) => CauchitLinkKernel.jet(eta),
1244 InverseLink::Standard(StandardLink::Identity) => LinkFunction::Identity.jet(eta),
1245 InverseLink::Standard(StandardLink::Log) => LinkFunction::Log.jet(eta),
1246 InverseLink::LatentCLogLog(state) => latent_cloglog_point_jet(state, eta),
1247 InverseLink::Sas(state) => state.jet(eta),
1248 InverseLink::BetaLogistic(state) => BetaLogisticKernel {
1249 log_shape_center: state.log_delta,
1250 epsilon: state.epsilon,
1251 }
1252 .jet(eta),
1253 InverseLink::Mixture(state) => state.jet(eta),
1254 }
1255 }
1256
1257 fn param_partials(&self, eta: f64) -> Result<Option<LinkParamPartials>, EstimationError> {
1258 match self {
1259 InverseLink::Standard(_) => Ok(None),
1260 InverseLink::LatentCLogLog(_) => Ok(None),
1261 InverseLink::Sas(state) => state.param_partials(eta),
1262 InverseLink::BetaLogistic(state) => BetaLogisticKernel {
1263 log_shape_center: state.log_delta,
1264 epsilon: state.epsilon,
1265 }
1266 .param_partials(eta),
1267 InverseLink::Mixture(state) => state.param_partials(eta),
1268 }
1269 }
1270}
1271
1272pub fn inverse_link_jet_for_inverse_link(
1279 link: &InverseLink,
1280 eta: f64,
1281) -> Result<InverseLinkJet, EstimationError> {
1282 link.jet(eta)
1283}
1284
1285pub fn inverse_link_mu_d1_for_inverse_link(
1296 link: &InverseLink,
1297 eta: f64,
1298) -> Result<(f64, f64), EstimationError> {
1299 match link {
1300 InverseLink::Standard(link_fn) => Ok(link_function_mu_d1(link_fn.as_link_function(), eta)?),
1301 InverseLink::LatentCLogLog(state) => {
1302 let jet = latent_cloglog_point_jet(state, eta)?;
1303 Ok((jet.mu, jet.d1))
1304 }
1305 InverseLink::Sas(state) => sas_inverse_link_mu_d1(eta, state.epsilon, state.log_delta),
1306 InverseLink::BetaLogistic(state) => Ok(beta_logistic_inverse_link_mu_d1(
1307 eta,
1308 state.log_delta,
1309 state.epsilon,
1310 )),
1311 InverseLink::Mixture(state) => Ok(mixture_inverse_link_mu_d1(state, eta)),
1312 }
1313}
1314
1315pub(crate) fn inverse_link_complement_for_inverse_link(
1330 link: &InverseLink,
1331 eta: f64,
1332 mu: f64,
1333) -> f64 {
1334 let raw = match link {
1335 InverseLink::Standard(link_fn) => standard_link_complement(*link_fn, eta, mu),
1336 InverseLink::Sas(state) => sas_link_complement(eta, state.epsilon, state.log_delta, mu),
1337 InverseLink::BetaLogistic(state) => {
1338 beta_logistic_link_complement(eta, state.log_delta, state.epsilon, mu)
1339 }
1340 InverseLink::Mixture(state) => mixture_link_complement(state, eta, mu),
1341 InverseLink::LatentCLogLog(_) => 1.0 - mu,
1349 };
1350 if raw.is_nan() {
1351 raw
1352 } else {
1353 raw.clamp(0.0, 1.0)
1354 }
1355}
1356
1357#[inline]
1359fn standard_link_complement(link: StandardLink, eta: f64, mu: f64) -> f64 {
1360 match link {
1361 StandardLink::Probit => {
1362 if eta.is_nan() {
1365 f64::NAN
1366 } else if eta == f64::INFINITY {
1367 0.0
1368 } else if eta == f64::NEG_INFINITY {
1369 1.0
1370 } else {
1371 normal_cdf(-eta)
1372 }
1373 }
1374 StandardLink::CLogLog => {
1375 if eta.is_nan() {
1377 f64::NAN
1378 } else {
1379 let t = eta.exp();
1380 if !t.is_finite() { 0.0 } else { (-t).exp() }
1381 }
1382 }
1383 StandardLink::LogLog => {
1384 if eta.is_nan() {
1386 f64::NAN
1387 } else {
1388 let r = (-eta).exp();
1389 if !r.is_finite() { 1.0 } else { -(-r).exp_m1() }
1390 }
1391 }
1392 StandardLink::Cauchit => {
1393 if eta.is_nan() {
1397 f64::NAN
1398 } else if !eta.is_finite() {
1399 if eta > 0.0 { 0.0 } else { 1.0 }
1400 } else if eta > 0.0 {
1401 (1.0 / eta).atan() / std::f64::consts::PI
1402 } else {
1403 0.5 - eta.atan() / std::f64::consts::PI
1404 }
1405 }
1406 StandardLink::Logit | StandardLink::Identity | StandardLink::Log => 1.0 - mu,
1410 }
1411}
1412
1413#[inline]
1424fn beta_logistic_link_complement(eta: f64, log_delta: f64, epsilon: f64, mu: f64) -> f64 {
1425 let logistic = logistic_uwith_derivatives(eta);
1426 if logistic.ln_u.is_nan() || logistic.ln_one_minus_u.is_nan() {
1427 return f64::NAN;
1428 }
1429 if logistic.ln_u == f64::NEG_INFINITY {
1430 return 1.0;
1431 }
1432 if logistic.ln_one_minus_u == f64::NEG_INFINITY {
1433 return 0.0;
1434 }
1435 let (a, b) = beta_logistic_shapes(log_delta, epsilon);
1436 if logistic.use_upper_tail {
1437 beta_reg(b, a, logistic.one_minus_u)
1438 } else {
1439 1.0 - mu
1440 }
1441}
1442
1443#[inline]
1452fn mixture_link_complement(state: &MixtureLinkState, eta: f64, mu: f64) -> f64 {
1453 let k = state.components.len().min(state.pi.len());
1454 let mut weight_total = 0.0_f64;
1455 let mut complement = 0.0_f64;
1456 for i in 0..k {
1457 let (mu_i, _) = component_inverse_link_mu_d1(state.components[i], eta);
1458 if mu_i.is_nan() {
1459 return f64::NAN;
1460 }
1461 let component_complement =
1462 standard_link_complement(state.components[i].as_standard_link(), eta, mu_i);
1463 if component_complement.is_nan() {
1464 return f64::NAN;
1465 }
1466 weight_total += state.pi[i];
1467 complement += state.pi[i] * component_complement;
1468 }
1469 if k == 0 {
1470 return 1.0 - mu;
1471 }
1472 (1.0 - weight_total) + complement
1473}
1474
1475#[inline]
1493pub(crate) fn sas_link_complement(eta: f64, epsilon: f64, log_delta: f64, mu: f64) -> f64 {
1494 let eta = match finite_inverse_link_eta("SAS inverse link complement", eta) {
1495 Ok(value) => value,
1496 Err(_) => return 1.0 - mu,
1497 };
1498 let delta = sas_delta_from_raw_log_delta(log_delta);
1499 if epsilon.abs() < 1e-12 && (delta - 1.0).abs() < 1e-12 {
1500 return standard_link_complement(StandardLink::Probit, eta, mu);
1501 }
1502 let u_raw = delta * asinh_jet5(eta).value + epsilon;
1503 let u = smooth_bound_jet(u_raw, SAS_U_CLAMP).g;
1504 normal_cdf(-u.sinh())
1505}
1506
1507pub(crate) fn sas_latent_probit_argument(
1531 eta: f64,
1532 epsilon: f64,
1533 log_delta: f64,
1534) -> Result<(f64, f64), EstimationError> {
1535 let eta = finite_inverse_link_eta("SAS inverse link", eta)?;
1536 let delta = sas_delta_from_raw_log_delta(log_delta);
1537 if epsilon.abs() < 1e-12 && (delta - 1.0).abs() < 1e-12 {
1538 return Ok((eta, 1.0));
1539 }
1540 let asinh = asinh_jet5(eta);
1541 let u_raw = delta * asinh.value + epsilon;
1542 let sb = smooth_bound_jet(u_raw, SAS_U_CLAMP);
1543 let u = sb.g;
1544 let c = u.cosh();
1545 let r1 = delta * asinh.d1;
1546 let u1 = sb.d1 * r1;
1547 Ok((u.sinh(), c * u1))
1548}
1549
1550fn link_function_mu_d1(link: LinkFunction, eta: f64) -> Result<(f64, f64), EstimationError> {
1551 match link {
1552 LinkFunction::Identity => Ok((eta, 1.0)),
1553 LinkFunction::Log => {
1554 let e = log_link_solver_exp(eta)?;
1557 Ok((e, e))
1558 }
1559 LinkFunction::Logit => Ok(component_inverse_link_mu_d1(LinkComponent::Logit, eta)),
1560 LinkFunction::Probit => Ok(component_inverse_link_mu_d1(LinkComponent::Probit, eta)),
1561 LinkFunction::CLogLog => Ok(component_inverse_link_mu_d1(LinkComponent::CLogLog, eta)),
1562 LinkFunction::LogLog => Ok(component_inverse_link_mu_d1(LinkComponent::LogLog, eta)),
1563 LinkFunction::Cauchit => Ok(component_inverse_link_mu_d1(LinkComponent::Cauchit, eta)),
1564 LinkFunction::Sas => Err(EstimationError::InvalidInput(
1565 "LinkFunction::Sas inverse-link requires explicit SAS link state".to_string(),
1566 )),
1567 LinkFunction::BetaLogistic => Err(EstimationError::InvalidInput(
1568 "LinkFunction::BetaLogistic inverse-link requires explicit Beta-Logistic link state"
1569 .to_string(),
1570 )),
1571 }
1572}
1573
1574#[inline]
1575fn component_inverse_link_mu_d1(component: LinkComponent, eta: f64) -> (f64, f64) {
1576 match component {
1582 LinkComponent::Logit => {
1583 let jet = logit_inverse_link_jet5(eta);
1584 (jet.mu, canonicalzero(jet.d1))
1585 }
1586 LinkComponent::Probit => {
1587 if eta.is_nan() {
1588 return (f64::NAN, f64::NAN);
1589 }
1590 if eta == f64::INFINITY {
1591 return (1.0, 0.0);
1592 }
1593 if eta == f64::NEG_INFINITY {
1594 return (0.0, 0.0);
1595 }
1596 let phi = normal_pdf(eta);
1597 (normal_cdf(eta), canonicalzero(phi))
1598 }
1599 LinkComponent::CLogLog => {
1600 if eta.is_nan() {
1601 return (f64::NAN, f64::NAN);
1602 }
1603 let t = eta.exp();
1604 if !t.is_finite() {
1605 return (1.0, 0.0);
1606 }
1607 (
1608 -(-t).exp_m1(),
1609 canonicalzero(stable_nonnegative_poly_times_exp_neg(t, &[0.0, 1.0])),
1610 )
1611 }
1612 LinkComponent::LogLog => {
1613 if eta.is_nan() {
1614 return (f64::NAN, f64::NAN);
1615 }
1616 let r = (-eta).exp();
1617 if !r.is_finite() {
1618 return (0.0, 0.0);
1619 }
1620 (
1621 (-r).exp(),
1622 canonicalzero(stable_nonnegative_poly_times_exp_neg(r, &[0.0, 1.0])),
1623 )
1624 }
1625 LinkComponent::Cauchit => {
1626 if eta.is_nan() {
1627 return (f64::NAN, f64::NAN);
1628 }
1629 let den = 1.0 + eta * eta;
1630 let d1 = if eta.is_finite() {
1631 1.0 / (std::f64::consts::PI * den)
1632 } else {
1633 0.0
1634 };
1635 (0.5 + eta.atan() / std::f64::consts::PI, canonicalzero(d1))
1636 }
1637 }
1638}
1639
1640fn sas_inverse_link_mu_d1(
1641 eta: f64,
1642 epsilon: f64,
1643 log_delta: f64,
1644) -> Result<(f64, f64), EstimationError> {
1645 let eta = finite_inverse_link_eta("SAS inverse link", eta)?;
1646 let delta_id = sas_delta_from_raw_log_delta(log_delta);
1647 if epsilon.abs() < 1e-12 && (delta_id - 1.0).abs() < 1e-12 {
1648 return Ok(component_inverse_link_mu_d1(LinkComponent::Probit, eta));
1649 }
1650 let asinh = asinh_jet5(eta);
1651 let delta = delta_id;
1652 let u_raw = delta * asinh.value + epsilon;
1653 let sb = smooth_bound_jet(u_raw, SAS_U_CLAMP);
1654 let u = sb.g;
1655 let g1 = sb.d1;
1656 let s = u.sinh();
1657 let c = u.cosh();
1658 let z = s;
1659 let r1 = delta * asinh.d1;
1660 let u1 = g1 * r1;
1661 let z1 = c * u1;
1662 let base = probit_jet(z);
1665 Ok((base.mu, canonicalzero(base.d1 * z1)))
1666}
1667
1668fn beta_logistic_inverse_link_mu_d1(eta: f64, delta: f64, epsilon: f64) -> (f64, f64) {
1669 let logistic = logistic_uwith_derivatives(eta);
1670 let (a, b) = beta_logistic_shapes(delta, epsilon);
1671 let mu = beta_reg_logistic(a, b, logistic);
1672 let log_d1 = beta_logistic_log_d1(a, b, logistic);
1673 (mu, log_d1.exp())
1674}
1675
1676fn mixture_inverse_link_mu_d1(state: &MixtureLinkState, eta: f64) -> (f64, f64) {
1677 let mut mu = 0.0_f64;
1678 let mut d1 = 0.0_f64;
1679 let k = state.components.len().min(state.pi.len());
1680 for i in 0..k {
1681 let (mu_i, d1_i) = component_inverse_link_mu_d1(state.components[i], eta);
1682 let w = state.pi[i];
1683 mu += w * mu_i;
1684 d1 += w * d1_i;
1685 }
1686 (mu, d1)
1687}
1688
1689#[derive(Clone, Copy)]
1690enum PdfDerivativeOrder {
1691 Third,
1692 Fourth,
1693}
1694
1695impl PdfDerivativeOrder {
1696 fn probit(self, eta: f64) -> f64 {
1697 match self {
1698 Self::Third => probit_pdfthird_derivative(eta),
1699 Self::Fourth => probit_pdffourth_derivative(eta),
1700 }
1701 }
1702
1703 fn component(self, component: LinkComponent, eta: f64) -> f64 {
1704 match self {
1705 Self::Third => component_inverse_link_pdfthird_derivative(component, eta),
1706 Self::Fourth => component_inverse_link_pdffourth_derivative(component, eta),
1707 }
1708 }
1709
1710 fn latent_cloglog(self, eta: f64, latent_sd: f64) -> Result<f64, EstimationError> {
1711 let jet = latent_cloglog_jet5(latent_cloglog_quadctx(), eta, latent_sd)?;
1712 Ok(match self {
1713 Self::Third => jet.d4,
1714 Self::Fourth => jet.d5,
1715 })
1716 }
1717
1718 fn sas(self, eta: f64, epsilon: f64, log_delta: f64) -> Result<f64, EstimationError> {
1719 match self {
1720 Self::Third => sas_inverse_link_pdfthird_derivative(eta, epsilon, log_delta),
1721 Self::Fourth => sas_inverse_link_pdffourth_derivative(eta, epsilon, log_delta),
1722 }
1723 }
1724
1725 fn beta_logistic(self, eta: f64, log_shape_center: f64, epsilon: f64) -> f64 {
1726 match self {
1727 Self::Third => {
1728 beta_logistic_inverse_link_pdfthird_derivative(eta, log_shape_center, epsilon)
1729 }
1730 Self::Fourth => {
1731 beta_logistic_inverse_link_pdffourth_derivative(eta, log_shape_center, epsilon)
1732 }
1733 }
1734 }
1735}
1736
1737fn inverse_link_pdf_derivative_for_inverse_link(
1738 link: &InverseLink,
1739 eta: f64,
1740 order: PdfDerivativeOrder,
1741) -> Result<f64, EstimationError> {
1742 match link {
1743 InverseLink::Standard(StandardLink::Identity) => Ok(0.0),
1744 InverseLink::Standard(StandardLink::Log) => log_link_solver_exp(eta),
1745 InverseLink::Standard(StandardLink::Probit) => Ok(order.probit(eta)),
1746 InverseLink::Standard(StandardLink::Logit) => {
1747 Ok(order.component(LinkComponent::Logit, eta))
1748 }
1749 InverseLink::Standard(StandardLink::CLogLog) => {
1750 Ok(order.component(LinkComponent::CLogLog, eta))
1751 }
1752 InverseLink::Standard(StandardLink::LogLog) => {
1753 Ok(order.component(LinkComponent::LogLog, eta))
1754 }
1755 InverseLink::Standard(StandardLink::Cauchit) => {
1756 Ok(order.component(LinkComponent::Cauchit, eta))
1757 }
1758 InverseLink::LatentCLogLog(state) => order.latent_cloglog(eta, state.latent_sd),
1759 InverseLink::Sas(state) => order.sas(eta, state.epsilon, state.log_delta),
1760 InverseLink::BetaLogistic(state) => {
1761 Ok(order.beta_logistic(eta, state.log_delta, state.epsilon))
1762 }
1763 InverseLink::Mixture(state) => Ok(state
1764 .components
1765 .iter()
1766 .zip(state.pi.iter())
1767 .map(|(&component, &weight)| weight * order.component(component, eta))
1768 .sum()),
1769 }
1770}
1771
1772pub fn inverse_link_pdfthird_derivative_for_inverse_link(
1773 link: &InverseLink,
1774 eta: f64,
1775) -> Result<f64, EstimationError> {
1776 inverse_link_pdf_derivative_for_inverse_link(link, eta, PdfDerivativeOrder::Third)
1794}
1795
1796pub fn inverse_link_pdffourth_derivative_for_inverse_link(
1803 link: &InverseLink,
1804 eta: f64,
1805) -> Result<f64, EstimationError> {
1806 inverse_link_pdf_derivative_for_inverse_link(link, eta, PdfDerivativeOrder::Fourth)
1807}
1808
1809#[inline]
1810fn royston_parmar_inverse_link_jet(eta: f64) -> Result<InverseLinkJet, EstimationError> {
1814 let eta = finite_inverse_link_eta("Royston-Parmar survival inverse link", eta)?;
1815 let hazard = eta.exp();
1816 let survival = (-hazard).exp();
1817 let d1 = -stable_nonnegative_poly_times_exp_neg(hazard, &[0.0, 1.0]);
1822 let d2 = stable_nonnegative_poly_times_exp_neg(hazard, &[0.0, -1.0, 1.0]);
1823 let d3 = stable_nonnegative_poly_times_exp_neg(hazard, &[0.0, -1.0, 3.0, -1.0]);
1824 Ok(InverseLinkJet {
1825 mu: survival,
1826 d1: canonicalzero(d1),
1827 d2: canonicalzero(d2),
1828 d3: canonicalzero(d3),
1829 })
1830}
1831
1832#[inline]
1840fn log_inverse_link_jet_exact(eta: f64) -> InverseLinkJet {
1841 let e = eta.exp();
1842 InverseLinkJet {
1843 mu: e,
1844 d1: e,
1845 d2: e,
1846 d3: e,
1847 }
1848}
1849
1850pub fn inverse_link_jet_for_family_public(
1862 spec: &LikelihoodSpec,
1863 eta: f64,
1864) -> Result<InverseLinkJet, EstimationError> {
1865 if matches!(spec.response, ResponseFamily::RoystonParmar) {
1866 return royston_parmar_inverse_link_jet(eta);
1867 }
1868 if let InverseLink::Standard(StandardLink::Log) = spec.link {
1869 return Ok(log_inverse_link_jet_exact(eta));
1870 }
1871 spec.link.jet(eta)
1872}
1873
1874#[inline]
1875pub fn mixture_inverse_link_jet(state: &MixtureLinkState, eta: f64) -> InverseLinkJet {
1876 let mut mu = 0.0_f64;
1877 let mut d1 = 0.0_f64;
1878 let mut d2 = 0.0_f64;
1879 let mut d3 = 0.0_f64;
1880 let k = state.components.len().min(state.pi.len());
1881 for i in 0..k {
1882 let jet = component_inverse_link_jet(state.components[i], eta);
1883 let w = state.pi[i];
1884 mu += w * jet.mu;
1885 d1 += w * jet.d1;
1886 d2 += w * jet.d2;
1887 d3 += w * jet.d3;
1888 }
1889 InverseLinkJet { mu, d1, d2, d3 }
1890}
1891
1892pub fn mixture_inverse_link_jetwith_rho_partials(
1900 state: &MixtureLinkState,
1901 eta: f64,
1902) -> MixtureJetWithRhoPartials {
1903 let k = state.components.len().min(state.pi.len());
1904 let m = k.saturating_sub(1);
1905 let mut djet_drho = vec![
1906 InverseLinkJet {
1907 mu: 0.0,
1908 d1: 0.0,
1909 d2: 0.0,
1910 d3: 0.0,
1911 };
1912 m
1913 ];
1914 let jet = mixture_inverse_link_jetwith_rho_partials_into(state, eta, &mut djet_drho);
1915 let mut d2mu_drho2 = Array2::<f64>::zeros((m, m));
1925 let mut d2d1_drho2 = Array2::<f64>::zeros((m, m));
1926 let mut d2d2_drho2 = Array2::<f64>::zeros((m, m));
1927 for j in 0..m {
1928 for k in j..m {
1929 let diagonal = if j == k { 1.0 } else { 0.0 };
1930 let mu = (diagonal - state.pi[k]) * djet_drho[j].mu
1931 - state.pi[j] * djet_drho[k].mu;
1932 let d1 = (diagonal - state.pi[k]) * djet_drho[j].d1
1933 - state.pi[j] * djet_drho[k].d1;
1934 let d2 = (diagonal - state.pi[k]) * djet_drho[j].d2
1935 - state.pi[j] * djet_drho[k].d2;
1936 d2mu_drho2[[j, k]] = mu;
1937 d2mu_drho2[[k, j]] = mu;
1938 d2d1_drho2[[j, k]] = d1;
1939 d2d1_drho2[[k, j]] = d1;
1940 d2d2_drho2[[j, k]] = d2;
1941 d2d2_drho2[[k, j]] = d2;
1942 }
1943 }
1944 MixtureJetWithRhoPartials {
1945 jet,
1946 djet_drho,
1947 d2mu_drho2,
1948 d2d1_drho2,
1949 d2d2_drho2,
1950 }
1951}
1952
1953pub fn mixture_inverse_link_jetwith_rho_partials_into(
1956 state: &MixtureLinkState,
1957 eta: f64,
1958 out: &mut [InverseLinkJet],
1959) -> InverseLinkJet {
1960 let k = state.components.len().min(state.pi.len());
1961 let m = k.saturating_sub(1);
1962 assert!(
1963 out.len() >= m,
1964 "rho-partial output buffer too small: got {}, need {}",
1965 out.len(),
1966 m
1967 );
1968 let mut mixed = InverseLinkJet {
1969 mu: 0.0,
1970 d1: 0.0,
1971 d2: 0.0,
1972 d3: 0.0,
1973 };
1974 for i in 0..k {
1975 let jet_i = component_inverse_link_jet(state.components[i], eta);
1976 let w = state.pi[i];
1977 mixed.mu += w * jet_i.mu;
1978 mixed.d1 += w * jet_i.d1;
1979 mixed.d2 += w * jet_i.d2;
1980 mixed.d3 += w * jet_i.d3;
1981 if i < m {
1984 out[i] = jet_i;
1985 }
1986 }
1987 for j in 0..m {
1988 let pi_j = state.pi[j];
1989 let cj = out[j];
1990 out[j] = InverseLinkJet {
1991 mu: pi_j * (cj.mu - mixed.mu),
1992 d1: pi_j * (cj.d1 - mixed.d1),
1993 d2: pi_j * (cj.d2 - mixed.d2),
1994 d3: pi_j * (cj.d3 - mixed.d3),
1995 };
1996 }
1997 mixed
1998}
1999
2000#[derive(Clone, Copy)]
2001struct LogisticU {
2002 u: f64,
2003 one_minus_u: f64,
2004 ln_u: f64,
2005 ln_one_minus_u: f64,
2006 du: f64,
2007 use_upper_tail: bool,
2008}
2009
2010#[inline]
2011fn logistic_uwith_derivatives(eta: f64) -> LogisticU {
2012 let ln_u = -gam_linalg::utils::stable_softplus(-eta);
2013 let ln_one_minus_u = -gam_linalg::utils::stable_softplus(eta);
2014 let u = ln_u.exp();
2015 let one_minus_u = ln_one_minus_u.exp();
2016 let du = (ln_u + ln_one_minus_u).exp();
2017 LogisticU {
2018 u,
2019 one_minus_u,
2020 ln_u,
2021 ln_one_minus_u,
2022 du,
2023 use_upper_tail: eta >= 0.0,
2024 }
2025}
2026
2027#[inline]
2028fn beta_reg_logistic(a: f64, b: f64, logistic: LogisticU) -> f64 {
2029 if logistic.ln_u.is_nan() || logistic.ln_one_minus_u.is_nan() {
2030 return f64::NAN;
2031 }
2032 if logistic.ln_u == f64::NEG_INFINITY {
2033 return 0.0;
2034 }
2035 if logistic.ln_one_minus_u == f64::NEG_INFINITY {
2036 return 1.0;
2037 }
2038 if logistic.use_upper_tail {
2039 1.0 - beta_reg(b, a, logistic.one_minus_u)
2040 } else {
2041 beta_reg(a, b, logistic.u)
2042 }
2043}
2044
2045#[derive(Clone, Copy)]
2046struct BetaShapePartials {
2047 value: f64,
2048 da: f64,
2049 db: f64,
2050 daa: f64,
2051 dab: f64,
2052 dbb: f64,
2053}
2054
2055impl BetaShapePartials {
2056 #[inline]
2057 fn constant(value: f64) -> Self {
2058 Self {
2059 value,
2060 da: 0.0,
2061 db: 0.0,
2062 daa: 0.0,
2063 dab: 0.0,
2064 dbb: 0.0,
2065 }
2066 }
2067}
2068
2069#[inline]
2070fn beta_reg_with_shape_partials_logistic(
2071 a: f64,
2072 b: f64,
2073 logistic: LogisticU,
2074) -> BetaShapePartials {
2075 if logistic.ln_u.is_nan() || logistic.ln_one_minus_u.is_nan() {
2076 return BetaShapePartials {
2077 value: f64::NAN,
2078 da: f64::NAN,
2079 db: f64::NAN,
2080 daa: f64::NAN,
2081 dab: f64::NAN,
2082 dbb: f64::NAN,
2083 };
2084 }
2085 if logistic.use_upper_tail {
2086 let tail = beta_reg_with_shape_partials(b, a, logistic.one_minus_u);
2087 BetaShapePartials {
2088 value: 1.0 - tail.value,
2089 da: -tail.db,
2090 db: -tail.da,
2091 daa: -tail.dbb,
2092 dab: -tail.dab,
2093 dbb: -tail.daa,
2094 }
2095 } else {
2096 beta_reg_with_shape_partials(a, b, logistic.u)
2097 }
2098}
2099
2100#[inline]
2101fn beta_logistic_log_d1(a: f64, b: f64, logistic: LogisticU) -> f64 {
2102 a * logistic.ln_u + b * logistic.ln_one_minus_u - ln_beta(a, b)
2103}
2104
2105#[derive(Clone, Copy)]
2106struct ShapeDual {
2107 v: f64,
2108 da: f64,
2109 db: f64,
2110 daa: f64,
2111 dab: f64,
2112 dbb: f64,
2113}
2114
2115impl ShapeDual {
2116 #[inline]
2117 fn constant(v: f64) -> Self {
2118 Self {
2119 v,
2120 da: 0.0,
2121 db: 0.0,
2122 daa: 0.0,
2123 dab: 0.0,
2124 dbb: 0.0,
2125 }
2126 }
2127
2128 #[inline]
2129 fn from_value_partials(v: f64, da: f64, db: f64) -> Self {
2130 Self {
2131 v,
2132 da,
2133 db,
2134 daa: 0.0,
2135 dab: 0.0,
2136 dbb: 0.0,
2137 }
2138 }
2139
2140 #[inline]
2141 fn clamp_small(self, floor: f64) -> Self {
2142 if self.v.abs() < floor {
2143 Self::constant(floor)
2144 } else {
2145 self
2146 }
2147 }
2148}
2149
2150impl std::ops::Add for ShapeDual {
2151 type Output = Self;
2152
2153 #[inline]
2154 fn add(self, rhs: Self) -> Self {
2155 Self {
2156 v: self.v + rhs.v,
2157 da: self.da + rhs.da,
2158 db: self.db + rhs.db,
2159 daa: self.daa + rhs.daa,
2160 dab: self.dab + rhs.dab,
2161 dbb: self.dbb + rhs.dbb,
2162 }
2163 }
2164}
2165
2166impl std::ops::Sub for ShapeDual {
2167 type Output = Self;
2168
2169 #[inline]
2170 fn sub(self, rhs: Self) -> Self {
2171 Self {
2172 v: self.v - rhs.v,
2173 da: self.da - rhs.da,
2174 db: self.db - rhs.db,
2175 daa: self.daa - rhs.daa,
2176 dab: self.dab - rhs.dab,
2177 dbb: self.dbb - rhs.dbb,
2178 }
2179 }
2180}
2181
2182impl std::ops::Mul for ShapeDual {
2183 type Output = Self;
2184
2185 #[inline]
2186 fn mul(self, rhs: Self) -> Self {
2187 Self {
2188 v: self.v * rhs.v,
2189 da: self.da * rhs.v + self.v * rhs.da,
2190 db: self.db * rhs.v + self.v * rhs.db,
2191 daa: self.daa * rhs.v + 2.0 * self.da * rhs.da + self.v * rhs.daa,
2192 dab: self.dab * rhs.v
2193 + self.da * rhs.db
2194 + self.db * rhs.da
2195 + self.v * rhs.dab,
2196 dbb: self.dbb * rhs.v + 2.0 * self.db * rhs.db + self.v * rhs.dbb,
2197 }
2198 }
2199}
2200
2201impl std::ops::Div for ShapeDual {
2202 type Output = Self;
2203
2204 #[inline]
2205 fn div(self, rhs: Self) -> Self {
2206 let inv = 1.0 / rhs.v;
2207 let inv2 = inv * inv;
2208 let inv3 = inv2 * inv;
2209 let reciprocal = Self {
2210 v: inv,
2211 da: -rhs.da * inv2,
2212 db: -rhs.db * inv2,
2213 daa: 2.0 * rhs.da * rhs.da * inv3 - rhs.daa * inv2,
2214 dab: 2.0 * rhs.da * rhs.db * inv3 - rhs.dab * inv2,
2215 dbb: 2.0 * rhs.db * rhs.db * inv3 - rhs.dbb * inv2,
2216 };
2217 self * reciprocal
2218 }
2219}
2220
2221impl std::ops::Neg for ShapeDual {
2222 type Output = Self;
2223
2224 #[inline]
2225 fn neg(self) -> Self {
2226 ShapeDual {
2227 v: -self.v,
2228 da: -self.da,
2229 db: -self.db,
2230 daa: -self.daa,
2231 dab: -self.dab,
2232 dbb: -self.dbb,
2233 }
2234 }
2235}
2236
2237#[inline]
2238fn shape_dual(v: f64) -> ShapeDual {
2239 ShapeDual::constant(v)
2240}
2241
2242fn beta_reg_with_shape_partials(a0: f64, b0: f64, x0: f64) -> BetaShapePartials {
2246 if x0 <= 0.0 {
2247 return BetaShapePartials::constant(0.0);
2248 }
2249 if x0 >= 1.0 {
2250 return BetaShapePartials::constant(1.0);
2251 }
2252
2253 let symm_transform = x0 >= (a0 + 1.0) / (a0 + b0 + 2.0);
2254 let (a, b, x) = if symm_transform {
2255 (
2256 ShapeDual::from_value_partials(b0, 0.0, 1.0),
2257 ShapeDual::from_value_partials(a0, 1.0, 0.0),
2258 1.0 - x0,
2259 )
2260 } else {
2261 (
2262 ShapeDual::from_value_partials(a0, 1.0, 0.0),
2263 ShapeDual::from_value_partials(b0, 0.0, 1.0),
2264 x0,
2265 )
2266 };
2267
2268 let ln_x = x.ln();
2269 let ln_1mx = (1.0 - x).ln();
2270 let psi_ab = digamma(a.v + b.v);
2271 let log_bt = statrs::function::gamma::ln_gamma(a.v + b.v)
2272 - statrs::function::gamma::ln_gamma(a.v)
2273 - statrs::function::gamma::ln_gamma(b.v)
2274 + a.v * ln_x
2275 + b.v * ln_1mx;
2276 let bt_v = log_bt.exp();
2277 let log_bt_a = psi_ab - digamma(a.v) + ln_x;
2278 let log_bt_b = psi_ab - digamma(b.v) + ln_1mx;
2279 let trigamma_ab = trigamma(a.v + b.v);
2280 let log_bt_aa = trigamma_ab - trigamma(a.v);
2281 let log_bt_ab = trigamma_ab;
2282 let log_bt_bb = trigamma_ab - trigamma(b.v);
2283 let log_bt_da = log_bt_a * a.da + log_bt_b * b.da;
2284 let log_bt_db = log_bt_a * a.db + log_bt_b * b.db;
2285 let log_bt_daa = log_bt_aa * a.da * a.da
2286 + 2.0 * log_bt_ab * a.da * b.da
2287 + log_bt_bb * b.da * b.da
2288 + log_bt_a * a.daa
2289 + log_bt_b * b.daa;
2290 let log_bt_dab = log_bt_aa * a.da * a.db
2291 + log_bt_ab * (a.da * b.db + b.da * a.db)
2292 + log_bt_bb * b.da * b.db
2293 + log_bt_a * a.dab
2294 + log_bt_b * b.dab;
2295 let log_bt_dbb = log_bt_aa * a.db * a.db
2296 + 2.0 * log_bt_ab * a.db * b.db
2297 + log_bt_bb * b.db * b.db
2298 + log_bt_a * a.dbb
2299 + log_bt_b * b.dbb;
2300 let bt = ShapeDual {
2301 v: bt_v,
2302 da: bt_v * log_bt_da,
2303 db: bt_v * log_bt_db,
2304 daa: bt_v * (log_bt_da * log_bt_da + log_bt_daa),
2305 dab: bt_v * (log_bt_da * log_bt_db + log_bt_dab),
2306 dbb: bt_v * (log_bt_db * log_bt_db + log_bt_dbb),
2307 };
2308
2309 let eps = 0.00000000000000011102230246251565;
2310 let fpmin = f64::MIN_POSITIVE / eps;
2311 let one = shape_dual(1.0);
2312 let qab = a + b;
2313 let qap = a + one;
2314 let qam = a - one;
2315 let mut c = one;
2316 let mut d = (one - qab * shape_dual(x) / qap).clamp_small(fpmin);
2317 d = one / d;
2318 let mut h = d;
2319
2320 for m in 1..141 {
2321 let mf = f64::from(m);
2322 let m2 = mf * 2.0;
2323 let md = shape_dual(mf);
2324 let m2d = shape_dual(m2);
2325 let mut aa = md * (b - md) * shape_dual(x) / ((qam + m2d) * (a + m2d));
2326 d = (one + aa * d).clamp_small(fpmin);
2327 c = (one + aa / c).clamp_small(fpmin);
2328 d = one / d;
2329 h = h * d * c;
2330
2331 aa = (a + md).neg() * (qab + md) * shape_dual(x) / ((a + m2d) * (qap + m2d));
2332 d = (one + aa * d).clamp_small(fpmin);
2333 c = (one + aa / c).clamp_small(fpmin);
2334 d = one / d;
2335 let del = d * c;
2336 h = h * del;
2337
2338 if (del.v - 1.0).abs() <= eps {
2339 let reg = bt * h / a;
2340 return if symm_transform {
2341 BetaShapePartials {
2342 value: 1.0 - reg.v,
2343 da: -reg.da,
2344 db: -reg.db,
2345 daa: -reg.daa,
2346 dab: -reg.dab,
2347 dbb: -reg.dbb,
2348 }
2349 } else {
2350 BetaShapePartials {
2351 value: reg.v,
2352 da: reg.da,
2353 db: reg.db,
2354 daa: reg.daa,
2355 dab: reg.dab,
2356 dbb: reg.dbb,
2357 }
2358 };
2359 }
2360 }
2361 let reg = bt * h / a;
2362 if symm_transform {
2363 BetaShapePartials {
2364 value: 1.0 - reg.v,
2365 da: -reg.da,
2366 db: -reg.db,
2367 daa: -reg.daa,
2368 dab: -reg.dab,
2369 dbb: -reg.dbb,
2370 }
2371 } else {
2372 BetaShapePartials {
2373 value: reg.v,
2374 da: reg.da,
2375 db: reg.db,
2376 daa: reg.daa,
2377 dab: reg.dab,
2378 dbb: reg.dbb,
2379 }
2380 }
2381}
2382
2383#[inline]
2406fn beta_logistic_shape_jets(
2407 log_shape_center: f64,
2408 epsilon: f64,
2409) -> (f64, f64, f64, f64, f64, f64) {
2410 let gs = smooth_bound_jet(log_shape_center - epsilon, BETA_LOGISTIC_LOG_SHAPE_BOUND);
2411 let gt = smooth_bound_jet(log_shape_center + epsilon, BETA_LOGISTIC_LOG_SHAPE_BOUND);
2412 let a = gs.g.exp();
2413 let b = gt.g.exp();
2414 (
2415 a,
2416 b,
2417 a * gs.d1,
2418 b * gt.d1,
2419 a * (gs.d1 * gs.d1 + gs.d2),
2420 b * (gt.d1 * gt.d1 + gt.d2),
2421 )
2422}
2423
2424#[inline]
2426fn beta_logistic_shapes(log_shape_center: f64, epsilon: f64) -> (f64, f64) {
2427 let (a, b, ..) = beta_logistic_shape_jets(log_shape_center, epsilon);
2428 (a, b)
2429}
2430
2431pub fn beta_logistic_inverse_link_jet(
2432 eta: f64,
2433 log_shape_center: f64,
2434 epsilon: f64,
2435) -> InverseLinkJet {
2436 let logistic = logistic_uwith_derivatives(eta);
2437 let (a, b) = beta_logistic_shapes(log_shape_center, epsilon);
2438 let mu = beta_reg_logistic(a, b, logistic);
2439 let log_d1 = beta_logistic_log_d1(a, b, logistic);
2440 let d1 = log_d1.exp();
2441 let t = a * logistic.one_minus_u - b * logistic.u;
2442 let d2 = d1 * t;
2443 let d3 = d1 * (t * t - (a + b) * logistic.du);
2444 InverseLinkJet { mu, d1, d2, d3 }
2445}
2446
2447pub fn beta_logistic_inverse_link_pdfthird_derivative(
2448 eta: f64,
2449 log_shape_center: f64,
2450 epsilon: f64,
2451) -> f64 {
2452 let logistic = logistic_uwith_derivatives(eta);
2475 let (a, b) = beta_logistic_shapes(log_shape_center, epsilon);
2476 let log_d1 = beta_logistic_log_d1(a, b, logistic);
2477 let d1 = log_d1.exp();
2478 let c = a + b;
2479 let t = a * logistic.one_minus_u - b * logistic.u;
2480 let u2 = logistic.du * (logistic.one_minus_u - logistic.u);
2481 d1 * (t * t * t - 3.0 * c * t * logistic.du - c * u2)
2482}
2483
2484pub fn beta_logistic_inverse_link_pdffourth_derivative(
2492 eta: f64,
2493 log_shape_center: f64,
2494 epsilon: f64,
2495) -> f64 {
2496 let logistic = logistic_uwith_derivatives(eta);
2497 let (a, b) = beta_logistic_shapes(log_shape_center, epsilon);
2498 let log_d1 = beta_logistic_log_d1(a, b, logistic);
2499 let d1 = log_d1.exp();
2500 let c = a + b;
2501 let t = a * logistic.one_minus_u - b * logistic.u;
2502 let u2 = logistic.du * (logistic.one_minus_u - logistic.u);
2503 let u3 = u2 * (logistic.one_minus_u - logistic.u) - 2.0 * logistic.du * logistic.du;
2504 let t2 = t * t;
2505 d1 * (t2 * t2 - 6.0 * c * t2 * logistic.du - 4.0 * c * t * u2
2506 + 3.0 * c * c * logistic.du * logistic.du
2507 - c * u3)
2508}
2509
2510pub fn beta_logistic_inverse_link_jetwith_param_partials(
2511 eta: f64,
2512 log_shape_center: f64,
2513 epsilon: f64,
2514) -> SasJetWithParamPartials {
2515 let logistic = logistic_uwith_derivatives(eta);
2516 let (a, b, da, db, daa, dbb) = beta_logistic_shape_jets(log_shape_center, epsilon);
2520 let shape = beta_reg_with_shape_partials_logistic(a, b, logistic);
2521 let mu = shape.value;
2522 let dmu_dlog_shape_center = da * shape.da + db * shape.db;
2523 let dmu_depsilon = -da * shape.da + db * shape.db;
2524 let log_d1 = beta_logistic_log_d1(a, b, logistic);
2525 let d1 = log_d1.exp();
2526 let t = a * logistic.one_minus_u - b * logistic.u;
2527 let d2 = d1 * t;
2528 let k = t * t - (a + b) * logistic.du;
2529 let d3 = d1 * k;
2530 let jet = InverseLinkJet { mu, d1, d2, d3 };
2531
2532 let psi_a = digamma(a);
2533 let psi_b = digamma(b);
2534 let psi_ab = digamma(a + b);
2535 let la = logistic.ln_u - psi_a + psi_ab;
2536 let lb = logistic.ln_one_minus_u - psi_b + psi_ab;
2537
2538 let partials_for = |a_p: f64, b_p: f64, dmu: f64| -> InverseLinkJet {
2539 let logd1_p = a_p * la + b_p * lb;
2540 let d1_p = d1 * logd1_p;
2541 let t_p = a_p * logistic.one_minus_u - b_p * logistic.u;
2542 let d2_p = d1_p * t + d1 * t_p;
2543 let k_p = 2.0 * t * t_p - (a_p + b_p) * logistic.du;
2544 let d3_p = d1_p * k + d1 * k_p;
2545 InverseLinkJet {
2546 mu: dmu,
2547 d1: d1_p,
2548 d2: d2_p,
2549 d3: d3_p,
2550 }
2551 };
2552 let djet_dlog_shape_center = partials_for(da, db, dmu_dlog_shape_center);
2553 let djet_depsilon = partials_for(-da, db, dmu_depsilon);
2554 let a_first = [-da, da];
2560 let b_first = [db, db];
2561 let a_second = [[daa, -daa], [-daa, daa]];
2562 let b_second = [[dbb, dbb], [dbb, dbb]];
2563 let logd1_first = [
2564 a_first[0] * la + b_first[0] * lb,
2565 a_first[1] * la + b_first[1] * lb,
2566 ];
2567 let trigamma_ab = trigamma(a + b);
2568 let la_a = trigamma_ab - trigamma(a);
2569 let la_b = trigamma_ab;
2570 let lb_a = trigamma_ab;
2571 let lb_b = trigamma_ab - trigamma(b);
2572 let t_first = [
2575 a_first[0] * logistic.one_minus_u - b_first[0] * logistic.u,
2576 a_first[1] * logistic.one_minus_u - b_first[1] * logistic.u,
2577 ];
2578 let mut d2mu_dparams2 = Array2::<f64>::zeros((2, 2));
2579 let mut d2d1_dparams2 = Array2::<f64>::zeros((2, 2));
2580 let mut d2d2_dparams2 = Array2::<f64>::zeros((2, 2));
2581 for j in 0..2 {
2582 for k in j..2 {
2583 let mu_jk = shape.daa * a_first[j] * a_first[k]
2584 + shape.dab
2585 * (a_first[j] * b_first[k] + b_first[j] * a_first[k])
2586 + shape.dbb * b_first[j] * b_first[k]
2587 + shape.da * a_second[j][k]
2588 + shape.db * b_second[j][k];
2589 let logd1_jk = la_a * a_first[j] * a_first[k]
2590 + la_b * a_first[j] * b_first[k]
2591 + lb_a * b_first[j] * a_first[k]
2592 + lb_b * b_first[j] * b_first[k]
2593 + la * a_second[j][k]
2594 + lb * b_second[j][k];
2595 let d1_jk = d1 * (logd1_first[j] * logd1_first[k] + logd1_jk);
2596 let t_jk = a_second[j][k] * logistic.one_minus_u - b_second[j][k] * logistic.u;
2599 let d2_jk = d1_jk * t
2600 + d1 * logd1_first[j] * t_first[k]
2601 + d1 * logd1_first[k] * t_first[j]
2602 + d1 * t_jk;
2603 d2mu_dparams2[[j, k]] = mu_jk;
2604 d2mu_dparams2[[k, j]] = mu_jk;
2605 d2d1_dparams2[[j, k]] = d1_jk;
2606 d2d1_dparams2[[k, j]] = d1_jk;
2607 d2d2_dparams2[[j, k]] = d2_jk;
2608 d2d2_dparams2[[k, j]] = d2_jk;
2609 }
2610 }
2611 SasJetWithParamPartials {
2612 jet,
2613 djet_depsilon,
2614 djet_dlog_delta: djet_dlog_shape_center,
2615 d2mu_dparams2,
2616 d2d1_dparams2,
2617 d2d2_dparams2,
2618 }
2619}
2620
2621pub fn sas_inverse_link_jet(
2633 eta: f64,
2634 epsilon: f64,
2635 log_delta: f64,
2636) -> Result<InverseLinkJet, EstimationError> {
2637 let eta = finite_inverse_link_eta("SAS inverse link", eta)?;
2638 let delta_id = sas_delta_from_raw_log_delta(log_delta);
2639 if epsilon.abs() < 1e-12 && (delta_id - 1.0).abs() < 1e-12 {
2640 return Ok(component_inverse_link_jet(LinkComponent::Probit, eta));
2641 }
2642 let asinh = asinh_jet5(eta);
2643 let delta = delta_id;
2644 let u_raw = delta * asinh.value + epsilon;
2645 let sb = smooth_bound_jet(u_raw, SAS_U_CLAMP);
2646 let u = sb.g;
2647 let g1 = sb.d1;
2648 let g2 = sb.d2;
2649 let g3 = sb.d3;
2650 let s = u.sinh();
2651 let c = u.cosh();
2652 let z = s;
2653 let r1 = delta * asinh.d1;
2654 let r2 = delta * asinh.d2;
2655 let r3 = delta * asinh.d3;
2656 let u1 = g1 * r1;
2657 let u2 = g2 * r1 * r1 + g1 * r2;
2658 let u3 = g3 * r1 * r1 * r1 + 3.0 * g2 * r1 * r2 + g1 * r3;
2659 let z1 = c * u1;
2660 let z2 = s * u1 * u1 + c * u2;
2661 let z3 = c * u1 * u1 * u1 + 3.0 * s * u1 * u2 + c * u3;
2662 let base = probit_jet(z);
2663 Ok(chain_inverse_link_jet(base, z1, z2, z3))
2664}
2665
2666pub fn sas_inverse_link_pdfthird_derivative(
2669 eta: f64,
2670 epsilon: f64,
2671 log_delta: f64,
2672) -> Result<f64, EstimationError> {
2673 let eta = finite_inverse_link_eta("SAS inverse link", eta)?;
2709 let asinh = asinh_jet5(eta);
2710 let delta = sas_delta_from_raw_log_delta(log_delta);
2711 let u_raw = delta * asinh.value + epsilon;
2712 let sb = smooth_bound_jet(u_raw, SAS_U_CLAMP);
2713 let u = sb.g;
2714 let g1 = sb.d1;
2715 let g2 = sb.d2;
2716 let g3 = sb.d3;
2717 let g4 = sb.d4;
2718 let s = u.sinh();
2719 let c = u.cosh();
2720 let z = s;
2721 let base = probit_jet(z);
2722 let r1 = delta * asinh.d1;
2723 let r2 = delta * asinh.d2;
2724 let r3 = delta * asinh.d3;
2725 let r4 = delta * asinh.d4;
2726 let u1 = g1 * r1;
2727 let u2 = g2 * r1 * r1 + g1 * r2;
2728 let u3 = g3 * r1 * r1 * r1 + 3.0 * g2 * r1 * r2 + g1 * r3;
2729 let u4 = g4 * r1.powi(4)
2730 + 6.0 * g3 * r1 * r1 * r2
2731 + 3.0 * g2 * r2 * r2
2732 + 4.0 * g2 * r1 * r3
2733 + g1 * r4;
2734 let z1 = c * u1;
2735 let z2 = s * u1 * u1 + c * u2;
2736 let z3 = c * u1 * u1 * u1 + 3.0 * s * u1 * u2 + c * u3;
2737 let z4 =
2738 s * u1.powi(4) + 6.0 * c * u1 * u1 * u2 + 3.0 * s * u2 * u2 + 4.0 * s * u1 * u3 + c * u4;
2739 let base4 = probit_pdfthird_derivative(z);
2740 let out = base4 * z1.powi(4)
2741 + 6.0 * base.d3 * z1 * z1 * z2
2742 + 3.0 * base.d2 * z2 * z2
2743 + 4.0 * base.d2 * z1 * z3
2744 + base.d1 * z4;
2745 Ok(canonicalzero(out))
2746}
2747
2748pub fn sas_inverse_link_pdffourth_derivative(
2767 eta: f64,
2768 epsilon: f64,
2769 log_delta: f64,
2770) -> Result<f64, EstimationError> {
2771 let eta = finite_inverse_link_eta("SAS inverse link", eta)?;
2772 let asinh = asinh_jet5(eta);
2773 let delta = sas_delta_from_raw_log_delta(log_delta);
2774 let u_raw = delta * asinh.value + epsilon;
2775 let sb = smooth_bound_jet(u_raw, SAS_U_CLAMP);
2776 let u = sb.g;
2777 let g1 = sb.d1;
2778 let g2 = sb.d2;
2779 let g3 = sb.d3;
2780 let g4 = sb.d4;
2781 let g5 = sb.d5;
2782 let s = u.sinh();
2783 let c = u.cosh();
2784 let z = s;
2785
2786 let base = probit_jet(z);
2788 let phi3 = probit_pdfthird_derivative(z); let phi4 = probit_pdffourth_derivative(z); let r1 = delta * asinh.d1;
2792 let r2 = delta * asinh.d2;
2793 let r3 = delta * asinh.d3;
2794 let r4 = delta * asinh.d4;
2795 let r5 = delta * asinh.d5;
2796
2797 let u1 = g1 * r1;
2799 let u2 = g2 * r1 * r1 + g1 * r2;
2800 let u3 = g3 * r1 * r1 * r1 + 3.0 * g2 * r1 * r2 + g1 * r3;
2801 let u4 = g4 * r1.powi(4)
2802 + 6.0 * g3 * r1 * r1 * r2
2803 + 3.0 * g2 * r2 * r2
2804 + 4.0 * g2 * r1 * r3
2805 + g1 * r4;
2806 let u5 = g5 * r1.powi(5)
2807 + 10.0 * g4 * r1 * r1 * r1 * r2
2808 + 15.0 * g3 * r1 * r2 * r2
2809 + 10.0 * g3 * r1 * r1 * r3
2810 + 10.0 * g2 * r2 * r3
2811 + 5.0 * g2 * r1 * r4
2812 + g1 * r5;
2813
2814 let z1 = c * u1;
2816 let z2 = s * u1 * u1 + c * u2;
2817 let z3 = c * u1 * u1 * u1 + 3.0 * s * u1 * u2 + c * u3;
2818 let z4 =
2819 s * u1.powi(4) + 6.0 * c * u1 * u1 * u2 + 3.0 * s * u2 * u2 + 4.0 * s * u1 * u3 + c * u4;
2820 let z5 = c * u1.powi(5)
2821 + 10.0 * s * u1 * u1 * u1 * u2
2822 + 15.0 * c * u1 * u2 * u2
2823 + 10.0 * c * u1 * u1 * u3
2824 + 10.0 * s * u2 * u3
2825 + 5.0 * s * u1 * u4
2826 + c * u5;
2827
2828 let out = phi4 * z1.powi(5)
2831 + 10.0 * phi3 * z1 * z1 * z1 * z2
2832 + 15.0 * base.d3 * z1 * z2 * z2
2833 + 10.0 * base.d3 * z1 * z1 * z3
2834 + 10.0 * base.d2 * z2 * z3
2835 + 5.0 * base.d2 * z1 * z4
2836 + base.d1 * z5;
2837 Ok(canonicalzero(out))
2838}
2839
2840pub fn sas_inverse_link_jetwith_param_partials(
2844 eta: f64,
2845 epsilon: f64,
2846 log_delta: f64,
2847) -> Result<SasJetWithParamPartials, EstimationError> {
2848 let eta = finite_inverse_link_eta("SAS inverse link", eta)?;
2849 let asinh = asinh_jet5(eta);
2850 let ld_sb = smooth_bound_jet(log_delta, SAS_LOG_DELTA_BOUND);
2851 let (ld_eff, dld_eff_draw) = (ld_sb.g, ld_sb.d1);
2852 let d2ld_eff_draw2 = ld_sb.d2;
2853 let delta = ld_eff.exp();
2854 let ddelta_draw = delta * dld_eff_draw;
2855 let d2delta_draw2 = delta * (dld_eff_draw * dld_eff_draw + d2ld_eff_draw2);
2856 let u_raw = delta * asinh.value + epsilon;
2857 let sb = smooth_bound_jet(u_raw, SAS_U_CLAMP);
2858 let u = sb.g;
2859 let g1 = sb.d1;
2860 let g2 = sb.d2;
2861 let g3 = sb.d3;
2862 let g4 = sb.d4;
2863 let s = u.sinh();
2864 let c = u.cosh();
2865 let z = s;
2866 let a1 = asinh.d1;
2867 let a2 = asinh.d2;
2868 let a3 = asinh.d3;
2869 let r1 = delta * a1;
2870 let r2 = delta * a2;
2871 let r3 = delta * a3;
2872 let u1 = g1 * r1;
2873 let u2 = g2 * r1 * r1 + g1 * r2;
2874 let u3 = g3 * r1 * r1 * r1 + 3.0 * g2 * r1 * r2 + g1 * r3;
2875 let z1 = c * u1;
2876 let z2 = s * u1 * u1 + c * u2;
2877 let z3 = c * u1 * u1 * u1 + 3.0 * s * u1 * u2 + c * u3;
2878
2879 let base = probit_jet(z);
2880 let jet = chain_inverse_link_jet(base, z1, z2, z3);
2881
2882 let param_partials = |u_t: f64, u1_t: f64, u2_t: f64, u3_t: f64| -> InverseLinkJet {
2885 let z_t = c * u_t;
2886 let z1_t = s * u_t * u1 + c * u1_t;
2887 let z2_t = c * u_t * u1 * u1 + 2.0 * s * u1 * u1_t + s * u_t * u2 + c * u2_t;
2888 let z3_t = s * u_t * u1 * u1 * u1
2889 + 3.0 * c * u1 * u1 * u1_t
2890 + 3.0 * c * u_t * u1 * u2
2891 + 3.0 * s * (u1_t * u2 + u1 * u2_t)
2892 + s * u_t * u3
2893 + c * u3_t;
2894
2895 InverseLinkJet {
2896 mu: base.d1 * z_t,
2897 d1: base.d2 * z_t * z1 + base.d1 * z1_t,
2898 d2: base.d3 * z_t * z1 * z1
2899 + 2.0 * base.d2 * z1 * z1_t
2900 + base.d2 * z_t * z2
2901 + base.d1 * z2_t,
2902 d3: probit_pdfthird_derivative(z) * z_t * z1.powi(3)
2903 + 3.0 * base.d3 * z1 * z1 * z1_t
2904 + 3.0 * base.d3 * z_t * z1 * z2
2905 + 3.0 * base.d2 * (z1_t * z2 + z1 * z2_t)
2906 + base.d2 * z_t * z3
2907 + base.d1 * z3_t,
2908 }
2909 };
2910
2911 let rt_eps = 1.0;
2913 let r1t_eps = 0.0;
2914 let r2t_eps = 0.0;
2915 let r3t_eps = 0.0;
2916 let u_eps = g1 * rt_eps;
2917 let u1_eps = g2 * rt_eps * r1 + g1 * r1t_eps;
2918 let u2_eps = g3 * rt_eps * r1 * r1 + 2.0 * g2 * r1 * r1t_eps + g2 * rt_eps * r2 + g1 * r2t_eps;
2919 let u3_eps = g4 * rt_eps * r1 * r1 * r1
2920 + 3.0 * g3 * r1 * r1 * r1t_eps
2921 + 3.0 * g3 * rt_eps * r1 * r2
2922 + 3.0 * g2 * (r1t_eps * r2 + r1 * r2t_eps)
2923 + g2 * rt_eps * r3
2924 + g1 * r3t_eps;
2925 let djet_depsilon = param_partials(u_eps, u1_eps, u2_eps, u3_eps);
2926
2927 let rt_ld = ddelta_draw * asinh.value;
2929 let r1t_ld = ddelta_draw * a1;
2930 let r2t_ld = ddelta_draw * a2;
2931 let r3t_ld = ddelta_draw * a3;
2932 let u_ld = g1 * rt_ld;
2933 let u1_ld = g2 * rt_ld * r1 + g1 * r1t_ld;
2934 let u2_ld = g3 * rt_ld * r1 * r1 + 2.0 * g2 * r1 * r1t_ld + g2 * rt_ld * r2 + g1 * r2t_ld;
2935 let u3_ld = g4 * rt_ld * r1 * r1 * r1
2936 + 3.0 * g3 * r1 * r1 * r1t_ld
2937 + 3.0 * g3 * rt_ld * r1 * r2
2938 + 3.0 * g2 * (r1t_ld * r2 + r1 * r2t_ld)
2939 + g2 * rt_ld * r3
2940 + g1 * r3t_ld;
2941 let djet_dlog_delta = param_partials(u_ld, u1_ld, u2_ld, u3_ld);
2942
2943 let r_t = [rt_eps, rt_ld];
2949 let r1_t = [r1t_eps, r1t_ld];
2950 let r2_t = [r2t_eps, r2t_ld];
2951 let r_tt = [[0.0, 0.0], [0.0, d2delta_draw2 * asinh.value]];
2952 let r1_tt = [[0.0, 0.0], [0.0, d2delta_draw2 * a1]];
2953 let r2_tt = [[0.0, 0.0], [0.0, d2delta_draw2 * a2]];
2954 let u_t = [u_eps, u_ld];
2955 let u1_t = [u1_eps, u1_ld];
2956 let u2_t = [u2_eps, u2_ld];
2957 let z_t = [c * u_t[0], c * u_t[1]];
2958 let z1_t = [
2959 s * u_t[0] * u1 + c * u1_t[0],
2960 s * u_t[1] * u1 + c * u1_t[1],
2961 ];
2962 let z2_t = [
2963 c * u_t[0] * u1 * u1 + 2.0 * s * u1 * u1_t[0] + s * u_t[0] * u2 + c * u2_t[0],
2964 c * u_t[1] * u1 * u1 + 2.0 * s * u1 * u1_t[1] + s * u_t[1] * u2 + c * u2_t[1],
2965 ];
2966 let phi3 = probit_pdfthird_derivative(z);
2967 let mut d2mu_dparams2 = Array2::<f64>::zeros((2, 2));
2968 let mut d2d1_dparams2 = Array2::<f64>::zeros((2, 2));
2969 let mut d2d2_dparams2 = Array2::<f64>::zeros((2, 2));
2970 for j in 0..2 {
2971 for k in j..2 {
2972 let u_jk = g2 * r_t[j] * r_t[k] + g1 * r_tt[j][k];
2973 let u1_jk = g3 * r_t[j] * r_t[k] * r1
2974 + g2 * r_tt[j][k] * r1
2975 + g2 * r_t[j] * r1_t[k]
2976 + g2 * r_t[k] * r1_t[j]
2977 + g1 * r1_tt[j][k];
2978 let u2_jk = g4 * r_t[j] * r_t[k] * r1 * r1
2980 + g3 * r_tt[j][k] * r1 * r1
2981 + 2.0 * g3 * r_t[j] * r1 * r1_t[k]
2982 + 2.0 * g3 * r_t[k] * r1 * r1_t[j]
2983 + 2.0 * g2 * (r1_t[j] * r1_t[k] + r1 * r1_tt[j][k])
2984 + g3 * r_t[j] * r_t[k] * r2
2985 + g2 * r_tt[j][k] * r2
2986 + g2 * r_t[j] * r2_t[k]
2987 + g2 * r_t[k] * r2_t[j]
2988 + g1 * r2_tt[j][k];
2989 let z_jk = s * u_t[j] * u_t[k] + c * u_jk;
2990 let z1_jk = c * u_t[j] * u_t[k] * u1
2991 + s * u_jk * u1
2992 + s * u_t[j] * u1_t[k]
2993 + s * u_t[k] * u1_t[j]
2994 + c * u1_jk;
2995 let z2_jk = s * u_t[j] * u_t[k] * u1 * u1
2997 + c * u_jk * u1 * u1
2998 + 2.0 * c * u_t[j] * u1 * u1_t[k]
2999 + 2.0 * c * u_t[k] * u1 * u1_t[j]
3000 + 2.0 * s * (u1_t[j] * u1_t[k] + u1 * u1_jk)
3001 + c * u_t[j] * u_t[k] * u2
3002 + s * u_jk * u2
3003 + s * u_t[j] * u2_t[k]
3004 + s * u_t[k] * u2_t[j]
3005 + c * u2_jk;
3006 let mu_jk = base.d2 * z_t[j] * z_t[k] + base.d1 * z_jk;
3007 let d1_jk = base.d3 * z_t[j] * z_t[k] * z1
3008 + base.d2 * z_jk * z1
3009 + base.d2 * z_t[j] * z1_t[k]
3010 + base.d2 * z_t[k] * z1_t[j]
3011 + base.d1 * z1_jk;
3012 let d2_jk = phi3 * z_t[j] * z_t[k] * z1 * z1
3015 + base.d3 * z_jk * z1 * z1
3016 + 2.0 * base.d3 * z_t[j] * z1 * z1_t[k]
3017 + 2.0 * base.d3 * z_t[k] * z1 * z1_t[j]
3018 + 2.0 * base.d2 * (z1_t[j] * z1_t[k] + z1 * z1_jk)
3019 + base.d3 * z_t[j] * z_t[k] * z2
3020 + base.d2 * z_jk * z2
3021 + base.d2 * z_t[j] * z2_t[k]
3022 + base.d2 * z_t[k] * z2_t[j]
3023 + base.d1 * z2_jk;
3024 d2mu_dparams2[[j, k]] = mu_jk;
3025 d2mu_dparams2[[k, j]] = mu_jk;
3026 d2d1_dparams2[[j, k]] = d1_jk;
3027 d2d1_dparams2[[k, j]] = d1_jk;
3028 d2d2_dparams2[[j, k]] = d2_jk;
3029 d2d2_dparams2[[k, j]] = d2_jk;
3030 }
3031 }
3032
3033 Ok(SasJetWithParamPartials {
3034 jet,
3035 djet_depsilon,
3036 djet_dlog_delta,
3037 d2mu_dparams2,
3038 d2d1_dparams2,
3039 d2d2_dparams2,
3040 })
3041}
3042
3043#[cfg(test)]
3044mod tests {
3045 use super::*;
3046 use gam_problem::{InverseLink, LikelihoodSpec, LinkComponent, MixtureLinkSpec, SasLinkState};
3047
3048 fn assert_finite_eta_domain_error(
3049 error: EstimationError,
3050 expected_link: &'static str,
3051 eta: f64,
3052 ) {
3053 match error {
3054 EstimationError::InverseLinkDomainViolation {
3055 link,
3056 eta: rejected,
3057 lower,
3058 upper,
3059 } => {
3060 assert_eq!(link, expected_link);
3061 if eta.is_nan() {
3062 assert!(rejected.is_nan());
3063 } else {
3064 assert_eq!(rejected, eta);
3065 }
3066 assert_eq!(lower, -f64::MAX);
3067 assert_eq!(upper, f64::MAX);
3068 }
3069 other => panic!("expected typed finite-eta domain refusal, got {other}"),
3070 }
3071 }
3072
3073 #[test]
3074 fn log_link_solver_value_gradient_is_consistent_near_both_domain_edges() {
3075 let link = InverseLink::Standard(StandardLink::Log);
3076 let h = 1.0e-5;
3077 for eta in [
3078 LOG_LINK_SOLVER_ETA_MIN + 1.0,
3079 0.0,
3080 LOG_LINK_SOLVER_ETA_MAX - 1.0,
3081 ] {
3082 let jet = inverse_link_jet_for_inverse_link(&link, eta).expect("interior jet");
3083 let eta_plus = eta + h;
3084 let eta_minus = eta - h;
3085 let mu_plus = inverse_link_jet_for_inverse_link(&link, eta_plus)
3086 .expect("plus jet")
3087 .mu;
3088 let mu_minus = inverse_link_jet_for_inverse_link(&link, eta_minus)
3089 .expect("minus jet")
3090 .mu;
3091 let finite_difference = (mu_plus - mu_minus) / (eta_plus - eta_minus);
3092 let relative_error = ((finite_difference - jet.d1) / jet.d1).abs();
3093 assert!(
3094 relative_error < 5.0e-10,
3095 "log-link value/gradient mismatch at eta={eta}: analytic={}, finite_difference={}, relative_error={relative_error}",
3096 jet.d1,
3097 finite_difference
3098 );
3099 }
3100 }
3101
3102 #[test]
3103 fn subnormal_inverse_link_derivatives_are_preserved_not_plateaued() {
3104 let left_eta = -743.0_f64;
3105 let left_scale = left_eta.exp();
3106 assert!(left_scale > 0.0 && left_scale < f64::MIN_POSITIVE);
3107 let left = logit_inverse_link_jet5(left_eta);
3108 for (order, derivative) in [left.d1, left.d2, left.d3, left.d4, left.d5]
3109 .into_iter()
3110 .enumerate()
3111 {
3112 assert!(
3113 derivative > 0.0 && derivative < f64::MIN_POSITIVE,
3114 "left-tail logit derivative order {} lost its represented subnormal: {derivative}",
3115 order + 1
3116 );
3117 }
3118
3119 let right_eta = 743.0_f64;
3120 let right_scale = (-right_eta).exp();
3121 assert!(right_scale > 0.0 && right_scale < f64::MIN_POSITIVE);
3122 let right = logit_inverse_link_jet5(right_eta);
3123 for (order, derivative, sign) in [
3124 (1, right.d1, 1.0),
3125 (2, right.d2, -1.0),
3126 (3, right.d3, 1.0),
3127 (4, right.d4, -1.0),
3128 (5, right.d5, 1.0),
3129 ] {
3130 assert_eq!(derivative.signum(), sign, "wrong order-{order} tail sign");
3131 assert!(
3132 derivative.abs() > 0.0 && derivative.abs() < f64::MIN_POSITIVE,
3133 "right-tail logit derivative order {order} lost its represented subnormal: {derivative}"
3134 );
3135 }
3136
3137 let royston_eta = 735.0_f64.ln();
3138 let royston = royston_parmar_inverse_link_jet(royston_eta)
3139 .expect("finite Royston-Parmar subnormal-tail eta");
3140 assert!(
3141 royston.d1 < 0.0 && royston.d1.abs() < f64::MIN_POSITIVE,
3142 "Royston-Parmar exact tail derivative must retain its subnormal: {}",
3143 royston.d1
3144 );
3145 }
3146
3147 #[test]
3148 fn sas_all_derivative_seams_refuse_nonfinite_eta_with_one_typed_contract() {
3149 let state = sas_link_state_from_raw(0.25, -0.35).expect("SAS state");
3150 let link = InverseLink::Sas(state);
3151 for eta in [f64::NEG_INFINITY, f64::INFINITY, f64::NAN] {
3152 assert_finite_eta_domain_error(
3153 sas_inverse_link_jet(eta, state.epsilon, state.log_delta)
3154 .expect_err("SAS full jet must refuse"),
3155 "SAS inverse link",
3156 eta,
3157 );
3158 assert_finite_eta_domain_error(
3159 sas_inverse_link_mu_d1(eta, state.epsilon, state.log_delta)
3160 .expect_err("SAS mu/d1 must refuse"),
3161 "SAS inverse link",
3162 eta,
3163 );
3164 assert_finite_eta_domain_error(
3165 sas_inverse_link_pdfthird_derivative(eta, state.epsilon, state.log_delta)
3166 .expect_err("SAS fourth derivative must refuse"),
3167 "SAS inverse link",
3168 eta,
3169 );
3170 assert_finite_eta_domain_error(
3171 sas_inverse_link_pdffourth_derivative(eta, state.epsilon, state.log_delta)
3172 .expect_err("SAS fifth derivative must refuse"),
3173 "SAS inverse link",
3174 eta,
3175 );
3176 assert_finite_eta_domain_error(
3177 sas_inverse_link_jetwith_param_partials(eta, state.epsilon, state.log_delta)
3178 .expect_err("SAS parameter partials must refuse"),
3179 "SAS inverse link",
3180 eta,
3181 );
3182 assert_finite_eta_domain_error(
3183 inverse_link_jet_for_inverse_link(&link, eta).expect_err("SAS kernel must refuse"),
3184 "SAS inverse link",
3185 eta,
3186 );
3187 assert_finite_eta_domain_error(
3188 inverse_link_mu_d1_for_inverse_link(&link, eta)
3189 .expect_err("SAS fast dispatch must refuse"),
3190 "SAS inverse link",
3191 eta,
3192 );
3193 assert_finite_eta_domain_error(
3194 inverse_link_pdfthird_derivative_for_inverse_link(&link, eta)
3195 .expect_err("SAS fourth-derivative dispatch must refuse"),
3196 "SAS inverse link",
3197 eta,
3198 );
3199 assert_finite_eta_domain_error(
3200 inverse_link_pdffourth_derivative_for_inverse_link(&link, eta)
3201 .expect_err("SAS fifth-derivative dispatch must refuse"),
3202 "SAS inverse link",
3203 eta,
3204 );
3205 }
3206 }
3207
3208 #[test]
3209 fn sas_jets_are_finite_at_both_finite_f64_domain_edges() {
3210 let state = sas_link_state_from_raw(0.25, -0.35).expect("SAS state");
3211 for eta in [-f64::MAX, f64::MAX] {
3212 let jet = sas_inverse_link_jet(eta, state.epsilon, state.log_delta)
3213 .expect("finite SAS boundary jet");
3214 let partials =
3215 sas_inverse_link_jetwith_param_partials(eta, state.epsilon, state.log_delta)
3216 .expect("finite SAS boundary partials");
3217 let h4 = sas_inverse_link_pdfthird_derivative(eta, state.epsilon, state.log_delta)
3218 .expect("finite SAS boundary fourth derivative");
3219 let h5 = sas_inverse_link_pdffourth_derivative(eta, state.epsilon, state.log_delta)
3220 .expect("finite SAS boundary fifth derivative");
3221 for value in [
3222 jet.mu,
3223 jet.d1,
3224 jet.d2,
3225 jet.d3,
3226 partials.jet.mu,
3227 partials.jet.d1,
3228 partials.jet.d2,
3229 partials.jet.d3,
3230 partials.djet_depsilon.mu,
3231 partials.djet_depsilon.d1,
3232 partials.djet_depsilon.d2,
3233 partials.djet_depsilon.d3,
3234 partials.djet_dlog_delta.mu,
3235 partials.djet_dlog_delta.d1,
3236 partials.djet_dlog_delta.d2,
3237 partials.djet_dlog_delta.d3,
3238 h4,
3239 h5,
3240 ] {
3241 assert!(
3242 value.is_finite(),
3243 "non-finite SAS boundary jet at eta={eta}: {value}"
3244 );
3245 }
3246 }
3247 }
3248
3249 #[test]
3250 fn royston_parmar_exact_jet_has_no_former_minus_thirty_plateau() {
3251 let left =
3252 royston_parmar_inverse_link_jet(-30.0 - 1.0e-6).expect("finite Royston-Parmar eta");
3253 let center = royston_parmar_inverse_link_jet(-30.0).expect("finite Royston-Parmar eta");
3254 let right =
3255 royston_parmar_inverse_link_jet(-30.0 + 1.0e-6).expect("finite Royston-Parmar eta");
3256
3257 assert!(
3258 left.d1 < 0.0,
3259 "the exact left tail must not be a constant plateau"
3260 );
3261 assert!(center.d1 < 0.0 && right.d1 < 0.0);
3262 let left_relative = ((left.d1 - center.d1) / center.d1).abs();
3263 let right_relative = ((right.d1 - center.d1) / center.d1).abs();
3264 assert!(
3265 left_relative < 2.0e-6,
3266 "left derivative kink: {left_relative}"
3267 );
3268 assert!(
3269 right_relative < 2.0e-6,
3270 "right derivative kink: {right_relative}"
3271 );
3272
3273 for eta in [-f64::MAX, -40.0, -30.0, 0.0, 7.0, 30.0, 40.0, f64::MAX] {
3274 let jet = royston_parmar_inverse_link_jet(eta).expect("finite Royston-Parmar eta");
3275 for value in [jet.mu, jet.d1, jet.d2, jet.d3] {
3276 assert!(
3277 value.is_finite(),
3278 "non-finite Royston-Parmar jet at eta={eta}: {value}"
3279 );
3280 }
3281 }
3282 }
3283
3284 #[test]
3285 fn mixture_jet_rho_partials_matchfd() {
3286 let spec = MixtureLinkSpec {
3287 components: vec![
3288 LinkComponent::Probit,
3289 LinkComponent::Logit,
3290 LinkComponent::CLogLog,
3291 LinkComponent::Cauchit,
3292 ],
3293 initial_rho: Array1::from_vec(vec![0.3, -0.6, 0.2]),
3294 };
3295 let state = state_fromspec(&spec).expect("state");
3296 let eta = 0.35;
3297 let out = mixture_inverse_link_jetwith_rho_partials(&state, eta);
3298 let h = 1e-6;
3299 for j in 0..state.rho.len() {
3300 let mut rp = state.rho.clone();
3301 rp[j] += h;
3302 let sp = MixtureLinkSpec {
3303 components: state.components.clone(),
3304 initial_rho: rp,
3305 };
3306 let jp = mixture_inverse_link_jet(&state_fromspec(&sp).expect("sp"), eta);
3307 let mut rm = state.rho.clone();
3308 rm[j] -= h;
3309 let sm = MixtureLinkSpec {
3310 components: state.components.clone(),
3311 initial_rho: rm,
3312 };
3313 let jm = mixture_inverse_link_jet(&state_fromspec(&sm).expect("sm"), eta);
3314 let fd = InverseLinkJet {
3315 mu: (jp.mu - jm.mu) / (2.0 * h),
3316 d1: (jp.d1 - jm.d1) / (2.0 * h),
3317 d2: (jp.d2 - jm.d2) / (2.0 * h),
3318 d3: (jp.d3 - jm.d3) / (2.0 * h),
3319 };
3320 let an = out.djet_drho[j];
3321 assert_eq!(an.mu.signum(), fd.mu.signum());
3322 assert_eq!(an.d1.signum(), fd.d1.signum());
3323 assert_eq!(an.d2.signum(), fd.d2.signum());
3324 assert_eq!(an.d3.signum(), fd.d3.signum());
3325 assert!((an.mu - fd.mu).abs() < 1e-6);
3326 assert!((an.d1 - fd.d1).abs() < 1e-6);
3327 assert!((an.d2 - fd.d2).abs() < 1e-6);
3328 assert!((an.d3 - fd.d3).abs() < 1e-6);
3329 }
3330 }
3331
3332 #[test]
3339 fn sas_d2d2_param_hessian_matchesfd_of_the_first_partial() {
3340 let eta = 0.37;
3341 let epsilon = -0.12;
3342 let log_delta = 0.21;
3343 let h = 1e-6;
3344 let out = sas_inverse_link_jetwith_param_partials(eta, epsilon, log_delta)
3345 .expect("finite SAS eta");
3346 let at = |e: f64, l: f64| {
3347 sas_inverse_link_jetwith_param_partials(eta, e, l).expect("finite SAS eta")
3348 };
3349 let d_eps = [
3351 (at(epsilon + h, log_delta).djet_depsilon.d2
3352 - at(epsilon - h, log_delta).djet_depsilon.d2)
3353 / (2.0 * h),
3354 (at(epsilon, log_delta + h).djet_depsilon.d2
3355 - at(epsilon, log_delta - h).djet_depsilon.d2)
3356 / (2.0 * h),
3357 ];
3358 let d_ld = [
3359 (at(epsilon + h, log_delta).djet_dlog_delta.d2
3360 - at(epsilon - h, log_delta).djet_dlog_delta.d2)
3361 / (2.0 * h),
3362 (at(epsilon, log_delta + h).djet_dlog_delta.d2
3363 - at(epsilon, log_delta - h).djet_dlog_delta.d2)
3364 / (2.0 * h),
3365 ];
3366 for (idx, reference) in [((0, 0), d_eps[0]), ((0, 1), d_eps[1])] {
3367 let got = out.d2d2_dparams2[[idx.0, idx.1]];
3368 assert!(
3369 (got - reference).abs() < 1e-5 * (1.0 + reference.abs()),
3370 "d2d2_dparams2{idx:?} = {got:e} against FD {reference:e}"
3371 );
3372 }
3373 for (idx, reference) in [((1, 0), d_ld[0]), ((1, 1), d_ld[1])] {
3374 let got = out.d2d2_dparams2[[idx.0, idx.1]];
3375 assert!(
3376 (got - reference).abs() < 1e-5 * (1.0 + reference.abs()),
3377 "d2d2_dparams2{idx:?} = {got:e} against FD {reference:e}"
3378 );
3379 }
3380 assert_eq!(out.d2d2_dparams2[[0, 1]], out.d2d2_dparams2[[1, 0]]);
3381 }
3382
3383 #[test]
3386 fn beta_logistic_d2d2_param_hessian_matchesfd_of_the_first_partial() {
3387 let eta = -0.29;
3388 let epsilon = 0.18;
3389 let log_shape_center = 0.24;
3390 let h = 1e-6;
3391 let out =
3392 beta_logistic_inverse_link_jetwith_param_partials(eta, log_shape_center, epsilon);
3393 let at =
3394 |e: f64, l: f64| beta_logistic_inverse_link_jetwith_param_partials(eta, l, e);
3395 let reference = [
3396 [
3397 (at(epsilon + h, log_shape_center).djet_depsilon.d2
3398 - at(epsilon - h, log_shape_center).djet_depsilon.d2)
3399 / (2.0 * h),
3400 (at(epsilon, log_shape_center + h).djet_depsilon.d2
3401 - at(epsilon, log_shape_center - h).djet_depsilon.d2)
3402 / (2.0 * h),
3403 ],
3404 [
3405 (at(epsilon + h, log_shape_center).djet_dlog_delta.d2
3406 - at(epsilon - h, log_shape_center).djet_dlog_delta.d2)
3407 / (2.0 * h),
3408 (at(epsilon, log_shape_center + h).djet_dlog_delta.d2
3409 - at(epsilon, log_shape_center - h).djet_dlog_delta.d2)
3410 / (2.0 * h),
3411 ],
3412 ];
3413 for j in 0..2 {
3414 for k in 0..2 {
3415 let got = out.d2d2_dparams2[[j, k]];
3416 let want = reference[j][k];
3417 assert!(
3418 (got - want).abs() < 1e-4 * (1.0 + want.abs()),
3419 "beta-logistic d2d2_dparams2[{j},{k}] = {got:e} against FD {want:e}"
3420 );
3421 }
3422 }
3423 }
3424
3425 #[test]
3427 fn mixture_d2d2_rho_hessian_matchesfd_of_the_first_partial() {
3428 let state = state_fromspec(&MixtureLinkSpec {
3429 components: vec![
3430 LinkComponent::Probit,
3431 LinkComponent::Logit,
3432 LinkComponent::CLogLog,
3433 ],
3434 initial_rho: Array1::from_vec(vec![0.31, -0.17]),
3435 })
3436 .expect("valid three-component mixture");
3437 let eta = 0.42;
3438 let h = 1e-6;
3439 let out = mixture_inverse_link_jetwith_rho_partials(&state, eta);
3440 let m = state.rho.len();
3441 let shifted = |j: usize, delta: f64| {
3442 let mut rho = state.rho.clone();
3443 rho[j] += delta;
3444 state_fromspec(&MixtureLinkSpec {
3445 components: state.components.clone(),
3446 initial_rho: rho,
3447 })
3448 .expect("valid shifted mixture")
3449 };
3450 for j in 0..m {
3451 for k in 0..m {
3452 let plus = mixture_inverse_link_jetwith_rho_partials(&shifted(k, h), eta);
3453 let minus = mixture_inverse_link_jetwith_rho_partials(&shifted(k, -h), eta);
3454 let want = (plus.djet_drho[j].d2 - minus.djet_drho[j].d2) / (2.0 * h);
3455 let got = out.d2d2_drho2[[j, k]];
3456 assert!(
3457 (got - want).abs() < 1e-5 * (1.0 + want.abs()),
3458 "mixture d2d2_drho2[{j},{k}] = {got:e} against FD {want:e}"
3459 );
3460 }
3461 }
3462 }
3463
3464 #[test]
3465 fn mixture_second_partials_obey_equal_weight_two_component_identity() {
3466 let state = state_fromspec(&MixtureLinkSpec {
3467 components: vec![LinkComponent::Probit, LinkComponent::Logit],
3468 initial_rho: Array1::from_vec(vec![0.0]),
3469 })
3470 .expect("valid two-component mixture");
3471 let out = mixture_inverse_link_jetwith_rho_partials(&state, 0.37);
3472 assert_eq!(out.d2mu_drho2.dim(), (1, 1));
3477 assert_eq!(out.d2d1_drho2.dim(), (1, 1));
3478 assert_eq!(out.d2mu_drho2[[0, 0]], 0.0);
3479 assert_eq!(out.d2d1_drho2[[0, 0]], 0.0);
3480 }
3481
3482 #[test]
3483 fn sas_param_partials_matchfd() {
3484 let eta = 0.37;
3485 let epsilon = -0.12;
3486 let log_delta = 0.21;
3487 let out = sas_inverse_link_jetwith_param_partials(eta, epsilon, log_delta)
3488 .expect("finite SAS eta");
3489 let h = 1e-6;
3490
3491 let ep_p = sas_inverse_link_jet(eta, epsilon + h, log_delta).expect("finite SAS eta");
3492 let ep_m = sas_inverse_link_jet(eta, epsilon - h, log_delta).expect("finite SAS eta");
3493 let fd_ep = InverseLinkJet {
3494 mu: (ep_p.mu - ep_m.mu) / (2.0 * h),
3495 d1: (ep_p.d1 - ep_m.d1) / (2.0 * h),
3496 d2: (ep_p.d2 - ep_m.d2) / (2.0 * h),
3497 d3: (ep_p.d3 - ep_m.d3) / (2.0 * h),
3498 };
3499 assert_eq!(out.djet_depsilon.mu.signum(), fd_ep.mu.signum());
3500 assert_eq!(out.djet_depsilon.d1.signum(), fd_ep.d1.signum());
3501 assert_eq!(out.djet_depsilon.d2.signum(), fd_ep.d2.signum());
3502 assert_eq!(out.djet_depsilon.d3.signum(), fd_ep.d3.signum());
3503 assert!((out.djet_depsilon.mu - fd_ep.mu).abs() < 5e-5);
3504 assert!((out.djet_depsilon.d1 - fd_ep.d1).abs() < 5e-5);
3505 assert!((out.djet_depsilon.d2 - fd_ep.d2).abs() < 5e-5);
3506 assert!((out.djet_depsilon.d3 - fd_ep.d3).abs() < 5e-4);
3507
3508 let ld_p = sas_inverse_link_jet(eta, epsilon, log_delta + h).expect("finite SAS eta");
3509 let ld_m = sas_inverse_link_jet(eta, epsilon, log_delta - h).expect("finite SAS eta");
3510 let fd_ld = InverseLinkJet {
3511 mu: (ld_p.mu - ld_m.mu) / (2.0 * h),
3512 d1: (ld_p.d1 - ld_m.d1) / (2.0 * h),
3513 d2: (ld_p.d2 - ld_m.d2) / (2.0 * h),
3514 d3: (ld_p.d3 - ld_m.d3) / (2.0 * h),
3515 };
3516 assert_eq!(out.djet_dlog_delta.mu.signum(), fd_ld.mu.signum());
3517 assert_eq!(out.djet_dlog_delta.d1.signum(), fd_ld.d1.signum());
3518 assert_eq!(out.djet_dlog_delta.d2.signum(), fd_ld.d2.signum());
3519 assert_eq!(out.djet_dlog_delta.d3.signum(), fd_ld.d3.signum());
3520 assert!((out.djet_dlog_delta.mu - fd_ld.mu).abs() < 5e-5);
3521 assert!((out.djet_dlog_delta.d1 - fd_ld.d1).abs() < 5e-5);
3522 assert!((out.djet_dlog_delta.d2 - fd_ld.d2).abs() < 5e-5);
3523 assert!((out.djet_dlog_delta.d3 - fd_ld.d3).abs() < 5e-4);
3524 }
3525
3526 #[test]
3527 fn sas_second_partials_have_exact_center_identities() {
3528 let out = sas_inverse_link_jetwith_param_partials(0.0, 0.0, 0.0)
3529 .expect("finite SAS center");
3530 let phi0 = normal_pdf(0.0);
3531 assert_eq!(out.d2mu_dparams2, Array2::<f64>::zeros((2, 2)));
3539 assert_eq!(out.d2d1_dparams2[[0, 1]], out.d2d1_dparams2[[1, 0]]);
3540 assert_eq!(
3541 out.d2d1_dparams2[[0, 0]], 0.0,
3542 "ε–ε ∂²(μ') must be exactly zero at the identity-interior center"
3543 );
3544 assert!((out.d2d1_dparams2[[1, 1]] - phi0).abs() < 1.0e-15);
3545 assert_eq!(out.d2d1_dparams2[[0, 1]], 0.0);
3546 }
3547
3548 #[test]
3569 fn sas_family_score_depsilon_matches_fd_and_reproduces_profile_1876() {
3570 let h = 1e-6;
3571 let mu_at = |eta: f64, eps: f64, ld: f64| {
3572 sas_inverse_link_jet(eta, eps, ld)
3573 .expect("finite SAS eta")
3574 .mu
3575 };
3576 let dmu_deps = |eta: f64, eps: f64, ld: f64| {
3577 sas_inverse_link_jetwith_param_partials(eta, eps, ld)
3578 .expect("finite SAS eta")
3579 .djet_depsilon
3580 .mu
3581 };
3582 let row_ll = |y: f64, w: f64, mu: f64| w * (y * mu.ln() + (1.0 - y) * (1.0 - mu).ln());
3584 let a1 = |y: f64, w: f64, mu: f64| w * (y / mu - (1.0 - y) / (1.0 - mu));
3585
3586 let etas = [-1.0, -0.6, -0.2, 0.15, 0.5, 0.9];
3588 let epsilons = [-0.5, -0.2, 0.0, 0.3, 0.6];
3589 let log_deltas = [-0.3, 0.0, 0.4];
3590 for &eta in &etas {
3591 for &eps in &epsilons {
3592 for &ld in &log_deltas {
3593 let mu0 = mu_at(eta, eps, ld);
3594 if !(0.02..=0.98).contains(&mu0) {
3597 continue;
3598 }
3599 let dmu = dmu_deps(eta, eps, ld);
3600 for &y in &[0.0_f64, 1.0] {
3601 let analytic = a1(y, 1.0, mu0) * dmu; let fd = (row_ll(y, 1.0, mu_at(eta, eps + h, ld))
3603 - row_ll(y, 1.0, mu_at(eta, eps - h, ld)))
3604 / (2.0 * h);
3605 let scale = analytic.abs().max(fd.abs()).max(1.0);
3606 assert_eq!(
3607 analytic.signum(),
3608 fd.signum(),
3609 "∂ℓ/∂ε sign mismatch η={eta} ε={eps} log_δ={ld} y={y}: \
3610 analytic={analytic:e} fd={fd:e}"
3611 );
3612 assert!(
3613 (analytic - fd).abs() < 1e-5 * scale,
3614 "∂ℓ/∂ε magnitude mismatch η={eta} ε={eps} log_δ={ld} y={y}: \
3615 analytic={analytic:e} fd={fd:e}"
3616 );
3617 }
3618 }
3619 }
3620 }
3621
3622 let eps_true = 0.38;
3624 let ld_true = 0.0;
3625 let etas_ds: Vec<f64> = (0..21).map(|i| -1.0 + 0.1 * i as f64).collect();
3626 let y: Vec<f64> = etas_ds
3627 .iter()
3628 .map(|&e| mu_at(e, eps_true, ld_true))
3629 .collect();
3630
3631 let grad_nll = |eps: f64| -> (f64, f64) {
3633 let mut analytic = 0.0;
3634 let mut nll_p = 0.0;
3635 let mut nll_m = 0.0;
3636 for (i, &eta) in etas_ds.iter().enumerate() {
3637 let mu0 = mu_at(eta, eps, ld_true);
3638 analytic += -a1(y[i], 1.0, mu0) * dmu_deps(eta, eps, ld_true);
3639 nll_p += -row_ll(y[i], 1.0, mu_at(eta, eps + h, ld_true));
3640 nll_m += -row_ll(y[i], 1.0, mu_at(eta, eps - h, ld_true));
3641 }
3642 (analytic, (nll_p - nll_m) / (2.0 * h))
3643 };
3644
3645 for &eps in &[0.0, eps_true, 0.6] {
3647 let (analytic, fd) = grad_nll(eps);
3648 let scale = analytic.abs().max(fd.abs()).max(1.0);
3649 assert!(
3650 (analytic - fd).abs() < 1e-4 * scale,
3651 "summed ∂NLL/∂ε analytic≠fd at ε={eps}: {analytic:e} vs {fd:e}"
3652 );
3653 }
3654 let (g_below, _) = grad_nll(0.0);
3656 let (g_at, _) = grad_nll(eps_true);
3657 let (g_above, _) = grad_nll(0.6);
3658 assert!(
3659 g_below < -1.0,
3660 "expected strongly negative ∂NLL/∂ε below ε* (pushes ε up toward truth), got {g_below:e}"
3661 );
3662 assert!(
3663 g_at.abs() < 1e-6,
3664 "expected ≈0 ∂NLL/∂ε at the planted ε* (score identity), got {g_at:e}"
3665 );
3666 assert!(
3667 g_above > 1.0,
3668 "expected strongly positive ∂NLL/∂ε above ε*, got {g_above:e}"
3669 );
3670 assert!(
3671 g_below < g_at && g_at < g_above,
3672 "∂NLL/∂ε must strictly increase through ε* (distinguishes ±ε): \
3673 {g_below:e} < {g_at:e} < {g_above:e}"
3674 );
3675 }
3676
3677 #[test]
3678 fn sas_jet_extreme_inputs_stay_finite() {
3679 let cases = [
3680 (-1e6, 0.0, 0.0),
3681 (1e6, 0.0, 0.0),
3682 (3.0, 12.0, 12.0),
3683 (-3.0, -12.0, -12.0),
3684 (0.5, 40.0, 10.0),
3685 (0.5, -40.0, -10.0),
3686 ];
3687 for (eta, eps, log_delta) in cases {
3688 let j = sas_inverse_link_jet(eta, eps, log_delta).expect("finite SAS eta");
3689 assert!(j.mu.is_finite());
3690 assert!(j.d1.is_finite());
3691 assert!(j.d2.is_finite());
3692 assert!(j.d3.is_finite());
3693 let p = sas_inverse_link_jetwith_param_partials(eta, eps, log_delta)
3694 .expect("finite SAS eta");
3695 assert!(p.djet_depsilon.mu.is_finite());
3696 assert!(p.djet_depsilon.d1.is_finite());
3697 assert!(p.djet_depsilon.d2.is_finite());
3698 assert!(p.djet_depsilon.d3.is_finite());
3699 assert!(p.djet_dlog_delta.mu.is_finite());
3700 assert!(p.djet_dlog_delta.d1.is_finite());
3701 assert!(p.djet_dlog_delta.d2.is_finite());
3702 assert!(p.djet_dlog_delta.d3.is_finite());
3703 }
3704 }
3705
3706 #[test]
3707 fn sas_param_partials_remain_finite_in_extreme_region() {
3708 let eta = 10.0;
3709 let epsilon = -60.0;
3710 let log_delta = 40.0;
3711 let j = sas_inverse_link_jetwith_param_partials(eta, epsilon, log_delta)
3712 .expect("finite SAS eta");
3713 assert!(j.djet_depsilon.mu.is_finite());
3714 assert!(j.djet_depsilon.d1.is_finite());
3715 assert!(j.djet_depsilon.d2.is_finite());
3716 assert!(j.djet_depsilon.d3.is_finite());
3717 assert!(j.djet_dlog_delta.mu.is_finite());
3718 assert!(j.djet_dlog_delta.d1.is_finite());
3719 assert!(j.djet_dlog_delta.d2.is_finite());
3720 assert!(j.djet_dlog_delta.d3.is_finite());
3721 }
3722
3723 #[test]
3724 fn sas_eta_jets_matchfd() {
3725 let eta = -0.43;
3726 let epsilon = 0.27;
3727 let log_delta = -0.31;
3728 let h = 1e-5;
3729 let j0 = sas_inverse_link_jet(eta, epsilon, log_delta).expect("finite SAS eta");
3730 let jp = sas_inverse_link_jet(eta + h, epsilon, log_delta).expect("finite SAS eta");
3731 let jm = sas_inverse_link_jet(eta - h, epsilon, log_delta).expect("finite SAS eta");
3732 let d1fd = (jp.mu - jm.mu) / (2.0 * h);
3733 let d2fd = (jp.d1 - jm.d1) / (2.0 * h);
3734 let d3fd = (jp.d2 - jm.d2) / (2.0 * h);
3735 assert_eq!(j0.d1.signum(), d1fd.signum());
3736 assert_eq!(j0.d2.signum(), d2fd.signum());
3737 assert_eq!(j0.d3.signum(), d3fd.signum());
3738 assert!((j0.d1 - d1fd).abs() < 5e-5);
3739 assert!((j0.d2 - d2fd).abs() < 2e-4);
3740 assert!((j0.d3 - d3fd).abs() < 1e-3);
3741 }
3742
3743 #[test]
3744 fn beta_logistic_reduces_to_logit_at_delta0_epsilon0() {
3745 let etas = [-40.0, -30.0, -5.0, 0.42, 5.0, 30.0, 40.0];
3746 for eta in etas {
3747 let j_bl = beta_logistic_inverse_link_jet(eta, 0.0, 0.0);
3748 let expected_mu = gam_linalg::utils::stable_logistic(eta);
3749 let expected_d1 = (-gam_linalg::utils::stable_softplus(-eta)
3750 - gam_linalg::utils::stable_softplus(eta))
3751 .exp();
3752 assert!(
3753 (j_bl.mu - expected_mu).abs() <= 1e-15 * expected_mu.abs().max(1.0),
3754 "mu mismatch at eta={eta}: got {}, expected {}",
3755 j_bl.mu,
3756 expected_mu
3757 );
3758 assert!(
3759 (j_bl.d1 - expected_d1).abs() <= 1e-12 * expected_d1.abs().max(f64::MIN_POSITIVE),
3760 "d1 mismatch at eta={eta}: got {}, expected {}",
3761 j_bl.d1,
3762 expected_d1
3763 );
3764 assert!(j_bl.d1 > 0.0, "d1 should stay positive at eta={eta}");
3765 }
3766
3767 let eta = 0.42;
3768 let j_bl = beta_logistic_inverse_link_jet(eta, 0.0, 0.0);
3769 let j_logit = component_inverse_link_jet(LinkComponent::Logit, eta);
3770 assert!((j_bl.d2 - j_logit.d2).abs() < 1e-10);
3771 assert!((j_bl.d3 - j_logit.d3).abs() < 1e-10);
3772 }
3773
3774 #[test]
3775 fn beta_logistic_eta_jets_matchfd() {
3776 let eta = -0.31;
3777 let delta = 0.27;
3778 let epsilon = -0.19;
3779 let h = 1e-5;
3780 let j0 = beta_logistic_inverse_link_jet(eta, delta, epsilon);
3781 let jp = beta_logistic_inverse_link_jet(eta + h, delta, epsilon);
3782 let jm = beta_logistic_inverse_link_jet(eta - h, delta, epsilon);
3783 let d1fd = (jp.mu - jm.mu) / (2.0 * h);
3784 let d2fd = (jp.d1 - jm.d1) / (2.0 * h);
3785 let d3fd = (jp.d2 - jm.d2) / (2.0 * h);
3786 assert_eq!(j0.d1.signum(), d1fd.signum());
3787 assert_eq!(j0.d2.signum(), d2fd.signum());
3788 assert_eq!(j0.d3.signum(), d3fd.signum());
3789 assert!((j0.d1 - d1fd).abs() < 5e-5);
3790 assert!((j0.d2 - d2fd).abs() < 5e-5);
3791 assert!((j0.d3 - d3fd).abs() < 2e-4);
3792 }
3793
3794 #[test]
3795 fn standard_kernel_structs_match_component_jets() {
3796 let eta = 0.73;
3797 assert_eq!(
3798 ProbitLinkKernel.jet(eta).expect("probit"),
3799 component_inverse_link_jet(LinkComponent::Probit, eta)
3800 );
3801 assert_eq!(
3802 LogitLinkKernel.jet(eta).expect("logit"),
3803 component_inverse_link_jet(LinkComponent::Logit, eta)
3804 );
3805 assert_eq!(
3806 CLogLogLinkKernel.jet(eta).expect("cloglog"),
3807 component_inverse_link_jet(LinkComponent::CLogLog, eta)
3808 );
3809 assert_eq!(
3810 LogLogLinkKernel.jet(eta).expect("loglog"),
3811 component_inverse_link_jet(LinkComponent::LogLog, eta)
3812 );
3813 assert_eq!(
3814 CauchitLinkKernel.jet(eta).expect("cauchit"),
3815 component_inverse_link_jet(LinkComponent::Cauchit, eta)
3816 );
3817 }
3818
3819 #[test]
3820 fn all_component_eta_jets_matchfd() {
3821 let components = [
3822 LinkComponent::Logit,
3823 LinkComponent::Probit,
3824 LinkComponent::CLogLog,
3825 LinkComponent::LogLog,
3826 LinkComponent::Cauchit,
3827 ];
3828 let points = [-3.0, -1.1, -0.2, 0.0, 0.7, 1.8, 3.2];
3829 let h = 1e-5;
3830 for c in components {
3831 for &eta in &points {
3832 let j0 = component_inverse_link_jet(c, eta);
3833 let jp = component_inverse_link_jet(c, eta + h);
3834 let jm = component_inverse_link_jet(c, eta - h);
3835 let d1fd = (jp.mu - jm.mu) / (2.0 * h);
3836 let d2fd = (jp.d1 - jm.d1) / (2.0 * h);
3837 let d3fd = (jp.d2 - jm.d2) / (2.0 * h);
3838 let d1_tol = if matches!(c, LinkComponent::CLogLog | LinkComponent::LogLog) {
3839 1.2e-4
3840 } else {
3841 5e-5
3842 };
3843 let d2_tol = if matches!(c, LinkComponent::CLogLog | LinkComponent::LogLog) {
3844 4e-4
3845 } else {
3846 1.2e-4
3847 };
3848 let d3_tol = if matches!(c, LinkComponent::CLogLog | LinkComponent::LogLog) {
3849 1.2e-3
3850 } else {
3851 4e-4
3852 };
3853 if j0.d1.abs().max(d1fd.abs()) > 1e-10 {
3854 assert_eq!(
3855 j0.d1.signum(),
3856 d1fd.signum(),
3857 "d1 sign mismatch for {c:?} eta={eta}"
3858 );
3859 }
3860 if j0.d2.abs().max(d2fd.abs()) > 1e-10 {
3861 assert_eq!(
3862 j0.d2.signum(),
3863 d2fd.signum(),
3864 "d2 sign mismatch for {c:?} eta={eta}: analytic={} fd={}",
3865 j0.d2,
3866 d2fd
3867 );
3868 }
3869 if j0.d3.abs().max(d3fd.abs()) > 1e-10 {
3870 assert_eq!(
3871 j0.d3.signum(),
3872 d3fd.signum(),
3873 "d3 sign mismatch for {c:?} eta={eta}"
3874 );
3875 }
3876 assert!(
3877 (j0.d1 - d1fd).abs() < d1_tol,
3878 "d1 mismatch for {c:?} eta={eta}: analytic={} fd={}",
3879 j0.d1,
3880 d1fd
3881 );
3882 assert!(
3883 (j0.d2 - d2fd).abs() < d2_tol,
3884 "d2 mismatch for {c:?} eta={eta}: analytic={} fd={}",
3885 j0.d2,
3886 d2fd
3887 );
3888 assert!(
3889 (j0.d3 - d3fd).abs() < d3_tol,
3890 "d3 mismatch for {c:?} eta={eta}: analytic={} fd={}",
3891 j0.d3,
3892 d3fd
3893 );
3894 }
3895 }
3896 }
3897
3898 #[test]
3899 fn sas_center_matches_probit_at_delta1_epsilon0() {
3900 let etas = [-3.0, -1.2, -0.3, 0.0, 0.4, 1.7, 3.0];
3903 for eta in etas {
3904 let sas = sas_inverse_link_jet(eta, 0.0, 0.0).expect("finite SAS eta");
3905 let probit = ProbitLinkKernel.jet(eta).expect("probit");
3906 assert_eq!(sas.mu.to_bits(), probit.mu.to_bits(), "mu at eta={eta}");
3907 assert_eq!(sas.d1.to_bits(), probit.d1.to_bits(), "d1 at eta={eta}");
3908 assert_eq!(sas.d2.to_bits(), probit.d2.to_bits(), "d2 at eta={eta}");
3909 assert_eq!(sas.d3.to_bits(), probit.d3.to_bits(), "d3 at eta={eta}");
3910 }
3911 }
3912
3913 #[test]
3924 fn sas_probit_reduction_is_exact_on_full_path_and_smooth_across_epsilon_zero() {
3925 let etas = [-3.0, -1.2, -0.3, 0.0, 0.4, 1.7, 3.0];
3926 for &eta in &etas {
3927 let probit = ProbitLinkKernel.jet(eta).expect("probit");
3928 for &eps in &[1e-11_f64, -1e-11] {
3932 let sas = sas_inverse_link_jet(eta, eps, 0.0).expect("finite SAS eta");
3933 assert!(
3936 (sas.mu - probit.mu).abs() < 1e-11,
3937 "full-path μ off probit at eta={eta} eps={eps}: {} vs {}",
3938 sas.mu,
3939 probit.mu
3940 );
3941 assert!(
3942 (sas.d1 - probit.d1).abs() < 1e-10,
3943 "full-path d1 off probit at eta={eta} eps={eps}"
3944 );
3945 }
3946 let center = sas_inverse_link_jet(eta, 0.0, 0.0).expect("fast path").mu;
3950 let lo = sas_inverse_link_jet(eta, -1e-11, 0.0).expect("full path").mu;
3951 let hi = sas_inverse_link_jet(eta, 1e-11, 0.0).expect("full path").mu;
3952 assert!(
3953 (hi - center).abs() < 1e-11 && (center - lo).abs() < 1e-11,
3954 "μ cliff across ε=0 at eta={eta}: lo={lo} center={center} hi={hi}"
3955 );
3956 }
3957 }
3958
3959 #[test]
3968 fn smooth_bound_jet_tower_is_c5_and_fd_exact() {
3969 let b = SAS_U_CLAMP;
3970 let a = SPLICE_INTERIOR_FRAC * b; let c = (2.0 - SPLICE_INTERIOR_FRAC) * b; let jet = |x: f64| smooth_bound_jet(x, b);
3973
3974 for &x in &[0.0, 0.5, 17.3, a - 1e-9, a] {
3976 let j = jet(x);
3977 assert_eq!(j.g, x, "interior identity value at x={x}");
3978 assert_eq!(j.d1, 1.0, "interior d1 at x={x}");
3979 assert_eq!((j.d2, j.d3, j.d4, j.d5), (0.0, 0.0, 0.0, 0.0));
3980 }
3981 for &x in &[c, c + 1e-9, 75.0, 1e12, f64::MAX] {
3983 let j = jet(x);
3984 assert_eq!(j.g, b, "saturation value at x={x}");
3985 assert_eq!((j.d1, j.d2, j.d3, j.d4, j.d5), (0.0, 0.0, 0.0, 0.0, 0.0));
3986 }
3987 assert_eq!(jet(c).g, b);
3989
3990 for &x in &[3.0, a - 2.0, 44.0, 50.0, 56.0, 100.0] {
3992 let p = jet(x);
3993 let m = jet(-x);
3994 assert_eq!(m.g, -p.g, "g odd at x={x}");
3995 assert_eq!(m.d1, p.d1, "d1 even at x={x}");
3996 assert_eq!(m.d2, -p.d2, "d2 odd at x={x}");
3997 assert_eq!(m.d3, p.d3, "d3 even at x={x}");
3998 assert_eq!(m.d4, -p.d4, "d4 odd at x={x}");
3999 assert_eq!(m.d5, p.d5, "d5 even at x={x}");
4000 }
4001
4002 for i in 0..=240 {
4004 let x = -80.0 + 160.0 * (i as f64) / 240.0;
4005 let j = jet(x);
4006 assert!((0.0..=1.0).contains(&j.d1), "d1 out of [0,1] at x={x}: {}", j.d1);
4007 assert!(j.g.abs() <= b + 1e-12, "|g| exceeds B at x={x}: {}", j.g);
4008 }
4009
4010 let h = 0.02;
4017 for &x0 in &[45.0, 47.5, 50.0, 52.5, 55.0, -47.5, -52.5] {
4018 let jp = jet(x0 + h);
4019 let jm = jet(x0 - h);
4020 let j0 = jet(x0);
4021 let fd = |hi: f64, lo: f64| (hi - lo) / (2.0 * h);
4022 let checks = [
4023 ("d1", j0.d1, fd(jp.g, jm.g), 2e-6),
4024 ("d2", j0.d2, fd(jp.d1, jm.d1), 2e-5),
4025 ("d3", j0.d3, fd(jp.d2, jm.d2), 2e-4),
4026 ("d4", j0.d4, fd(jp.d3, jm.d3), 2e-3),
4027 ("d5", j0.d5, fd(jp.d4, jm.d4), 2e-2),
4028 ];
4029 for (name, analytic, numeric, tol) in checks {
4030 assert!(
4031 (analytic - numeric).abs() < tol * (1.0 + analytic.abs()),
4032 "smooth_bound {name} FD mismatch at x={x0}: analytic={analytic:e} fd={numeric:e}"
4033 );
4034 }
4035 }
4036 }
4037
4038 #[test]
4052 fn sas_link_complement_mirrors_forward_map_across_finite_domain() {
4053 let params = [
4054 (0.0, 0.0), (0.25, -0.35), (0.0, -6.0), (-0.5, -6.0),
4058 (0.3, 6.0), ];
4060 let etas = [
4061 -f64::MAX,
4062 -1e160, -1e6,
4064 -3.0,
4065 -0.2,
4066 0.0,
4067 0.4,
4068 3.0,
4069 1e6,
4070 1e160,
4071 f64::MAX,
4072 ];
4073 for &(epsilon, log_delta) in ¶ms {
4074 for &eta in &etas {
4075 let mu = sas_inverse_link_jet(eta, epsilon, log_delta)
4076 .expect("finite SAS eta")
4077 .mu;
4078 let complement = sas_link_complement(eta, epsilon, log_delta, mu);
4079 assert!(
4080 complement.is_finite() && (0.0..=1.0).contains(&complement),
4081 "SAS complement out of [0,1] at η={eta} ε={epsilon} log_δ={log_delta}: {complement}"
4082 );
4083 let sum = mu + complement;
4086 assert!(
4087 (sum - 1.0).abs() < 1e-12,
4088 "SAS complement off the forward surface at η={eta} ε={epsilon} \
4089 log_δ={log_delta}: μ={mu} complement={complement} μ+comp={sum}"
4090 );
4091 }
4092 }
4093 }
4094
4095 #[test]
4106 fn beta_logistic_shapes_are_bounded_and_interior_exact_2685() {
4107 for &(center, epsilon) in &[(0.0, 0.0), (0.5, -0.25), (-0.7, 0.3)] {
4109 let (a, b) = beta_logistic_shapes(center, epsilon);
4110 assert_eq!(a.to_bits(), (center - epsilon).exp().to_bits());
4111 assert_eq!(b.to_bits(), (center + epsilon).exp().to_bits());
4112 }
4113
4114 let floor = (-BETA_LOGISTIC_LOG_SHAPE_BOUND).exp();
4115 let ceiling = BETA_LOGISTIC_LOG_SHAPE_BOUND.exp();
4116 for &(center, epsilon) in &[
4118 (-5.246_934_642_530_043_f64, -0.458_486_194_756_360_9_f64),
4119 (5.246_934_642_530_043, 0.458_486_194_756_360_9),
4120 (-40.0, 12.0),
4121 (40.0, -12.0),
4122 ] {
4123 let (a, b) = beta_logistic_shapes(center, epsilon);
4124 assert!(
4125 (floor..=ceiling).contains(&a) && (floor..=ceiling).contains(&b),
4126 "beta-logistic shapes escaped [{floor:e}, {ceiling:e}] at (center={center}, epsilon={epsilon}): a={a:e} b={b:e}"
4127 );
4128 }
4129
4130 let jet = beta_logistic_inverse_link_jet(
4135 0.0,
4136 -5.246_934_642_530_043,
4137 -0.458_486_194_756_360_9,
4138 );
4139 assert!(
4140 jet.d1 > 1.0e-2,
4141 "bounded beta-logistic link sensitivity collapsed at the runaway theta: d1={} (canonical logit is 0.25)",
4142 jet.d1
4143 );
4144 }
4145
4146 #[test]
4152 fn beta_logistic_param_partials_matchfd_inside_the_shape_splice_2685() {
4153 let bound = BETA_LOGISTIC_LOG_SHAPE_BOUND;
4154 let eta = -0.41;
4155 let center = bound;
4158 let epsilon = 0.15 * bound;
4159 assert!((0.8 * bound..1.2 * bound).contains(&(center - epsilon)));
4160 assert!((0.8 * bound..1.2 * bound).contains(&(center + epsilon)));
4161
4162 let out = beta_logistic_inverse_link_jetwith_param_partials(eta, center, epsilon);
4163 let h = 1e-6;
4164 let fd = |plus: InverseLinkJet, minus: InverseLinkJet| InverseLinkJet {
4165 mu: (plus.mu - minus.mu) / (2.0 * h),
4166 d1: (plus.d1 - minus.d1) / (2.0 * h),
4167 d2: (plus.d2 - minus.d2) / (2.0 * h),
4168 d3: (plus.d3 - minus.d3) / (2.0 * h),
4169 };
4170 let fd_center = fd(
4171 beta_logistic_inverse_link_jet(eta, center + h, epsilon),
4172 beta_logistic_inverse_link_jet(eta, center - h, epsilon),
4173 );
4174 let fd_epsilon = fd(
4175 beta_logistic_inverse_link_jet(eta, center, epsilon + h),
4176 beta_logistic_inverse_link_jet(eta, center, epsilon - h),
4177 );
4178 let close = |analytic: f64, numeric: f64, what: &str| {
4181 let tol = 1.0e-7 + 1.0e-5 * analytic.abs().max(numeric.abs());
4182 assert!(
4183 (analytic - numeric).abs() <= tol,
4184 "{what}: analytic={analytic:e} fd={numeric:e} tol={tol:e}"
4185 );
4186 };
4187 close(out.djet_dlog_delta.mu, fd_center.mu, "dmu/dcenter");
4188 close(out.djet_dlog_delta.d1, fd_center.d1, "dd1/dcenter");
4189 close(out.djet_dlog_delta.d2, fd_center.d2, "dd2/dcenter");
4190 close(out.djet_dlog_delta.d3, fd_center.d3, "dd3/dcenter");
4191 close(out.djet_depsilon.mu, fd_epsilon.mu, "dmu/depsilon");
4192 close(out.djet_depsilon.d1, fd_epsilon.d1, "dd1/depsilon");
4193 close(out.djet_depsilon.d2, fd_epsilon.d2, "dd2/depsilon");
4194 close(out.djet_depsilon.d3, fd_epsilon.d3, "dd3/depsilon");
4195
4196 assert!(out.djet_dlog_delta.mu.abs() > 1.0e-6);
4199 assert!(out.djet_depsilon.mu.abs() > 1.0e-6);
4200 }
4201
4202 #[test]
4203 fn beta_logistic_param_partials_matchfd() {
4204 let eta = -0.41;
4205 let delta = 0.23;
4206 let epsilon = -0.17;
4207 let out = beta_logistic_inverse_link_jetwith_param_partials(eta, delta, epsilon);
4208 let h = 1e-6;
4209
4210 let dp = beta_logistic_inverse_link_jet(eta, delta + h, epsilon);
4211 let dm = beta_logistic_inverse_link_jet(eta, delta - h, epsilon);
4212 let fd_delta = InverseLinkJet {
4213 mu: (dp.mu - dm.mu) / (2.0 * h),
4214 d1: (dp.d1 - dm.d1) / (2.0 * h),
4215 d2: (dp.d2 - dm.d2) / (2.0 * h),
4216 d3: (dp.d3 - dm.d3) / (2.0 * h),
4217 };
4218 assert_eq!(out.djet_dlog_delta.mu.signum(), fd_delta.mu.signum());
4219 assert_eq!(out.djet_dlog_delta.d1.signum(), fd_delta.d1.signum());
4220 assert_eq!(out.djet_dlog_delta.d2.signum(), fd_delta.d2.signum());
4221 assert_eq!(out.djet_dlog_delta.d3.signum(), fd_delta.d3.signum());
4222 assert!((out.djet_dlog_delta.mu - fd_delta.mu).abs() < 5e-5);
4223 assert!((out.djet_dlog_delta.d1 - fd_delta.d1).abs() < 5e-5);
4224 assert!((out.djet_dlog_delta.d2 - fd_delta.d2).abs() < 1.2e-4);
4225 assert!((out.djet_dlog_delta.d3 - fd_delta.d3).abs() < 4e-4);
4226
4227 let ep = beta_logistic_inverse_link_jet(eta, delta, epsilon + h);
4228 let em = beta_logistic_inverse_link_jet(eta, delta, epsilon - h);
4229 let fd_epsilon = InverseLinkJet {
4230 mu: (ep.mu - em.mu) / (2.0 * h),
4231 d1: (ep.d1 - em.d1) / (2.0 * h),
4232 d2: (ep.d2 - em.d2) / (2.0 * h),
4233 d3: (ep.d3 - em.d3) / (2.0 * h),
4234 };
4235 assert_eq!(out.djet_depsilon.mu.signum(), fd_epsilon.mu.signum());
4236 assert_eq!(out.djet_depsilon.d1.signum(), fd_epsilon.d1.signum());
4237 assert_eq!(out.djet_depsilon.d2.signum(), fd_epsilon.d2.signum());
4238 assert_eq!(out.djet_depsilon.d3.signum(), fd_epsilon.d3.signum());
4239 assert!((out.djet_depsilon.mu - fd_epsilon.mu).abs() < 5e-5);
4240 assert!((out.djet_depsilon.d1 - fd_epsilon.d1).abs() < 5e-5);
4241 assert!((out.djet_depsilon.d2 - fd_epsilon.d2).abs() < 1.2e-4);
4242 assert!((out.djet_depsilon.d3 - fd_epsilon.d3).abs() < 4e-4);
4243 }
4244
4245 #[test]
4246 fn beta_logistic_second_partials_obey_center_symmetry() {
4247 let out = beta_logistic_inverse_link_jetwith_param_partials(0.0, 0.37, 0.0);
4248 assert_eq!(out.d2mu_dparams2[[0, 1]], out.d2mu_dparams2[[1, 0]]);
4253 assert_eq!(out.d2d1_dparams2[[0, 1]], out.d2d1_dparams2[[1, 0]]);
4254 assert!(out.d2mu_dparams2[[0, 0]].abs() < 1.0e-12);
4255 assert!(out.d2mu_dparams2[[1, 1]].abs() < 1.0e-12);
4256 assert!(out.d2d1_dparams2[[0, 1]].abs() < 1.0e-12);
4257 }
4258
4259 #[test]
4260 fn beta_logistic_left_tail_uses_unclamped_log_space() {
4261 let eta = -40.0_f64;
4262 let delta = 0.2_f64;
4263 let epsilon = -0.1_f64;
4264 let a = (delta - epsilon).exp();
4265 let b = (delta + epsilon).exp();
4266 let expected_mu = beta_reg(a, b, eta.exp());
4267 let out = beta_logistic_inverse_link_jet(eta, delta, epsilon);
4268
4269 assert!(
4270 (out.mu - expected_mu).abs() <= 1e-12 * expected_mu.abs().max(f64::MIN_POSITIVE),
4271 "left-tail mu mismatch: got {}, expected {}",
4272 out.mu,
4273 expected_mu
4274 );
4275 assert!(out.d1 > 0.0);
4276 assert!(out.d2 > 0.0);
4277 assert!(out.d3 > 0.0);
4278 assert!(out.d1 < 1e-20);
4279
4280 let partials = beta_logistic_inverse_link_jetwith_param_partials(eta, delta, epsilon);
4281 assert!(partials.jet.d1 > 0.0);
4282 assert!(partials.jet.d2 > 0.0);
4283 assert!(partials.jet.d3 > 0.0);
4284 assert!(partials.djet_dlog_delta.d1.is_finite());
4285 assert!(partials.djet_depsilon.d1.is_finite());
4286 }
4287
4288 #[test]
4289 fn beta_logistic_mu_is_symmetric_in_logistic_tails() {
4290 let delta = 0.2;
4291 let epsilon = -0.35;
4292 let etas = [-40.0, -30.0, -5.0, -0.42, 0.0, 0.42, 5.0, 30.0, 40.0];
4293 for eta in etas {
4294 let left = beta_logistic_inverse_link_jet(eta, delta, epsilon).mu;
4295 let right = 1.0 - beta_logistic_inverse_link_jet(-eta, delta, -epsilon).mu;
4296 assert!(
4297 (left - right).abs() <= 1e-14,
4298 "symmetry mismatch at eta={eta}: left={left}, right={right}"
4299 );
4300 }
4301 }
4302
4303 #[test]
4304 fn inverse_link_pdfthird_derivative_matches_d3_finite_difference() {
4305 let sas = InverseLink::Sas(sas_link_state_from_raw(-0.25, 0.35).expect("sas state"));
4306 let beta_logistic = InverseLink::BetaLogistic(SasLinkState {
4307 epsilon: 0.18,
4308 log_delta: -0.22,
4309 delta: (-0.22_f64).exp(),
4310 });
4311 let mixture = InverseLink::Mixture(
4312 state_fromspec(&MixtureLinkSpec {
4313 components: vec![
4314 LinkComponent::Probit,
4315 LinkComponent::Logit,
4316 LinkComponent::CLogLog,
4317 LinkComponent::Cauchit,
4318 ],
4319 initial_rho: Array1::from_vec(vec![0.35, -0.45, 0.2]),
4320 })
4321 .expect("mixture state"),
4322 );
4323 let links = [
4324 InverseLink::Standard(StandardLink::Probit),
4325 InverseLink::Standard(StandardLink::Logit),
4326 InverseLink::Standard(StandardLink::CLogLog),
4327 sas,
4328 beta_logistic,
4329 mixture,
4330 ];
4331 let etas = [-1.1, -0.2, 0.6];
4332 let h = 1e-5;
4333
4334 for link in &links {
4335 for &eta in &etas {
4336 let jp = inverse_link_jet_for_inverse_link(link, eta + h).expect("jet+");
4337 let jm = inverse_link_jet_for_inverse_link(link, eta - h).expect("jet-");
4338 let d4fd = (jp.d3 - jm.d3) / (2.0 * h);
4339 let d4 = inverse_link_pdfthird_derivative_for_inverse_link(link, eta)
4340 .expect("analytic d4");
4341 assert_eq!(
4342 d4.signum(),
4343 d4fd.signum(),
4344 "d4 sign mismatch for {:?} at eta={eta}: analytic={} fd={}",
4345 link,
4346 d4,
4347 d4fd
4348 );
4349 assert!(
4350 (d4 - d4fd).abs() < 5e-3,
4351 "d4 mismatch for {:?} at eta={eta}: analytic={} fd={}",
4352 link,
4353 d4,
4354 d4fd
4355 );
4356 }
4357 }
4358 }
4359
4360 #[test]
4361 fn cloglog_large_finite_eta_should_saturate_without_nan_derivatives() {
4362 let eta = 800.0;
4363 let jet = component_inverse_link_jet(LinkComponent::CLogLog, eta);
4364 assert_eq!(jet.mu, 1.0);
4365 assert!(
4366 jet.d1 == 0.0,
4367 "for mu(eta)=1-exp(-exp(eta)), dmu/deta = exp(eta-exp(eta)) and should underflow to 0 at eta={eta}; got d1={}",
4368 jet.d1
4369 );
4370 assert!(
4371 jet.d2 == 0.0,
4372 "the saturated cloglog second derivative should also be 0 at eta={eta}; got d2={}",
4373 jet.d2
4374 );
4375 assert!(
4376 jet.d3 == 0.0,
4377 "the saturated cloglog third derivative should also be 0 at eta={eta}; got d3={}",
4378 jet.d3
4379 );
4380
4381 let d4 = inverse_link_pdfthird_derivative_for_inverse_link(
4382 &InverseLink::Standard(StandardLink::CLogLog),
4383 eta,
4384 )
4385 .expect("cloglog d4");
4386 assert!(
4387 d4 == 0.0,
4388 "the saturated cloglog fourth derivative should also be 0 at eta={eta}; got d4={d4}"
4389 );
4390 }
4391
4392 #[test]
4393 fn loglog_large_negative_finite_eta_should_saturate_without_nan_derivatives() {
4394 let eta = -800.0;
4395 let jet = component_inverse_link_jet(LinkComponent::LogLog, eta);
4396 assert_eq!(jet.mu, 0.0);
4397 assert!(
4398 jet.d1 == 0.0,
4399 "for mu(eta)=exp(-exp(-eta)), dmu/deta = exp(-eta-exp(-eta)) and should underflow to 0 at eta={eta}; got d1={}",
4400 jet.d1
4401 );
4402 assert!(
4403 jet.d2 == 0.0,
4404 "the saturated loglog second derivative should also be 0 at eta={eta}; got d2={}",
4405 jet.d2
4406 );
4407 assert!(
4408 jet.d3 == 0.0,
4409 "the saturated loglog third derivative should also be 0 at eta={eta}; got d3={}",
4410 jet.d3
4411 );
4412
4413 let d4 = inverse_link_pdfthird_derivative_for_inverse_link(
4414 &InverseLink::Mixture(
4415 state_fromspec(&MixtureLinkSpec {
4416 components: vec![LinkComponent::LogLog, LinkComponent::Probit],
4417 initial_rho: Array1::from_vec(vec![12.0]),
4418 })
4419 .expect("mixture state"),
4420 ),
4421 eta,
4422 )
4423 .expect("loglog mixture d4");
4424 assert!(
4425 d4.is_finite(),
4426 "even a nearly pure loglog mixture should not produce NaN fourth derivatives at eta={eta}; got d4={d4}"
4427 );
4428 }
4429
4430 #[test]
4431 fn logit_tail_derivatives_should_match_stable_closed_forms() {
4432 let eta = 50.0_f64;
4433 let z = (-eta).exp();
4434 let denom = 1.0_f64 + z;
4435 let stable_d1 = z / denom.powi(2);
4436 let stable_d2 = z * (z - 1.0) / denom.powi(3);
4437 let stable_d3 = z * (z * z - 4.0 * z + 1.0) / denom.powi(4);
4438 let stable_d4 = z * (z * z * z - 11.0 * z * z + 11.0 * z - 1.0) / denom.powi(5);
4439 let stable_d5 =
4440 z * (z * z * z * z - 26.0 * z * z * z + 66.0 * z * z - 26.0 * z + 1.0) / denom.powi(6);
4441
4442 assert!(stable_d1 > 0.0);
4443 assert!(stable_d2 < 0.0);
4444 assert!(stable_d3 > 0.0);
4445 assert!(stable_d4 < 0.0);
4446 assert!(stable_d5 > 0.0);
4447
4448 let jet = component_inverse_link_jet(LinkComponent::Logit, eta);
4449 assert!(
4450 (jet.d1 - stable_d1).abs() < 1e-30,
4451 "logit d1 should equal the stable tail formula z/(1+z)^2 at eta={eta}; got {} vs {}",
4452 jet.d1,
4453 stable_d1
4454 );
4455 assert!(
4456 (jet.d2 - stable_d2).abs() < 1e-30,
4457 "logit d2 should equal the stable tail formula z(z-1)/(1+z)^3 at eta={eta}; got {} vs {}",
4458 jet.d2,
4459 stable_d2
4460 );
4461 assert!(
4462 (jet.d3 - stable_d3).abs() < 1e-30,
4463 "logit d3 should equal the stable tail formula z(z^2-4z+1)/(1+z)^4 at eta={eta}; got {} vs {}",
4464 jet.d3,
4465 stable_d3
4466 );
4467
4468 let d4 = inverse_link_pdfthird_derivative_for_inverse_link(
4469 &InverseLink::Standard(StandardLink::Logit),
4470 eta,
4471 )
4472 .expect("logit d4");
4473 assert!(
4474 (d4 - stable_d4).abs() < 1e-30,
4475 "logit d4 should equal the stable tail formula z(z^3-11z^2+11z-1)/(1+z)^5 at eta={eta}; got {} vs {}",
4476 d4,
4477 stable_d4
4478 );
4479
4480 let d5 = inverse_link_pdffourth_derivative_for_inverse_link(
4481 &InverseLink::Standard(StandardLink::Logit),
4482 eta,
4483 )
4484 .expect("logit d5");
4485 assert!(
4486 (d5 - stable_d5).abs() < 1e-30,
4487 "logit d5 should equal the stable tail formula z(z^4-26z^3+66z^2-26z+1)/(1+z)^6 at eta={eta}; got {} vs {}",
4488 d5,
4489 stable_d5
4490 );
4491 }
4492
4493 #[test]
4494 fn cloglog_negative_tail_value_should_match_expm1_form() {
4495 let eta = -50.0_f64;
4496 let t = eta.exp();
4497 let stable_mu = -(-t).exp_m1();
4498 assert!(stable_mu > 0.0);
4499
4500 let jet = component_inverse_link_jet(LinkComponent::CLogLog, eta);
4501 assert!(
4502 (jet.mu - stable_mu).abs() < 1e-30,
4503 "cloglog mu should equal -expm1(-exp(eta)) in the negative tail at eta={eta}; got {} vs {}",
4504 jet.mu,
4505 stable_mu
4506 );
4507 }
4508
4509 #[test]
4510 fn non_logit_probit_fisher_weight_jets_match_finite_differences() {
4511 fn rel_err(a: f64, b: f64) -> f64 {
4512 (a - b).abs() / a.abs().max(b.abs()).max(1.0e-8)
4513 }
4514
4515 let cases = [
4516 (LinkComponent::CLogLog, [-3.0_f64, -0.5, 0.4, 1.5]),
4517 (LinkComponent::LogLog, [-1.5_f64, -0.4, 0.5, 3.0]),
4518 (LinkComponent::Cauchit, [-3.0_f64, -0.7, 0.6, 3.0]),
4519 ];
4520 for (component, etas) in cases {
4521 for eta in etas {
4522 let (w, w1, w2, w3, w4) = component_fisher_weight_jet5(component, eta);
4523 let jet = component_inverse_link_jet(component, eta);
4524 let expected = jet.d1 * jet.d1 / (jet.mu * (1.0 - jet.mu));
4525 assert!(
4526 rel_err(w, expected) < 1.0e-12,
4527 "{component:?} Fisher weight mismatch at eta={eta}: got {w}, expected {expected}"
4528 );
4529
4530 let h = 1.0e-4;
4531 let fd1 = (component_fisher_weight_jet5(component, eta + h).0
4532 - component_fisher_weight_jet5(component, eta - h).0)
4533 / (2.0 * h);
4534 let fd2 = (component_fisher_weight_jet5(component, eta + h).1
4535 - component_fisher_weight_jet5(component, eta - h).1)
4536 / (2.0 * h);
4537 let fd3 = (component_fisher_weight_jet5(component, eta + h).2
4538 - component_fisher_weight_jet5(component, eta - h).2)
4539 / (2.0 * h);
4540 let fd4 = (component_fisher_weight_jet5(component, eta + h).3
4541 - component_fisher_weight_jet5(component, eta - h).3)
4542 / (2.0 * h);
4543
4544 assert!(
4545 rel_err(w1, fd1) < 1.0e-5,
4546 "{component:?} W' mismatch at eta={eta}: {w1} vs {fd1}"
4547 );
4548 assert!(
4549 rel_err(w2, fd2) < 1.0e-5,
4550 "{component:?} W'' mismatch at eta={eta}: {w2} vs {fd2}"
4551 );
4552 assert!(
4553 rel_err(w3, fd3) < 5.0e-5,
4554 "{component:?} W''' mismatch at eta={eta}: {w3} vs {fd3}"
4555 );
4556 assert!(
4557 rel_err(w4, fd4) < 5.0e-4,
4558 "{component:?} W'''' mismatch at eta={eta}: {w4} vs {fd4}"
4559 );
4560 }
4561 }
4562 }
4563
4564 #[test]
4565 fn mixture_fisher_weight_jet_covers_loglog_and_cauchit_components() {
4566 let state = state_fromspec(&MixtureLinkSpec {
4567 components: vec![
4568 LinkComponent::CLogLog,
4569 LinkComponent::LogLog,
4570 LinkComponent::Cauchit,
4571 ],
4572 initial_rho: Array1::from_vec(vec![0.3, -0.2]),
4573 })
4574 .expect("mixture state");
4575 let link = InverseLink::Mixture(state);
4576 assert!(
4577 link.has_fisher_weight_jet(),
4578 "anchored mixtures with loglog/cauchit components must remain eligible for Firth"
4579 );
4580 assert!(
4581 LikelihoodSpec::new(ResponseFamily::Binomial, link.clone()).supports_firth(),
4582 "Firth support should use the mixture inverse-link Fisher jet, not standalone LinkFunction coverage"
4583 );
4584
4585 for eta in [-2.0_f64, -0.25, 0.75, 2.5] {
4586 let (w, w1, w2, w3, w4) =
4587 fisher_weight_jet5_for_inverse_link(&link, eta).expect("mixture Fisher jet");
4588 for value in [w, w1, w2, w3, w4] {
4589 assert!(
4590 value.is_finite(),
4591 "mixture Fisher weight jet should be finite at eta={eta}; got {value}"
4592 );
4593 }
4594 assert!(
4595 w > 0.0,
4596 "mixture Fisher working weight should be positive away from saturated tails at eta={eta}; got {w}"
4597 );
4598 }
4599 }
4600
4601 #[test]
4602 fn loglog_fifth_derivative_should_match_closed_form_sign() {
4603 let eta = 0.0_f64;
4604 let r = (-eta).exp();
4605 let expected =
4606 (-r).exp() * (r - 15.0 * r * r + 25.0 * r.powi(3) - 10.0 * r.powi(4) + r.powi(5));
4607 let d5 = component_inverse_link_pdffourth_derivative(LinkComponent::LogLog, eta);
4608 assert!(
4609 (d5 - expected).abs() < 1e-15,
4610 "loglog d5 should equal exp(-r) * (r - 15r^2 + 25r^3 - 10r^4 + r^5) at eta={eta}; got {d5} vs {expected}"
4611 );
4612 assert!(d5 > 0.0, "loglog d5 should be positive at eta=0; got {d5}");
4613 }
4614}