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 softmaxwith_jacobian_last_fixedzero(
996 rho: &Array1<f64>,
997) -> (Array1<f64>, ndarray::Array2<f64>) {
998 let pi = softmax_last_fixedzero(rho);
999 let k = pi.len();
1000 let m = k.saturating_sub(1);
1001 let mut jac = ndarray::Array2::<f64>::zeros((k, m));
1002 for j in 0..m {
1003 let pi_j = pi[j];
1004 for kk in 0..k {
1005 let delta = if kk == j { 1.0 } else { 0.0 };
1006 jac[[kk, j]] = pi[kk] * (delta - pi_j);
1007 }
1008 }
1009 (pi, jac)
1010}
1011
1012pub fn state_fromspec(spec: &MixtureLinkSpec) -> Result<MixtureLinkState, String> {
1013 validate_mixturespec(spec)?;
1014 let pi = softmax_last_fixedzero(&spec.initial_rho);
1015 Ok(MixtureLinkState {
1016 components: spec.components.clone(),
1017 rho: spec.initial_rho.clone(),
1018 pi,
1019 })
1020}
1021
1022#[inline]
1023pub fn component_inverse_link_jet(component: LinkComponent, eta: f64) -> InverseLinkJet {
1024 canonicalize_jet(match component {
1025 LinkComponent::Logit => {
1026 let jet = logit_inverse_link_jet5(eta);
1027 InverseLinkJet {
1028 mu: jet.mu,
1029 d1: jet.d1,
1030 d2: jet.d2,
1031 d3: jet.d3,
1032 }
1033 }
1034 LinkComponent::Probit => probit_jet(eta),
1035 LinkComponent::CLogLog => {
1036 if eta.is_nan() {
1037 return InverseLinkJet {
1038 mu: f64::NAN,
1039 d1: f64::NAN,
1040 d2: f64::NAN,
1041 d3: f64::NAN,
1042 };
1043 }
1044 let t = eta.exp();
1045 if !t.is_finite() {
1046 return InverseLinkJet {
1047 mu: 1.0,
1048 d1: 0.0,
1049 d2: 0.0,
1050 d3: 0.0,
1051 };
1052 }
1053 InverseLinkJet {
1054 mu: -(-t).exp_m1(),
1055 d1: stable_nonnegative_poly_times_exp_neg(t, &[0.0, 1.0]),
1056 d2: stable_nonnegative_poly_times_exp_neg(t, &[0.0, 1.0, -1.0]),
1057 d3: stable_nonnegative_poly_times_exp_neg(t, &[0.0, 1.0, -3.0, 1.0]),
1058 }
1059 }
1060 LinkComponent::LogLog => {
1061 if eta.is_nan() {
1062 return InverseLinkJet {
1063 mu: f64::NAN,
1064 d1: f64::NAN,
1065 d2: f64::NAN,
1066 d3: f64::NAN,
1067 };
1068 }
1069 let r = (-eta).exp();
1070 if !r.is_finite() {
1071 return InverseLinkJet {
1072 mu: 0.0,
1073 d1: 0.0,
1074 d2: 0.0,
1075 d3: 0.0,
1076 };
1077 }
1078 InverseLinkJet {
1079 mu: (-r).exp(),
1080 d1: stable_nonnegative_poly_times_exp_neg(r, &[0.0, 1.0]),
1081 d2: stable_nonnegative_poly_times_exp_neg(r, &[0.0, -1.0, 1.0]),
1082 d3: stable_nonnegative_poly_times_exp_neg(r, &[0.0, 1.0, -3.0, 1.0]),
1083 }
1084 }
1085 LinkComponent::Cauchit => {
1086 if eta.is_nan() {
1087 return InverseLinkJet {
1088 mu: f64::NAN,
1089 d1: f64::NAN,
1090 d2: f64::NAN,
1091 d3: f64::NAN,
1092 };
1093 }
1094 let den = 1.0 + eta * eta;
1095 let d1 = if eta.is_finite() {
1096 1.0 / (std::f64::consts::PI * den)
1097 } else {
1098 0.0
1099 };
1100 let d2 = if eta.is_finite() {
1101 -2.0 * eta / (std::f64::consts::PI * den * den)
1102 } else {
1103 0.0
1104 };
1105 let d3 = if eta.is_finite() {
1106 (6.0 * eta * eta - 2.0) / (std::f64::consts::PI * den * den * den)
1107 } else {
1108 0.0
1109 };
1110 InverseLinkJet {
1111 mu: 0.5 + eta.atan() / std::f64::consts::PI,
1112 d1,
1113 d2,
1114 d3,
1115 }
1116 }
1117 })
1118}
1119
1120impl InverseLinkKernel for ProbitLinkKernel {
1121 #[inline]
1122 fn jet(&self, eta: f64) -> Result<InverseLinkJet, EstimationError> {
1123 Ok(component_inverse_link_jet(LinkComponent::Probit, eta))
1124 }
1125}
1126
1127impl InverseLinkKernel for LogitLinkKernel {
1128 #[inline]
1129 fn jet(&self, eta: f64) -> Result<InverseLinkJet, EstimationError> {
1130 Ok(component_inverse_link_jet(LinkComponent::Logit, eta))
1131 }
1132}
1133
1134impl InverseLinkKernel for CLogLogLinkKernel {
1135 #[inline]
1136 fn jet(&self, eta: f64) -> Result<InverseLinkJet, EstimationError> {
1137 Ok(component_inverse_link_jet(LinkComponent::CLogLog, eta))
1138 }
1139}
1140
1141impl InverseLinkKernel for LogLogLinkKernel {
1142 #[inline]
1143 fn jet(&self, eta: f64) -> Result<InverseLinkJet, EstimationError> {
1144 Ok(component_inverse_link_jet(LinkComponent::LogLog, eta))
1145 }
1146}
1147
1148impl InverseLinkKernel for CauchitLinkKernel {
1149 #[inline]
1150 fn jet(&self, eta: f64) -> Result<InverseLinkJet, EstimationError> {
1151 Ok(component_inverse_link_jet(LinkComponent::Cauchit, eta))
1152 }
1153}
1154
1155impl InverseLinkKernel for LinkComponent {
1156 #[inline]
1157 fn jet(&self, eta: f64) -> Result<InverseLinkJet, EstimationError> {
1158 Ok(component_inverse_link_jet(*self, eta))
1159 }
1160}
1161
1162impl InverseLinkKernel for LinkFunction {
1163 fn jet(&self, eta: f64) -> Result<InverseLinkJet, EstimationError> {
1164 match self {
1165 LinkFunction::Logit => LogitLinkKernel.jet(eta),
1166 LinkFunction::Probit => ProbitLinkKernel.jet(eta),
1167 LinkFunction::CLogLog => CLogLogLinkKernel.jet(eta),
1168 LinkFunction::LogLog => LogLogLinkKernel.jet(eta),
1169 LinkFunction::Cauchit => CauchitLinkKernel.jet(eta),
1170 LinkFunction::Identity => Ok(InverseLinkJet {
1171 mu: eta,
1172 d1: 1.0,
1173 d2: 0.0,
1174 d3: 0.0,
1175 }),
1176 LinkFunction::Log => {
1177 let e = log_link_solver_exp(eta)?;
1185 Ok(InverseLinkJet {
1186 mu: e,
1187 d1: e,
1188 d2: e,
1189 d3: e,
1190 })
1191 }
1192 LinkFunction::Sas => Err(EstimationError::InvalidInput(
1193 "LinkFunction::Sas inverse-link requires explicit SAS link state".to_string(),
1194 )),
1195 LinkFunction::BetaLogistic => Err(EstimationError::InvalidInput(
1196 "LinkFunction::BetaLogistic inverse-link requires explicit Beta-Logistic link state"
1197 .to_string(),
1198 )),
1199 }
1200 }
1201}
1202
1203impl InverseLinkKernel for SasLinkState {
1204 fn jet(&self, eta: f64) -> Result<InverseLinkJet, EstimationError> {
1205 sas_inverse_link_jet(eta, self.epsilon, self.log_delta)
1206 }
1207
1208 fn param_partials(&self, eta: f64) -> Result<Option<LinkParamPartials>, EstimationError> {
1209 Ok(Some(LinkParamPartials::Sas(
1210 sas_inverse_link_jetwith_param_partials(eta, self.epsilon, self.log_delta)?,
1211 )))
1212 }
1213}
1214
1215#[derive(Clone, Copy, Debug)]
1216pub struct BetaLogisticKernel {
1217 pub log_shape_center: f64,
1220 pub epsilon: f64,
1221}
1222
1223impl InverseLinkKernel for BetaLogisticKernel {
1224 fn jet(&self, eta: f64) -> Result<InverseLinkJet, EstimationError> {
1225 Ok(beta_logistic_inverse_link_jet(
1226 eta,
1227 self.log_shape_center,
1228 self.epsilon,
1229 ))
1230 }
1231
1232 fn param_partials(&self, eta: f64) -> Result<Option<LinkParamPartials>, EstimationError> {
1233 Ok(Some(LinkParamPartials::Sas(
1234 beta_logistic_inverse_link_jetwith_param_partials(
1235 eta,
1236 self.log_shape_center,
1237 self.epsilon,
1238 ),
1239 )))
1240 }
1241}
1242
1243impl InverseLinkKernel for MixtureLinkState {
1244 fn jet(&self, eta: f64) -> Result<InverseLinkJet, EstimationError> {
1245 Ok(mixture_inverse_link_jet(self, eta))
1246 }
1247
1248 fn param_partials(&self, eta: f64) -> Result<Option<LinkParamPartials>, EstimationError> {
1249 Ok(Some(LinkParamPartials::Mixture(
1250 mixture_inverse_link_jetwith_rho_partials(self, eta),
1251 )))
1252 }
1253}
1254
1255impl InverseLinkKernel for InverseLink {
1256 fn jet(&self, eta: f64) -> Result<InverseLinkJet, EstimationError> {
1257 match self {
1258 InverseLink::Standard(StandardLink::Logit) => LogitLinkKernel.jet(eta),
1259 InverseLink::Standard(StandardLink::Probit) => ProbitLinkKernel.jet(eta),
1260 InverseLink::Standard(StandardLink::CLogLog) => CLogLogLinkKernel.jet(eta),
1261 InverseLink::Standard(StandardLink::LogLog) => LogLogLinkKernel.jet(eta),
1262 InverseLink::Standard(StandardLink::Cauchit) => CauchitLinkKernel.jet(eta),
1263 InverseLink::Standard(StandardLink::Identity) => LinkFunction::Identity.jet(eta),
1264 InverseLink::Standard(StandardLink::Log) => LinkFunction::Log.jet(eta),
1265 InverseLink::LatentCLogLog(state) => latent_cloglog_point_jet(state, eta),
1266 InverseLink::Sas(state) => state.jet(eta),
1267 InverseLink::BetaLogistic(state) => BetaLogisticKernel {
1268 log_shape_center: state.log_delta,
1269 epsilon: state.epsilon,
1270 }
1271 .jet(eta),
1272 InverseLink::Mixture(state) => state.jet(eta),
1273 }
1274 }
1275
1276 fn param_partials(&self, eta: f64) -> Result<Option<LinkParamPartials>, EstimationError> {
1277 match self {
1278 InverseLink::Standard(_) => Ok(None),
1279 InverseLink::LatentCLogLog(_) => Ok(None),
1280 InverseLink::Sas(state) => state.param_partials(eta),
1281 InverseLink::BetaLogistic(state) => BetaLogisticKernel {
1282 log_shape_center: state.log_delta,
1283 epsilon: state.epsilon,
1284 }
1285 .param_partials(eta),
1286 InverseLink::Mixture(state) => state.param_partials(eta),
1287 }
1288 }
1289}
1290
1291pub fn inverse_link_jet_for_inverse_link(
1298 link: &InverseLink,
1299 eta: f64,
1300) -> Result<InverseLinkJet, EstimationError> {
1301 link.jet(eta)
1302}
1303
1304pub fn inverse_link_mu_d1_for_inverse_link(
1315 link: &InverseLink,
1316 eta: f64,
1317) -> Result<(f64, f64), EstimationError> {
1318 match link {
1319 InverseLink::Standard(link_fn) => Ok(link_function_mu_d1(link_fn.as_link_function(), eta)?),
1320 InverseLink::LatentCLogLog(state) => {
1321 let jet = latent_cloglog_point_jet(state, eta)?;
1322 Ok((jet.mu, jet.d1))
1323 }
1324 InverseLink::Sas(state) => sas_inverse_link_mu_d1(eta, state.epsilon, state.log_delta),
1325 InverseLink::BetaLogistic(state) => Ok(beta_logistic_inverse_link_mu_d1(
1326 eta,
1327 state.log_delta,
1328 state.epsilon,
1329 )),
1330 InverseLink::Mixture(state) => Ok(mixture_inverse_link_mu_d1(state, eta)),
1331 }
1332}
1333
1334pub(crate) fn inverse_link_complement_for_inverse_link(
1349 link: &InverseLink,
1350 eta: f64,
1351 mu: f64,
1352) -> f64 {
1353 let raw = match link {
1354 InverseLink::Standard(link_fn) => standard_link_complement(*link_fn, eta, mu),
1355 InverseLink::Sas(state) => sas_link_complement(eta, state.epsilon, state.log_delta, mu),
1356 InverseLink::BetaLogistic(state) => {
1357 beta_logistic_link_complement(eta, state.log_delta, state.epsilon, mu)
1358 }
1359 InverseLink::Mixture(state) => mixture_link_complement(state, eta, mu),
1360 InverseLink::LatentCLogLog(_) => 1.0 - mu,
1368 };
1369 if raw.is_nan() {
1370 raw
1371 } else {
1372 raw.clamp(0.0, 1.0)
1373 }
1374}
1375
1376#[inline]
1378fn standard_link_complement(link: StandardLink, eta: f64, mu: f64) -> f64 {
1379 match link {
1380 StandardLink::Probit => {
1381 if eta.is_nan() {
1384 f64::NAN
1385 } else if eta == f64::INFINITY {
1386 0.0
1387 } else if eta == f64::NEG_INFINITY {
1388 1.0
1389 } else {
1390 normal_cdf(-eta)
1391 }
1392 }
1393 StandardLink::CLogLog => {
1394 if eta.is_nan() {
1396 f64::NAN
1397 } else {
1398 let t = eta.exp();
1399 if !t.is_finite() { 0.0 } else { (-t).exp() }
1400 }
1401 }
1402 StandardLink::LogLog => {
1403 if eta.is_nan() {
1405 f64::NAN
1406 } else {
1407 let r = (-eta).exp();
1408 if !r.is_finite() { 1.0 } else { -(-r).exp_m1() }
1409 }
1410 }
1411 StandardLink::Cauchit => {
1412 if eta.is_nan() {
1416 f64::NAN
1417 } else if !eta.is_finite() {
1418 if eta > 0.0 { 0.0 } else { 1.0 }
1419 } else if eta > 0.0 {
1420 (1.0 / eta).atan() / std::f64::consts::PI
1421 } else {
1422 0.5 - eta.atan() / std::f64::consts::PI
1423 }
1424 }
1425 StandardLink::Logit | StandardLink::Identity | StandardLink::Log => 1.0 - mu,
1429 }
1430}
1431
1432#[inline]
1443fn beta_logistic_link_complement(eta: f64, log_delta: f64, epsilon: f64, mu: f64) -> f64 {
1444 let logistic = logistic_uwith_derivatives(eta);
1445 if logistic.ln_u.is_nan() || logistic.ln_one_minus_u.is_nan() {
1446 return f64::NAN;
1447 }
1448 if logistic.ln_u == f64::NEG_INFINITY {
1449 return 1.0;
1450 }
1451 if logistic.ln_one_minus_u == f64::NEG_INFINITY {
1452 return 0.0;
1453 }
1454 let (a, b) = beta_logistic_shapes(log_delta, epsilon);
1455 if logistic.use_upper_tail {
1456 beta_reg(b, a, logistic.one_minus_u)
1457 } else {
1458 1.0 - mu
1459 }
1460}
1461
1462#[inline]
1471fn mixture_link_complement(state: &MixtureLinkState, eta: f64, mu: f64) -> f64 {
1472 let k = state.components.len().min(state.pi.len());
1473 let mut weight_total = 0.0_f64;
1474 let mut complement = 0.0_f64;
1475 for i in 0..k {
1476 let (mu_i, _) = component_inverse_link_mu_d1(state.components[i], eta);
1477 if mu_i.is_nan() {
1478 return f64::NAN;
1479 }
1480 let component_complement =
1481 standard_link_complement(state.components[i].as_standard_link(), eta, mu_i);
1482 if component_complement.is_nan() {
1483 return f64::NAN;
1484 }
1485 weight_total += state.pi[i];
1486 complement += state.pi[i] * component_complement;
1487 }
1488 if k == 0 {
1489 return 1.0 - mu;
1490 }
1491 (1.0 - weight_total) + complement
1492}
1493
1494#[inline]
1512pub(crate) fn sas_link_complement(eta: f64, epsilon: f64, log_delta: f64, mu: f64) -> f64 {
1513 let eta = match finite_inverse_link_eta("SAS inverse link complement", eta) {
1514 Ok(value) => value,
1515 Err(_) => return 1.0 - mu,
1516 };
1517 let delta = sas_delta_from_raw_log_delta(log_delta);
1518 if epsilon.abs() < 1e-12 && (delta - 1.0).abs() < 1e-12 {
1519 return standard_link_complement(StandardLink::Probit, eta, mu);
1520 }
1521 let u_raw = delta * asinh_jet5(eta).value + epsilon;
1522 let u = smooth_bound_jet(u_raw, SAS_U_CLAMP).g;
1523 normal_cdf(-u.sinh())
1524}
1525
1526pub(crate) fn sas_latent_probit_argument(
1550 eta: f64,
1551 epsilon: f64,
1552 log_delta: f64,
1553) -> Result<(f64, f64), EstimationError> {
1554 let eta = finite_inverse_link_eta("SAS inverse link", eta)?;
1555 let delta = sas_delta_from_raw_log_delta(log_delta);
1556 if epsilon.abs() < 1e-12 && (delta - 1.0).abs() < 1e-12 {
1557 return Ok((eta, 1.0));
1558 }
1559 let asinh = asinh_jet5(eta);
1560 let u_raw = delta * asinh.value + epsilon;
1561 let sb = smooth_bound_jet(u_raw, SAS_U_CLAMP);
1562 let u = sb.g;
1563 let c = u.cosh();
1564 let r1 = delta * asinh.d1;
1565 let u1 = sb.d1 * r1;
1566 Ok((u.sinh(), c * u1))
1567}
1568
1569fn link_function_mu_d1(link: LinkFunction, eta: f64) -> Result<(f64, f64), EstimationError> {
1570 match link {
1571 LinkFunction::Identity => Ok((eta, 1.0)),
1572 LinkFunction::Log => {
1573 let e = log_link_solver_exp(eta)?;
1576 Ok((e, e))
1577 }
1578 LinkFunction::Logit => Ok(component_inverse_link_mu_d1(LinkComponent::Logit, eta)),
1579 LinkFunction::Probit => Ok(component_inverse_link_mu_d1(LinkComponent::Probit, eta)),
1580 LinkFunction::CLogLog => Ok(component_inverse_link_mu_d1(LinkComponent::CLogLog, eta)),
1581 LinkFunction::LogLog => Ok(component_inverse_link_mu_d1(LinkComponent::LogLog, eta)),
1582 LinkFunction::Cauchit => Ok(component_inverse_link_mu_d1(LinkComponent::Cauchit, eta)),
1583 LinkFunction::Sas => Err(EstimationError::InvalidInput(
1584 "LinkFunction::Sas inverse-link requires explicit SAS link state".to_string(),
1585 )),
1586 LinkFunction::BetaLogistic => Err(EstimationError::InvalidInput(
1587 "LinkFunction::BetaLogistic inverse-link requires explicit Beta-Logistic link state"
1588 .to_string(),
1589 )),
1590 }
1591}
1592
1593#[inline]
1594fn component_inverse_link_mu_d1(component: LinkComponent, eta: f64) -> (f64, f64) {
1595 match component {
1601 LinkComponent::Logit => {
1602 let jet = logit_inverse_link_jet5(eta);
1603 (jet.mu, canonicalzero(jet.d1))
1604 }
1605 LinkComponent::Probit => {
1606 if eta.is_nan() {
1607 return (f64::NAN, f64::NAN);
1608 }
1609 if eta == f64::INFINITY {
1610 return (1.0, 0.0);
1611 }
1612 if eta == f64::NEG_INFINITY {
1613 return (0.0, 0.0);
1614 }
1615 let phi = normal_pdf(eta);
1616 (normal_cdf(eta), canonicalzero(phi))
1617 }
1618 LinkComponent::CLogLog => {
1619 if eta.is_nan() {
1620 return (f64::NAN, f64::NAN);
1621 }
1622 let t = eta.exp();
1623 if !t.is_finite() {
1624 return (1.0, 0.0);
1625 }
1626 (
1627 -(-t).exp_m1(),
1628 canonicalzero(stable_nonnegative_poly_times_exp_neg(t, &[0.0, 1.0])),
1629 )
1630 }
1631 LinkComponent::LogLog => {
1632 if eta.is_nan() {
1633 return (f64::NAN, f64::NAN);
1634 }
1635 let r = (-eta).exp();
1636 if !r.is_finite() {
1637 return (0.0, 0.0);
1638 }
1639 (
1640 (-r).exp(),
1641 canonicalzero(stable_nonnegative_poly_times_exp_neg(r, &[0.0, 1.0])),
1642 )
1643 }
1644 LinkComponent::Cauchit => {
1645 if eta.is_nan() {
1646 return (f64::NAN, f64::NAN);
1647 }
1648 let den = 1.0 + eta * eta;
1649 let d1 = if eta.is_finite() {
1650 1.0 / (std::f64::consts::PI * den)
1651 } else {
1652 0.0
1653 };
1654 (0.5 + eta.atan() / std::f64::consts::PI, canonicalzero(d1))
1655 }
1656 }
1657}
1658
1659fn sas_inverse_link_mu_d1(
1660 eta: f64,
1661 epsilon: f64,
1662 log_delta: f64,
1663) -> Result<(f64, f64), EstimationError> {
1664 let eta = finite_inverse_link_eta("SAS inverse link", eta)?;
1665 let delta_id = sas_delta_from_raw_log_delta(log_delta);
1666 if epsilon.abs() < 1e-12 && (delta_id - 1.0).abs() < 1e-12 {
1667 return Ok(component_inverse_link_mu_d1(LinkComponent::Probit, eta));
1668 }
1669 let asinh = asinh_jet5(eta);
1670 let delta = delta_id;
1671 let u_raw = delta * asinh.value + epsilon;
1672 let sb = smooth_bound_jet(u_raw, SAS_U_CLAMP);
1673 let u = sb.g;
1674 let g1 = sb.d1;
1675 let s = u.sinh();
1676 let c = u.cosh();
1677 let z = s;
1678 let r1 = delta * asinh.d1;
1679 let u1 = g1 * r1;
1680 let z1 = c * u1;
1681 let base = probit_jet(z);
1684 Ok((base.mu, canonicalzero(base.d1 * z1)))
1685}
1686
1687fn beta_logistic_inverse_link_mu_d1(eta: f64, delta: f64, epsilon: f64) -> (f64, f64) {
1688 let logistic = logistic_uwith_derivatives(eta);
1689 let (a, b) = beta_logistic_shapes(delta, epsilon);
1690 let mu = beta_reg_logistic(a, b, logistic);
1691 let log_d1 = beta_logistic_log_d1(a, b, logistic);
1692 (mu, log_d1.exp())
1693}
1694
1695fn mixture_inverse_link_mu_d1(state: &MixtureLinkState, eta: f64) -> (f64, f64) {
1696 let mut mu = 0.0_f64;
1697 let mut d1 = 0.0_f64;
1698 let k = state.components.len().min(state.pi.len());
1699 for i in 0..k {
1700 let (mu_i, d1_i) = component_inverse_link_mu_d1(state.components[i], eta);
1701 let w = state.pi[i];
1702 mu += w * mu_i;
1703 d1 += w * d1_i;
1704 }
1705 (mu, d1)
1706}
1707
1708#[derive(Clone, Copy)]
1709enum PdfDerivativeOrder {
1710 Third,
1711 Fourth,
1712}
1713
1714impl PdfDerivativeOrder {
1715 fn probit(self, eta: f64) -> f64 {
1716 match self {
1717 Self::Third => probit_pdfthird_derivative(eta),
1718 Self::Fourth => probit_pdffourth_derivative(eta),
1719 }
1720 }
1721
1722 fn component(self, component: LinkComponent, eta: f64) -> f64 {
1723 match self {
1724 Self::Third => component_inverse_link_pdfthird_derivative(component, eta),
1725 Self::Fourth => component_inverse_link_pdffourth_derivative(component, eta),
1726 }
1727 }
1728
1729 fn latent_cloglog(self, eta: f64, latent_sd: f64) -> Result<f64, EstimationError> {
1730 let jet = latent_cloglog_jet5(latent_cloglog_quadctx(), eta, latent_sd)?;
1731 Ok(match self {
1732 Self::Third => jet.d4,
1733 Self::Fourth => jet.d5,
1734 })
1735 }
1736
1737 fn sas(self, eta: f64, epsilon: f64, log_delta: f64) -> Result<f64, EstimationError> {
1738 match self {
1739 Self::Third => sas_inverse_link_pdfthird_derivative(eta, epsilon, log_delta),
1740 Self::Fourth => sas_inverse_link_pdffourth_derivative(eta, epsilon, log_delta),
1741 }
1742 }
1743
1744 fn beta_logistic(self, eta: f64, log_shape_center: f64, epsilon: f64) -> f64 {
1745 match self {
1746 Self::Third => {
1747 beta_logistic_inverse_link_pdfthird_derivative(eta, log_shape_center, epsilon)
1748 }
1749 Self::Fourth => {
1750 beta_logistic_inverse_link_pdffourth_derivative(eta, log_shape_center, epsilon)
1751 }
1752 }
1753 }
1754}
1755
1756fn inverse_link_pdf_derivative_for_inverse_link(
1757 link: &InverseLink,
1758 eta: f64,
1759 order: PdfDerivativeOrder,
1760) -> Result<f64, EstimationError> {
1761 match link {
1762 InverseLink::Standard(StandardLink::Identity) => Ok(0.0),
1763 InverseLink::Standard(StandardLink::Log) => log_link_solver_exp(eta),
1764 InverseLink::Standard(StandardLink::Probit) => Ok(order.probit(eta)),
1765 InverseLink::Standard(StandardLink::Logit) => {
1766 Ok(order.component(LinkComponent::Logit, eta))
1767 }
1768 InverseLink::Standard(StandardLink::CLogLog) => {
1769 Ok(order.component(LinkComponent::CLogLog, eta))
1770 }
1771 InverseLink::Standard(StandardLink::LogLog) => {
1772 Ok(order.component(LinkComponent::LogLog, eta))
1773 }
1774 InverseLink::Standard(StandardLink::Cauchit) => {
1775 Ok(order.component(LinkComponent::Cauchit, eta))
1776 }
1777 InverseLink::LatentCLogLog(state) => order.latent_cloglog(eta, state.latent_sd),
1778 InverseLink::Sas(state) => order.sas(eta, state.epsilon, state.log_delta),
1779 InverseLink::BetaLogistic(state) => {
1780 Ok(order.beta_logistic(eta, state.log_delta, state.epsilon))
1781 }
1782 InverseLink::Mixture(state) => Ok(state
1783 .components
1784 .iter()
1785 .zip(state.pi.iter())
1786 .map(|(&component, &weight)| weight * order.component(component, eta))
1787 .sum()),
1788 }
1789}
1790
1791pub fn inverse_link_pdfthird_derivative_for_inverse_link(
1792 link: &InverseLink,
1793 eta: f64,
1794) -> Result<f64, EstimationError> {
1795 inverse_link_pdf_derivative_for_inverse_link(link, eta, PdfDerivativeOrder::Third)
1813}
1814
1815pub fn inverse_link_pdffourth_derivative_for_inverse_link(
1822 link: &InverseLink,
1823 eta: f64,
1824) -> Result<f64, EstimationError> {
1825 inverse_link_pdf_derivative_for_inverse_link(link, eta, PdfDerivativeOrder::Fourth)
1826}
1827
1828#[inline]
1829fn royston_parmar_inverse_link_jet(eta: f64) -> Result<InverseLinkJet, EstimationError> {
1833 let eta = finite_inverse_link_eta("Royston-Parmar survival inverse link", eta)?;
1834 let hazard = eta.exp();
1835 let survival = (-hazard).exp();
1836 let d1 = -stable_nonnegative_poly_times_exp_neg(hazard, &[0.0, 1.0]);
1841 let d2 = stable_nonnegative_poly_times_exp_neg(hazard, &[0.0, -1.0, 1.0]);
1842 let d3 = stable_nonnegative_poly_times_exp_neg(hazard, &[0.0, -1.0, 3.0, -1.0]);
1843 Ok(InverseLinkJet {
1844 mu: survival,
1845 d1: canonicalzero(d1),
1846 d2: canonicalzero(d2),
1847 d3: canonicalzero(d3),
1848 })
1849}
1850
1851pub fn inverse_link_jet_for_family(
1852 spec: &LikelihoodSpec,
1853 eta: f64,
1854) -> Result<InverseLinkJet, EstimationError> {
1855 if matches!(spec.response, ResponseFamily::RoystonParmar) {
1858 return royston_parmar_inverse_link_jet(eta);
1859 }
1860 spec.link.jet(eta)
1861}
1862
1863#[inline]
1871fn log_inverse_link_jet_exact(eta: f64) -> InverseLinkJet {
1872 let e = eta.exp();
1873 InverseLinkJet {
1874 mu: e,
1875 d1: e,
1876 d2: e,
1877 d3: e,
1878 }
1879}
1880
1881pub fn inverse_link_jet_for_family_public(
1893 spec: &LikelihoodSpec,
1894 eta: f64,
1895) -> Result<InverseLinkJet, EstimationError> {
1896 if matches!(spec.response, ResponseFamily::RoystonParmar) {
1897 return royston_parmar_inverse_link_jet(eta);
1898 }
1899 if let InverseLink::Standard(StandardLink::Log) = spec.link {
1900 return Ok(log_inverse_link_jet_exact(eta));
1901 }
1902 spec.link.jet(eta)
1903}
1904
1905#[inline]
1906pub fn mixture_inverse_link_jet(state: &MixtureLinkState, eta: f64) -> InverseLinkJet {
1907 let mut mu = 0.0_f64;
1908 let mut d1 = 0.0_f64;
1909 let mut d2 = 0.0_f64;
1910 let mut d3 = 0.0_f64;
1911 let k = state.components.len().min(state.pi.len());
1912 for i in 0..k {
1913 let jet = component_inverse_link_jet(state.components[i], eta);
1914 let w = state.pi[i];
1915 mu += w * jet.mu;
1916 d1 += w * jet.d1;
1917 d2 += w * jet.d2;
1918 d3 += w * jet.d3;
1919 }
1920 InverseLinkJet { mu, d1, d2, d3 }
1921}
1922
1923pub fn mixture_inverse_link_jetwith_rho_partials(
1931 state: &MixtureLinkState,
1932 eta: f64,
1933) -> MixtureJetWithRhoPartials {
1934 let k = state.components.len().min(state.pi.len());
1935 let m = k.saturating_sub(1);
1936 let mut djet_drho = vec![
1937 InverseLinkJet {
1938 mu: 0.0,
1939 d1: 0.0,
1940 d2: 0.0,
1941 d3: 0.0,
1942 };
1943 m
1944 ];
1945 let jet = mixture_inverse_link_jetwith_rho_partials_into(state, eta, &mut djet_drho);
1946 let mut d2mu_drho2 = Array2::<f64>::zeros((m, m));
1956 let mut d2d1_drho2 = Array2::<f64>::zeros((m, m));
1957 let mut d2d2_drho2 = Array2::<f64>::zeros((m, m));
1958 for j in 0..m {
1959 for k in j..m {
1960 let diagonal = if j == k { 1.0 } else { 0.0 };
1961 let mu = (diagonal - state.pi[k]) * djet_drho[j].mu
1962 - state.pi[j] * djet_drho[k].mu;
1963 let d1 = (diagonal - state.pi[k]) * djet_drho[j].d1
1964 - state.pi[j] * djet_drho[k].d1;
1965 let d2 = (diagonal - state.pi[k]) * djet_drho[j].d2
1966 - state.pi[j] * djet_drho[k].d2;
1967 d2mu_drho2[[j, k]] = mu;
1968 d2mu_drho2[[k, j]] = mu;
1969 d2d1_drho2[[j, k]] = d1;
1970 d2d1_drho2[[k, j]] = d1;
1971 d2d2_drho2[[j, k]] = d2;
1972 d2d2_drho2[[k, j]] = d2;
1973 }
1974 }
1975 MixtureJetWithRhoPartials {
1976 jet,
1977 djet_drho,
1978 d2mu_drho2,
1979 d2d1_drho2,
1980 d2d2_drho2,
1981 }
1982}
1983
1984pub fn mixture_inverse_link_jetwith_rho_partials_into(
1987 state: &MixtureLinkState,
1988 eta: f64,
1989 out: &mut [InverseLinkJet],
1990) -> InverseLinkJet {
1991 let k = state.components.len().min(state.pi.len());
1992 let m = k.saturating_sub(1);
1993 assert!(
1994 out.len() >= m,
1995 "rho-partial output buffer too small: got {}, need {}",
1996 out.len(),
1997 m
1998 );
1999 let mut mixed = InverseLinkJet {
2000 mu: 0.0,
2001 d1: 0.0,
2002 d2: 0.0,
2003 d3: 0.0,
2004 };
2005 for i in 0..k {
2006 let jet_i = component_inverse_link_jet(state.components[i], eta);
2007 let w = state.pi[i];
2008 mixed.mu += w * jet_i.mu;
2009 mixed.d1 += w * jet_i.d1;
2010 mixed.d2 += w * jet_i.d2;
2011 mixed.d3 += w * jet_i.d3;
2012 if i < m {
2015 out[i] = jet_i;
2016 }
2017 }
2018 for j in 0..m {
2019 let pi_j = state.pi[j];
2020 let cj = out[j];
2021 out[j] = InverseLinkJet {
2022 mu: pi_j * (cj.mu - mixed.mu),
2023 d1: pi_j * (cj.d1 - mixed.d1),
2024 d2: pi_j * (cj.d2 - mixed.d2),
2025 d3: pi_j * (cj.d3 - mixed.d3),
2026 };
2027 }
2028 mixed
2029}
2030
2031#[derive(Clone, Copy)]
2032struct LogisticU {
2033 u: f64,
2034 one_minus_u: f64,
2035 ln_u: f64,
2036 ln_one_minus_u: f64,
2037 du: f64,
2038 use_upper_tail: bool,
2039}
2040
2041#[inline]
2042fn logistic_uwith_derivatives(eta: f64) -> LogisticU {
2043 let ln_u = -gam_linalg::utils::stable_softplus(-eta);
2044 let ln_one_minus_u = -gam_linalg::utils::stable_softplus(eta);
2045 let u = ln_u.exp();
2046 let one_minus_u = ln_one_minus_u.exp();
2047 let du = (ln_u + ln_one_minus_u).exp();
2048 LogisticU {
2049 u,
2050 one_minus_u,
2051 ln_u,
2052 ln_one_minus_u,
2053 du,
2054 use_upper_tail: eta >= 0.0,
2055 }
2056}
2057
2058#[inline]
2059fn beta_reg_logistic(a: f64, b: f64, logistic: LogisticU) -> f64 {
2060 if logistic.ln_u.is_nan() || logistic.ln_one_minus_u.is_nan() {
2061 return f64::NAN;
2062 }
2063 if logistic.ln_u == f64::NEG_INFINITY {
2064 return 0.0;
2065 }
2066 if logistic.ln_one_minus_u == f64::NEG_INFINITY {
2067 return 1.0;
2068 }
2069 if logistic.use_upper_tail {
2070 1.0 - beta_reg(b, a, logistic.one_minus_u)
2071 } else {
2072 beta_reg(a, b, logistic.u)
2073 }
2074}
2075
2076#[derive(Clone, Copy)]
2077struct BetaShapePartials {
2078 value: f64,
2079 da: f64,
2080 db: f64,
2081 daa: f64,
2082 dab: f64,
2083 dbb: f64,
2084}
2085
2086impl BetaShapePartials {
2087 #[inline]
2088 fn constant(value: f64) -> Self {
2089 Self {
2090 value,
2091 da: 0.0,
2092 db: 0.0,
2093 daa: 0.0,
2094 dab: 0.0,
2095 dbb: 0.0,
2096 }
2097 }
2098}
2099
2100#[inline]
2101fn beta_reg_with_shape_partials_logistic(
2102 a: f64,
2103 b: f64,
2104 logistic: LogisticU,
2105) -> BetaShapePartials {
2106 if logistic.ln_u.is_nan() || logistic.ln_one_minus_u.is_nan() {
2107 return BetaShapePartials {
2108 value: f64::NAN,
2109 da: f64::NAN,
2110 db: f64::NAN,
2111 daa: f64::NAN,
2112 dab: f64::NAN,
2113 dbb: f64::NAN,
2114 };
2115 }
2116 if logistic.use_upper_tail {
2117 let tail = beta_reg_with_shape_partials(b, a, logistic.one_minus_u);
2118 BetaShapePartials {
2119 value: 1.0 - tail.value,
2120 da: -tail.db,
2121 db: -tail.da,
2122 daa: -tail.dbb,
2123 dab: -tail.dab,
2124 dbb: -tail.daa,
2125 }
2126 } else {
2127 beta_reg_with_shape_partials(a, b, logistic.u)
2128 }
2129}
2130
2131#[inline]
2132fn beta_logistic_log_d1(a: f64, b: f64, logistic: LogisticU) -> f64 {
2133 a * logistic.ln_u + b * logistic.ln_one_minus_u - ln_beta(a, b)
2134}
2135
2136#[derive(Clone, Copy)]
2137struct ShapeDual {
2138 v: f64,
2139 da: f64,
2140 db: f64,
2141 daa: f64,
2142 dab: f64,
2143 dbb: f64,
2144}
2145
2146impl ShapeDual {
2147 #[inline]
2148 fn constant(v: f64) -> Self {
2149 Self {
2150 v,
2151 da: 0.0,
2152 db: 0.0,
2153 daa: 0.0,
2154 dab: 0.0,
2155 dbb: 0.0,
2156 }
2157 }
2158
2159 #[inline]
2160 fn from_value_partials(v: f64, da: f64, db: f64) -> Self {
2161 Self {
2162 v,
2163 da,
2164 db,
2165 daa: 0.0,
2166 dab: 0.0,
2167 dbb: 0.0,
2168 }
2169 }
2170
2171 #[inline]
2172 fn clamp_small(self, floor: f64) -> Self {
2173 if self.v.abs() < floor {
2174 Self::constant(floor)
2175 } else {
2176 self
2177 }
2178 }
2179}
2180
2181impl std::ops::Add for ShapeDual {
2182 type Output = Self;
2183
2184 #[inline]
2185 fn add(self, rhs: Self) -> Self {
2186 Self {
2187 v: self.v + rhs.v,
2188 da: self.da + rhs.da,
2189 db: self.db + rhs.db,
2190 daa: self.daa + rhs.daa,
2191 dab: self.dab + rhs.dab,
2192 dbb: self.dbb + rhs.dbb,
2193 }
2194 }
2195}
2196
2197impl std::ops::Sub for ShapeDual {
2198 type Output = Self;
2199
2200 #[inline]
2201 fn sub(self, rhs: Self) -> Self {
2202 Self {
2203 v: self.v - rhs.v,
2204 da: self.da - rhs.da,
2205 db: self.db - rhs.db,
2206 daa: self.daa - rhs.daa,
2207 dab: self.dab - rhs.dab,
2208 dbb: self.dbb - rhs.dbb,
2209 }
2210 }
2211}
2212
2213impl std::ops::Mul for ShapeDual {
2214 type Output = Self;
2215
2216 #[inline]
2217 fn mul(self, rhs: Self) -> Self {
2218 Self {
2219 v: self.v * rhs.v,
2220 da: self.da * rhs.v + self.v * rhs.da,
2221 db: self.db * rhs.v + self.v * rhs.db,
2222 daa: self.daa * rhs.v + 2.0 * self.da * rhs.da + self.v * rhs.daa,
2223 dab: self.dab * rhs.v
2224 + self.da * rhs.db
2225 + self.db * rhs.da
2226 + self.v * rhs.dab,
2227 dbb: self.dbb * rhs.v + 2.0 * self.db * rhs.db + self.v * rhs.dbb,
2228 }
2229 }
2230}
2231
2232impl std::ops::Div for ShapeDual {
2233 type Output = Self;
2234
2235 #[inline]
2236 fn div(self, rhs: Self) -> Self {
2237 let inv = 1.0 / rhs.v;
2238 let inv2 = inv * inv;
2239 let inv3 = inv2 * inv;
2240 let reciprocal = Self {
2241 v: inv,
2242 da: -rhs.da * inv2,
2243 db: -rhs.db * inv2,
2244 daa: 2.0 * rhs.da * rhs.da * inv3 - rhs.daa * inv2,
2245 dab: 2.0 * rhs.da * rhs.db * inv3 - rhs.dab * inv2,
2246 dbb: 2.0 * rhs.db * rhs.db * inv3 - rhs.dbb * inv2,
2247 };
2248 self * reciprocal
2249 }
2250}
2251
2252impl std::ops::Neg for ShapeDual {
2253 type Output = Self;
2254
2255 #[inline]
2256 fn neg(self) -> Self {
2257 ShapeDual {
2258 v: -self.v,
2259 da: -self.da,
2260 db: -self.db,
2261 daa: -self.daa,
2262 dab: -self.dab,
2263 dbb: -self.dbb,
2264 }
2265 }
2266}
2267
2268#[inline]
2269fn shape_dual(v: f64) -> ShapeDual {
2270 ShapeDual::constant(v)
2271}
2272
2273fn beta_reg_with_shape_partials(a0: f64, b0: f64, x0: f64) -> BetaShapePartials {
2277 if x0 <= 0.0 {
2278 return BetaShapePartials::constant(0.0);
2279 }
2280 if x0 >= 1.0 {
2281 return BetaShapePartials::constant(1.0);
2282 }
2283
2284 let symm_transform = x0 >= (a0 + 1.0) / (a0 + b0 + 2.0);
2285 let (a, b, x) = if symm_transform {
2286 (
2287 ShapeDual::from_value_partials(b0, 0.0, 1.0),
2288 ShapeDual::from_value_partials(a0, 1.0, 0.0),
2289 1.0 - x0,
2290 )
2291 } else {
2292 (
2293 ShapeDual::from_value_partials(a0, 1.0, 0.0),
2294 ShapeDual::from_value_partials(b0, 0.0, 1.0),
2295 x0,
2296 )
2297 };
2298
2299 let ln_x = x.ln();
2300 let ln_1mx = (1.0 - x).ln();
2301 let psi_ab = digamma(a.v + b.v);
2302 let log_bt = statrs::function::gamma::ln_gamma(a.v + b.v)
2303 - statrs::function::gamma::ln_gamma(a.v)
2304 - statrs::function::gamma::ln_gamma(b.v)
2305 + a.v * ln_x
2306 + b.v * ln_1mx;
2307 let bt_v = log_bt.exp();
2308 let log_bt_a = psi_ab - digamma(a.v) + ln_x;
2309 let log_bt_b = psi_ab - digamma(b.v) + ln_1mx;
2310 let trigamma_ab = trigamma(a.v + b.v);
2311 let log_bt_aa = trigamma_ab - trigamma(a.v);
2312 let log_bt_ab = trigamma_ab;
2313 let log_bt_bb = trigamma_ab - trigamma(b.v);
2314 let log_bt_da = log_bt_a * a.da + log_bt_b * b.da;
2315 let log_bt_db = log_bt_a * a.db + log_bt_b * b.db;
2316 let log_bt_daa = log_bt_aa * a.da * a.da
2317 + 2.0 * log_bt_ab * a.da * b.da
2318 + log_bt_bb * b.da * b.da
2319 + log_bt_a * a.daa
2320 + log_bt_b * b.daa;
2321 let log_bt_dab = log_bt_aa * a.da * a.db
2322 + log_bt_ab * (a.da * b.db + b.da * a.db)
2323 + log_bt_bb * b.da * b.db
2324 + log_bt_a * a.dab
2325 + log_bt_b * b.dab;
2326 let log_bt_dbb = log_bt_aa * a.db * a.db
2327 + 2.0 * log_bt_ab * a.db * b.db
2328 + log_bt_bb * b.db * b.db
2329 + log_bt_a * a.dbb
2330 + log_bt_b * b.dbb;
2331 let bt = ShapeDual {
2332 v: bt_v,
2333 da: bt_v * log_bt_da,
2334 db: bt_v * log_bt_db,
2335 daa: bt_v * (log_bt_da * log_bt_da + log_bt_daa),
2336 dab: bt_v * (log_bt_da * log_bt_db + log_bt_dab),
2337 dbb: bt_v * (log_bt_db * log_bt_db + log_bt_dbb),
2338 };
2339
2340 let eps = 0.00000000000000011102230246251565;
2341 let fpmin = f64::MIN_POSITIVE / eps;
2342 let one = shape_dual(1.0);
2343 let qab = a + b;
2344 let qap = a + one;
2345 let qam = a - one;
2346 let mut c = one;
2347 let mut d = (one - qab * shape_dual(x) / qap).clamp_small(fpmin);
2348 d = one / d;
2349 let mut h = d;
2350
2351 for m in 1..141 {
2352 let mf = f64::from(m);
2353 let m2 = mf * 2.0;
2354 let md = shape_dual(mf);
2355 let m2d = shape_dual(m2);
2356 let mut aa = md * (b - md) * shape_dual(x) / ((qam + m2d) * (a + m2d));
2357 d = (one + aa * d).clamp_small(fpmin);
2358 c = (one + aa / c).clamp_small(fpmin);
2359 d = one / d;
2360 h = h * d * c;
2361
2362 aa = (a + md).neg() * (qab + md) * shape_dual(x) / ((a + m2d) * (qap + m2d));
2363 d = (one + aa * d).clamp_small(fpmin);
2364 c = (one + aa / c).clamp_small(fpmin);
2365 d = one / d;
2366 let del = d * c;
2367 h = h * del;
2368
2369 if (del.v - 1.0).abs() <= eps {
2370 let reg = bt * h / a;
2371 return if symm_transform {
2372 BetaShapePartials {
2373 value: 1.0 - reg.v,
2374 da: -reg.da,
2375 db: -reg.db,
2376 daa: -reg.daa,
2377 dab: -reg.dab,
2378 dbb: -reg.dbb,
2379 }
2380 } else {
2381 BetaShapePartials {
2382 value: reg.v,
2383 da: reg.da,
2384 db: reg.db,
2385 daa: reg.daa,
2386 dab: reg.dab,
2387 dbb: reg.dbb,
2388 }
2389 };
2390 }
2391 }
2392 let reg = bt * h / a;
2393 if symm_transform {
2394 BetaShapePartials {
2395 value: 1.0 - reg.v,
2396 da: -reg.da,
2397 db: -reg.db,
2398 daa: -reg.daa,
2399 dab: -reg.dab,
2400 dbb: -reg.dbb,
2401 }
2402 } else {
2403 BetaShapePartials {
2404 value: reg.v,
2405 da: reg.da,
2406 db: reg.db,
2407 daa: reg.daa,
2408 dab: reg.dab,
2409 dbb: reg.dbb,
2410 }
2411 }
2412}
2413
2414#[inline]
2437fn beta_logistic_shape_jets(
2438 log_shape_center: f64,
2439 epsilon: f64,
2440) -> (f64, f64, f64, f64, f64, f64) {
2441 let gs = smooth_bound_jet(log_shape_center - epsilon, BETA_LOGISTIC_LOG_SHAPE_BOUND);
2442 let gt = smooth_bound_jet(log_shape_center + epsilon, BETA_LOGISTIC_LOG_SHAPE_BOUND);
2443 let a = gs.g.exp();
2444 let b = gt.g.exp();
2445 (
2446 a,
2447 b,
2448 a * gs.d1,
2449 b * gt.d1,
2450 a * (gs.d1 * gs.d1 + gs.d2),
2451 b * (gt.d1 * gt.d1 + gt.d2),
2452 )
2453}
2454
2455#[inline]
2457fn beta_logistic_shapes(log_shape_center: f64, epsilon: f64) -> (f64, f64) {
2458 let (a, b, ..) = beta_logistic_shape_jets(log_shape_center, epsilon);
2459 (a, b)
2460}
2461
2462pub fn beta_logistic_inverse_link_jet(
2463 eta: f64,
2464 log_shape_center: f64,
2465 epsilon: f64,
2466) -> InverseLinkJet {
2467 let logistic = logistic_uwith_derivatives(eta);
2468 let (a, b) = beta_logistic_shapes(log_shape_center, epsilon);
2469 let mu = beta_reg_logistic(a, b, logistic);
2470 let log_d1 = beta_logistic_log_d1(a, b, logistic);
2471 let d1 = log_d1.exp();
2472 let t = a * logistic.one_minus_u - b * logistic.u;
2473 let d2 = d1 * t;
2474 let d3 = d1 * (t * t - (a + b) * logistic.du);
2475 InverseLinkJet { mu, d1, d2, d3 }
2476}
2477
2478pub fn beta_logistic_inverse_link_pdfthird_derivative(
2479 eta: f64,
2480 log_shape_center: f64,
2481 epsilon: f64,
2482) -> f64 {
2483 let logistic = logistic_uwith_derivatives(eta);
2506 let (a, b) = beta_logistic_shapes(log_shape_center, epsilon);
2507 let log_d1 = beta_logistic_log_d1(a, b, logistic);
2508 let d1 = log_d1.exp();
2509 let c = a + b;
2510 let t = a * logistic.one_minus_u - b * logistic.u;
2511 let u2 = logistic.du * (logistic.one_minus_u - logistic.u);
2512 d1 * (t * t * t - 3.0 * c * t * logistic.du - c * u2)
2513}
2514
2515pub fn beta_logistic_inverse_link_pdffourth_derivative(
2523 eta: f64,
2524 log_shape_center: f64,
2525 epsilon: f64,
2526) -> f64 {
2527 let logistic = logistic_uwith_derivatives(eta);
2528 let (a, b) = beta_logistic_shapes(log_shape_center, epsilon);
2529 let log_d1 = beta_logistic_log_d1(a, b, logistic);
2530 let d1 = log_d1.exp();
2531 let c = a + b;
2532 let t = a * logistic.one_minus_u - b * logistic.u;
2533 let u2 = logistic.du * (logistic.one_minus_u - logistic.u);
2534 let u3 = u2 * (logistic.one_minus_u - logistic.u) - 2.0 * logistic.du * logistic.du;
2535 let t2 = t * t;
2536 d1 * (t2 * t2 - 6.0 * c * t2 * logistic.du - 4.0 * c * t * u2
2537 + 3.0 * c * c * logistic.du * logistic.du
2538 - c * u3)
2539}
2540
2541pub fn beta_logistic_inverse_link_jetwith_param_partials(
2542 eta: f64,
2543 log_shape_center: f64,
2544 epsilon: f64,
2545) -> SasJetWithParamPartials {
2546 let logistic = logistic_uwith_derivatives(eta);
2547 let (a, b, da, db, daa, dbb) = beta_logistic_shape_jets(log_shape_center, epsilon);
2551 let shape = beta_reg_with_shape_partials_logistic(a, b, logistic);
2552 let mu = shape.value;
2553 let dmu_dlog_shape_center = da * shape.da + db * shape.db;
2554 let dmu_depsilon = -da * shape.da + db * shape.db;
2555 let log_d1 = beta_logistic_log_d1(a, b, logistic);
2556 let d1 = log_d1.exp();
2557 let t = a * logistic.one_minus_u - b * logistic.u;
2558 let d2 = d1 * t;
2559 let k = t * t - (a + b) * logistic.du;
2560 let d3 = d1 * k;
2561 let jet = InverseLinkJet { mu, d1, d2, d3 };
2562
2563 let psi_a = digamma(a);
2564 let psi_b = digamma(b);
2565 let psi_ab = digamma(a + b);
2566 let la = logistic.ln_u - psi_a + psi_ab;
2567 let lb = logistic.ln_one_minus_u - psi_b + psi_ab;
2568
2569 let partials_for = |a_p: f64, b_p: f64, dmu: f64| -> InverseLinkJet {
2570 let logd1_p = a_p * la + b_p * lb;
2571 let d1_p = d1 * logd1_p;
2572 let t_p = a_p * logistic.one_minus_u - b_p * logistic.u;
2573 let d2_p = d1_p * t + d1 * t_p;
2574 let k_p = 2.0 * t * t_p - (a_p + b_p) * logistic.du;
2575 let d3_p = d1_p * k + d1 * k_p;
2576 InverseLinkJet {
2577 mu: dmu,
2578 d1: d1_p,
2579 d2: d2_p,
2580 d3: d3_p,
2581 }
2582 };
2583 let djet_dlog_shape_center = partials_for(da, db, dmu_dlog_shape_center);
2584 let djet_depsilon = partials_for(-da, db, dmu_depsilon);
2585 let a_first = [-da, da];
2591 let b_first = [db, db];
2592 let a_second = [[daa, -daa], [-daa, daa]];
2593 let b_second = [[dbb, dbb], [dbb, dbb]];
2594 let logd1_first = [
2595 a_first[0] * la + b_first[0] * lb,
2596 a_first[1] * la + b_first[1] * lb,
2597 ];
2598 let trigamma_ab = trigamma(a + b);
2599 let la_a = trigamma_ab - trigamma(a);
2600 let la_b = trigamma_ab;
2601 let lb_a = trigamma_ab;
2602 let lb_b = trigamma_ab - trigamma(b);
2603 let t_first = [
2606 a_first[0] * logistic.one_minus_u - b_first[0] * logistic.u,
2607 a_first[1] * logistic.one_minus_u - b_first[1] * logistic.u,
2608 ];
2609 let mut d2mu_dparams2 = Array2::<f64>::zeros((2, 2));
2610 let mut d2d1_dparams2 = Array2::<f64>::zeros((2, 2));
2611 let mut d2d2_dparams2 = Array2::<f64>::zeros((2, 2));
2612 for j in 0..2 {
2613 for k in j..2 {
2614 let mu_jk = shape.daa * a_first[j] * a_first[k]
2615 + shape.dab
2616 * (a_first[j] * b_first[k] + b_first[j] * a_first[k])
2617 + shape.dbb * b_first[j] * b_first[k]
2618 + shape.da * a_second[j][k]
2619 + shape.db * b_second[j][k];
2620 let logd1_jk = la_a * a_first[j] * a_first[k]
2621 + la_b * a_first[j] * b_first[k]
2622 + lb_a * b_first[j] * a_first[k]
2623 + lb_b * b_first[j] * b_first[k]
2624 + la * a_second[j][k]
2625 + lb * b_second[j][k];
2626 let d1_jk = d1 * (logd1_first[j] * logd1_first[k] + logd1_jk);
2627 let t_jk = a_second[j][k] * logistic.one_minus_u - b_second[j][k] * logistic.u;
2630 let d2_jk = d1_jk * t
2631 + d1 * logd1_first[j] * t_first[k]
2632 + d1 * logd1_first[k] * t_first[j]
2633 + d1 * t_jk;
2634 d2mu_dparams2[[j, k]] = mu_jk;
2635 d2mu_dparams2[[k, j]] = mu_jk;
2636 d2d1_dparams2[[j, k]] = d1_jk;
2637 d2d1_dparams2[[k, j]] = d1_jk;
2638 d2d2_dparams2[[j, k]] = d2_jk;
2639 d2d2_dparams2[[k, j]] = d2_jk;
2640 }
2641 }
2642 SasJetWithParamPartials {
2643 jet,
2644 djet_depsilon,
2645 djet_dlog_delta: djet_dlog_shape_center,
2646 d2mu_dparams2,
2647 d2d1_dparams2,
2648 d2d2_dparams2,
2649 }
2650}
2651
2652pub fn sas_inverse_link_jet(
2664 eta: f64,
2665 epsilon: f64,
2666 log_delta: f64,
2667) -> Result<InverseLinkJet, EstimationError> {
2668 let eta = finite_inverse_link_eta("SAS inverse link", eta)?;
2669 let delta_id = sas_delta_from_raw_log_delta(log_delta);
2670 if epsilon.abs() < 1e-12 && (delta_id - 1.0).abs() < 1e-12 {
2671 return Ok(component_inverse_link_jet(LinkComponent::Probit, eta));
2672 }
2673 let asinh = asinh_jet5(eta);
2674 let delta = delta_id;
2675 let u_raw = delta * asinh.value + epsilon;
2676 let sb = smooth_bound_jet(u_raw, SAS_U_CLAMP);
2677 let u = sb.g;
2678 let g1 = sb.d1;
2679 let g2 = sb.d2;
2680 let g3 = sb.d3;
2681 let s = u.sinh();
2682 let c = u.cosh();
2683 let z = s;
2684 let r1 = delta * asinh.d1;
2685 let r2 = delta * asinh.d2;
2686 let r3 = delta * asinh.d3;
2687 let u1 = g1 * r1;
2688 let u2 = g2 * r1 * r1 + g1 * r2;
2689 let u3 = g3 * r1 * r1 * r1 + 3.0 * g2 * r1 * r2 + g1 * r3;
2690 let z1 = c * u1;
2691 let z2 = s * u1 * u1 + c * u2;
2692 let z3 = c * u1 * u1 * u1 + 3.0 * s * u1 * u2 + c * u3;
2693 let base = probit_jet(z);
2694 Ok(chain_inverse_link_jet(base, z1, z2, z3))
2695}
2696
2697pub fn sas_inverse_link_pdfthird_derivative(
2700 eta: f64,
2701 epsilon: f64,
2702 log_delta: f64,
2703) -> Result<f64, EstimationError> {
2704 let eta = finite_inverse_link_eta("SAS inverse link", eta)?;
2740 let asinh = asinh_jet5(eta);
2741 let delta = sas_delta_from_raw_log_delta(log_delta);
2742 let u_raw = delta * asinh.value + epsilon;
2743 let sb = smooth_bound_jet(u_raw, SAS_U_CLAMP);
2744 let u = sb.g;
2745 let g1 = sb.d1;
2746 let g2 = sb.d2;
2747 let g3 = sb.d3;
2748 let g4 = sb.d4;
2749 let s = u.sinh();
2750 let c = u.cosh();
2751 let z = s;
2752 let base = probit_jet(z);
2753 let r1 = delta * asinh.d1;
2754 let r2 = delta * asinh.d2;
2755 let r3 = delta * asinh.d3;
2756 let r4 = delta * asinh.d4;
2757 let u1 = g1 * r1;
2758 let u2 = g2 * r1 * r1 + g1 * r2;
2759 let u3 = g3 * r1 * r1 * r1 + 3.0 * g2 * r1 * r2 + g1 * r3;
2760 let u4 = g4 * r1.powi(4)
2761 + 6.0 * g3 * r1 * r1 * r2
2762 + 3.0 * g2 * r2 * r2
2763 + 4.0 * g2 * r1 * r3
2764 + g1 * r4;
2765 let z1 = c * u1;
2766 let z2 = s * u1 * u1 + c * u2;
2767 let z3 = c * u1 * u1 * u1 + 3.0 * s * u1 * u2 + c * u3;
2768 let z4 =
2769 s * u1.powi(4) + 6.0 * c * u1 * u1 * u2 + 3.0 * s * u2 * u2 + 4.0 * s * u1 * u3 + c * u4;
2770 let base4 = probit_pdfthird_derivative(z);
2771 let out = base4 * z1.powi(4)
2772 + 6.0 * base.d3 * z1 * z1 * z2
2773 + 3.0 * base.d2 * z2 * z2
2774 + 4.0 * base.d2 * z1 * z3
2775 + base.d1 * z4;
2776 Ok(canonicalzero(out))
2777}
2778
2779pub fn sas_inverse_link_pdffourth_derivative(
2798 eta: f64,
2799 epsilon: f64,
2800 log_delta: f64,
2801) -> Result<f64, EstimationError> {
2802 let eta = finite_inverse_link_eta("SAS inverse link", eta)?;
2803 let asinh = asinh_jet5(eta);
2804 let delta = sas_delta_from_raw_log_delta(log_delta);
2805 let u_raw = delta * asinh.value + epsilon;
2806 let sb = smooth_bound_jet(u_raw, SAS_U_CLAMP);
2807 let u = sb.g;
2808 let g1 = sb.d1;
2809 let g2 = sb.d2;
2810 let g3 = sb.d3;
2811 let g4 = sb.d4;
2812 let g5 = sb.d5;
2813 let s = u.sinh();
2814 let c = u.cosh();
2815 let z = s;
2816
2817 let base = probit_jet(z);
2819 let phi3 = probit_pdfthird_derivative(z); let phi4 = probit_pdffourth_derivative(z); let r1 = delta * asinh.d1;
2823 let r2 = delta * asinh.d2;
2824 let r3 = delta * asinh.d3;
2825 let r4 = delta * asinh.d4;
2826 let r5 = delta * asinh.d5;
2827
2828 let u1 = g1 * r1;
2830 let u2 = g2 * r1 * r1 + g1 * r2;
2831 let u3 = g3 * r1 * r1 * r1 + 3.0 * g2 * r1 * r2 + g1 * r3;
2832 let u4 = g4 * r1.powi(4)
2833 + 6.0 * g3 * r1 * r1 * r2
2834 + 3.0 * g2 * r2 * r2
2835 + 4.0 * g2 * r1 * r3
2836 + g1 * r4;
2837 let u5 = g5 * r1.powi(5)
2838 + 10.0 * g4 * r1 * r1 * r1 * r2
2839 + 15.0 * g3 * r1 * r2 * r2
2840 + 10.0 * g3 * r1 * r1 * r3
2841 + 10.0 * g2 * r2 * r3
2842 + 5.0 * g2 * r1 * r4
2843 + g1 * r5;
2844
2845 let z1 = c * u1;
2847 let z2 = s * u1 * u1 + c * u2;
2848 let z3 = c * u1 * u1 * u1 + 3.0 * s * u1 * u2 + c * u3;
2849 let z4 =
2850 s * u1.powi(4) + 6.0 * c * u1 * u1 * u2 + 3.0 * s * u2 * u2 + 4.0 * s * u1 * u3 + c * u4;
2851 let z5 = c * u1.powi(5)
2852 + 10.0 * s * u1 * u1 * u1 * u2
2853 + 15.0 * c * u1 * u2 * u2
2854 + 10.0 * c * u1 * u1 * u3
2855 + 10.0 * s * u2 * u3
2856 + 5.0 * s * u1 * u4
2857 + c * u5;
2858
2859 let out = phi4 * z1.powi(5)
2862 + 10.0 * phi3 * z1 * z1 * z1 * z2
2863 + 15.0 * base.d3 * z1 * z2 * z2
2864 + 10.0 * base.d3 * z1 * z1 * z3
2865 + 10.0 * base.d2 * z2 * z3
2866 + 5.0 * base.d2 * z1 * z4
2867 + base.d1 * z5;
2868 Ok(canonicalzero(out))
2869}
2870
2871pub fn sas_inverse_link_jetwith_param_partials(
2875 eta: f64,
2876 epsilon: f64,
2877 log_delta: f64,
2878) -> Result<SasJetWithParamPartials, EstimationError> {
2879 let eta = finite_inverse_link_eta("SAS inverse link", eta)?;
2880 let asinh = asinh_jet5(eta);
2881 let ld_sb = smooth_bound_jet(log_delta, SAS_LOG_DELTA_BOUND);
2882 let (ld_eff, dld_eff_draw) = (ld_sb.g, ld_sb.d1);
2883 let d2ld_eff_draw2 = ld_sb.d2;
2884 let delta = ld_eff.exp();
2885 let ddelta_draw = delta * dld_eff_draw;
2886 let d2delta_draw2 = delta * (dld_eff_draw * dld_eff_draw + d2ld_eff_draw2);
2887 let u_raw = delta * asinh.value + epsilon;
2888 let sb = smooth_bound_jet(u_raw, SAS_U_CLAMP);
2889 let u = sb.g;
2890 let g1 = sb.d1;
2891 let g2 = sb.d2;
2892 let g3 = sb.d3;
2893 let g4 = sb.d4;
2894 let s = u.sinh();
2895 let c = u.cosh();
2896 let z = s;
2897 let a1 = asinh.d1;
2898 let a2 = asinh.d2;
2899 let a3 = asinh.d3;
2900 let r1 = delta * a1;
2901 let r2 = delta * a2;
2902 let r3 = delta * a3;
2903 let u1 = g1 * r1;
2904 let u2 = g2 * r1 * r1 + g1 * r2;
2905 let u3 = g3 * r1 * r1 * r1 + 3.0 * g2 * r1 * r2 + g1 * r3;
2906 let z1 = c * u1;
2907 let z2 = s * u1 * u1 + c * u2;
2908 let z3 = c * u1 * u1 * u1 + 3.0 * s * u1 * u2 + c * u3;
2909
2910 let base = probit_jet(z);
2911 let jet = chain_inverse_link_jet(base, z1, z2, z3);
2912
2913 let param_partials = |u_t: f64, u1_t: f64, u2_t: f64, u3_t: f64| -> InverseLinkJet {
2916 let z_t = c * u_t;
2917 let z1_t = s * u_t * u1 + c * u1_t;
2918 let z2_t = c * u_t * u1 * u1 + 2.0 * s * u1 * u1_t + s * u_t * u2 + c * u2_t;
2919 let z3_t = s * u_t * u1 * u1 * u1
2920 + 3.0 * c * u1 * u1 * u1_t
2921 + 3.0 * c * u_t * u1 * u2
2922 + 3.0 * s * (u1_t * u2 + u1 * u2_t)
2923 + s * u_t * u3
2924 + c * u3_t;
2925
2926 InverseLinkJet {
2927 mu: base.d1 * z_t,
2928 d1: base.d2 * z_t * z1 + base.d1 * z1_t,
2929 d2: base.d3 * z_t * z1 * z1
2930 + 2.0 * base.d2 * z1 * z1_t
2931 + base.d2 * z_t * z2
2932 + base.d1 * z2_t,
2933 d3: probit_pdfthird_derivative(z) * z_t * z1.powi(3)
2934 + 3.0 * base.d3 * z1 * z1 * z1_t
2935 + 3.0 * base.d3 * z_t * z1 * z2
2936 + 3.0 * base.d2 * (z1_t * z2 + z1 * z2_t)
2937 + base.d2 * z_t * z3
2938 + base.d1 * z3_t,
2939 }
2940 };
2941
2942 let rt_eps = 1.0;
2944 let r1t_eps = 0.0;
2945 let r2t_eps = 0.0;
2946 let r3t_eps = 0.0;
2947 let u_eps = g1 * rt_eps;
2948 let u1_eps = g2 * rt_eps * r1 + g1 * r1t_eps;
2949 let u2_eps = g3 * rt_eps * r1 * r1 + 2.0 * g2 * r1 * r1t_eps + g2 * rt_eps * r2 + g1 * r2t_eps;
2950 let u3_eps = g4 * rt_eps * r1 * r1 * r1
2951 + 3.0 * g3 * r1 * r1 * r1t_eps
2952 + 3.0 * g3 * rt_eps * r1 * r2
2953 + 3.0 * g2 * (r1t_eps * r2 + r1 * r2t_eps)
2954 + g2 * rt_eps * r3
2955 + g1 * r3t_eps;
2956 let djet_depsilon = param_partials(u_eps, u1_eps, u2_eps, u3_eps);
2957
2958 let rt_ld = ddelta_draw * asinh.value;
2960 let r1t_ld = ddelta_draw * a1;
2961 let r2t_ld = ddelta_draw * a2;
2962 let r3t_ld = ddelta_draw * a3;
2963 let u_ld = g1 * rt_ld;
2964 let u1_ld = g2 * rt_ld * r1 + g1 * r1t_ld;
2965 let u2_ld = g3 * rt_ld * r1 * r1 + 2.0 * g2 * r1 * r1t_ld + g2 * rt_ld * r2 + g1 * r2t_ld;
2966 let u3_ld = g4 * rt_ld * r1 * r1 * r1
2967 + 3.0 * g3 * r1 * r1 * r1t_ld
2968 + 3.0 * g3 * rt_ld * r1 * r2
2969 + 3.0 * g2 * (r1t_ld * r2 + r1 * r2t_ld)
2970 + g2 * rt_ld * r3
2971 + g1 * r3t_ld;
2972 let djet_dlog_delta = param_partials(u_ld, u1_ld, u2_ld, u3_ld);
2973
2974 let r_t = [rt_eps, rt_ld];
2980 let r1_t = [r1t_eps, r1t_ld];
2981 let r2_t = [r2t_eps, r2t_ld];
2982 let r_tt = [[0.0, 0.0], [0.0, d2delta_draw2 * asinh.value]];
2983 let r1_tt = [[0.0, 0.0], [0.0, d2delta_draw2 * a1]];
2984 let r2_tt = [[0.0, 0.0], [0.0, d2delta_draw2 * a2]];
2985 let u_t = [u_eps, u_ld];
2986 let u1_t = [u1_eps, u1_ld];
2987 let u2_t = [u2_eps, u2_ld];
2988 let z_t = [c * u_t[0], c * u_t[1]];
2989 let z1_t = [
2990 s * u_t[0] * u1 + c * u1_t[0],
2991 s * u_t[1] * u1 + c * u1_t[1],
2992 ];
2993 let z2_t = [
2994 c * u_t[0] * u1 * u1 + 2.0 * s * u1 * u1_t[0] + s * u_t[0] * u2 + c * u2_t[0],
2995 c * u_t[1] * u1 * u1 + 2.0 * s * u1 * u1_t[1] + s * u_t[1] * u2 + c * u2_t[1],
2996 ];
2997 let phi3 = probit_pdfthird_derivative(z);
2998 let mut d2mu_dparams2 = Array2::<f64>::zeros((2, 2));
2999 let mut d2d1_dparams2 = Array2::<f64>::zeros((2, 2));
3000 let mut d2d2_dparams2 = Array2::<f64>::zeros((2, 2));
3001 for j in 0..2 {
3002 for k in j..2 {
3003 let u_jk = g2 * r_t[j] * r_t[k] + g1 * r_tt[j][k];
3004 let u1_jk = g3 * r_t[j] * r_t[k] * r1
3005 + g2 * r_tt[j][k] * r1
3006 + g2 * r_t[j] * r1_t[k]
3007 + g2 * r_t[k] * r1_t[j]
3008 + g1 * r1_tt[j][k];
3009 let u2_jk = g4 * r_t[j] * r_t[k] * r1 * r1
3011 + g3 * r_tt[j][k] * r1 * r1
3012 + 2.0 * g3 * r_t[j] * r1 * r1_t[k]
3013 + 2.0 * g3 * r_t[k] * r1 * r1_t[j]
3014 + 2.0 * g2 * (r1_t[j] * r1_t[k] + r1 * r1_tt[j][k])
3015 + g3 * r_t[j] * r_t[k] * r2
3016 + g2 * r_tt[j][k] * r2
3017 + g2 * r_t[j] * r2_t[k]
3018 + g2 * r_t[k] * r2_t[j]
3019 + g1 * r2_tt[j][k];
3020 let z_jk = s * u_t[j] * u_t[k] + c * u_jk;
3021 let z1_jk = c * u_t[j] * u_t[k] * u1
3022 + s * u_jk * u1
3023 + s * u_t[j] * u1_t[k]
3024 + s * u_t[k] * u1_t[j]
3025 + c * u1_jk;
3026 let z2_jk = s * u_t[j] * u_t[k] * u1 * u1
3028 + c * u_jk * u1 * u1
3029 + 2.0 * c * u_t[j] * u1 * u1_t[k]
3030 + 2.0 * c * u_t[k] * u1 * u1_t[j]
3031 + 2.0 * s * (u1_t[j] * u1_t[k] + u1 * u1_jk)
3032 + c * u_t[j] * u_t[k] * u2
3033 + s * u_jk * u2
3034 + s * u_t[j] * u2_t[k]
3035 + s * u_t[k] * u2_t[j]
3036 + c * u2_jk;
3037 let mu_jk = base.d2 * z_t[j] * z_t[k] + base.d1 * z_jk;
3038 let d1_jk = base.d3 * z_t[j] * z_t[k] * z1
3039 + base.d2 * z_jk * z1
3040 + base.d2 * z_t[j] * z1_t[k]
3041 + base.d2 * z_t[k] * z1_t[j]
3042 + base.d1 * z1_jk;
3043 let d2_jk = phi3 * z_t[j] * z_t[k] * z1 * z1
3046 + base.d3 * z_jk * z1 * z1
3047 + 2.0 * base.d3 * z_t[j] * z1 * z1_t[k]
3048 + 2.0 * base.d3 * z_t[k] * z1 * z1_t[j]
3049 + 2.0 * base.d2 * (z1_t[j] * z1_t[k] + z1 * z1_jk)
3050 + base.d3 * z_t[j] * z_t[k] * z2
3051 + base.d2 * z_jk * z2
3052 + base.d2 * z_t[j] * z2_t[k]
3053 + base.d2 * z_t[k] * z2_t[j]
3054 + base.d1 * z2_jk;
3055 d2mu_dparams2[[j, k]] = mu_jk;
3056 d2mu_dparams2[[k, j]] = mu_jk;
3057 d2d1_dparams2[[j, k]] = d1_jk;
3058 d2d1_dparams2[[k, j]] = d1_jk;
3059 d2d2_dparams2[[j, k]] = d2_jk;
3060 d2d2_dparams2[[k, j]] = d2_jk;
3061 }
3062 }
3063
3064 Ok(SasJetWithParamPartials {
3065 jet,
3066 djet_depsilon,
3067 djet_dlog_delta,
3068 d2mu_dparams2,
3069 d2d1_dparams2,
3070 d2d2_dparams2,
3071 })
3072}
3073
3074#[cfg(test)]
3075mod tests {
3076 use super::*;
3077 use gam_problem::{InverseLink, LikelihoodSpec, LinkComponent, MixtureLinkSpec, SasLinkState};
3078
3079 fn assert_log_link_domain_error(error: EstimationError, eta: f64) {
3080 match error {
3081 EstimationError::InverseLinkDomainViolation {
3082 link,
3083 eta: rejected,
3084 lower,
3085 upper,
3086 } => {
3087 assert_eq!(link, "standard log inverse link");
3088 if eta.is_nan() {
3089 assert!(rejected.is_nan());
3090 } else {
3091 assert_eq!(rejected, eta);
3092 }
3093 assert_eq!(lower, LOG_LINK_SOLVER_ETA_MIN);
3094 assert_eq!(upper, LOG_LINK_SOLVER_ETA_MAX);
3095 }
3096 other => panic!("expected typed log-link domain refusal, got {other}"),
3097 }
3098 }
3099
3100 fn assert_finite_eta_domain_error(
3101 error: EstimationError,
3102 expected_link: &'static str,
3103 eta: f64,
3104 ) {
3105 match error {
3106 EstimationError::InverseLinkDomainViolation {
3107 link,
3108 eta: rejected,
3109 lower,
3110 upper,
3111 } => {
3112 assert_eq!(link, expected_link);
3113 if eta.is_nan() {
3114 assert!(rejected.is_nan());
3115 } else {
3116 assert_eq!(rejected, eta);
3117 }
3118 assert_eq!(lower, -f64::MAX);
3119 assert_eq!(upper, f64::MAX);
3120 }
3121 other => panic!("expected typed finite-eta domain refusal, got {other}"),
3122 }
3123 }
3124
3125 #[test]
3126 fn log_link_solver_boundaries_are_inclusive_exact_exp_jets() {
3127 let link = InverseLink::Standard(StandardLink::Log);
3128 let spec = LikelihoodSpec::poisson_log();
3129 for eta in [LOG_LINK_SOLVER_ETA_MIN, LOG_LINK_SOLVER_ETA_MAX] {
3130 let expected = eta.exp();
3131 assert!(expected.is_finite() && expected > 0.0);
3132
3133 let jet = inverse_link_jet_for_inverse_link(&link, eta).expect("boundary jet");
3134 assert_eq!(
3135 LinkFunction::Log.jet(eta).expect("kernel boundary jet"),
3136 jet
3137 );
3138 assert_eq!(jet.mu, expected);
3139 assert_eq!(jet.d1, expected);
3140 assert_eq!(jet.d2, expected);
3141 assert_eq!(jet.d3, expected);
3142
3143 assert_eq!(
3144 inverse_link_mu_d1_for_inverse_link(&link, eta).expect("boundary mu/d1"),
3145 (expected, expected)
3146 );
3147 assert_eq!(
3148 inverse_link_pdfthird_derivative_for_inverse_link(&link, eta)
3149 .expect("boundary fourth derivative"),
3150 expected
3151 );
3152 assert_eq!(
3153 inverse_link_pdffourth_derivative_for_inverse_link(&link, eta)
3154 .expect("boundary fifth derivative"),
3155 expected
3156 );
3157 assert_eq!(
3158 inverse_link_jet_for_family(&spec, eta).expect("boundary family jet"),
3159 jet
3160 );
3161 }
3162 }
3163
3164 #[test]
3165 fn log_link_solver_seams_refuse_every_eta_outside_the_declared_domain() {
3166 let link = InverseLink::Standard(StandardLink::Log);
3167 let spec = LikelihoodSpec::poisson_log();
3168 let just_below = f64::from_bits(LOG_LINK_SOLVER_ETA_MIN.to_bits() + 1);
3169 let just_above = f64::from_bits(LOG_LINK_SOLVER_ETA_MAX.to_bits() + 1);
3170
3171 for eta in [
3172 just_below,
3173 just_above,
3174 f64::NEG_INFINITY,
3175 f64::INFINITY,
3176 f64::NAN,
3177 ] {
3178 assert_log_link_domain_error(
3179 inverse_link_jet_for_inverse_link(&link, eta).expect_err("full jet must refuse"),
3180 eta,
3181 );
3182 assert_log_link_domain_error(
3183 LinkFunction::Log
3184 .jet(eta)
3185 .expect_err("kernel jet must refuse"),
3186 eta,
3187 );
3188 assert_log_link_domain_error(
3189 inverse_link_mu_d1_for_inverse_link(&link, eta)
3190 .expect_err("mu/d1 seam must refuse"),
3191 eta,
3192 );
3193 assert_log_link_domain_error(
3194 inverse_link_pdfthird_derivative_for_inverse_link(&link, eta)
3195 .expect_err("fourth derivative seam must refuse"),
3196 eta,
3197 );
3198 assert_log_link_domain_error(
3199 inverse_link_pdffourth_derivative_for_inverse_link(&link, eta)
3200 .expect_err("fifth derivative seam must refuse"),
3201 eta,
3202 );
3203 assert_log_link_domain_error(
3204 inverse_link_jet_for_family(&spec, eta).expect_err("family jet seam must refuse"),
3205 eta,
3206 );
3207 }
3208 }
3209
3210 #[test]
3211 fn log_link_solver_value_gradient_is_consistent_near_both_domain_edges() {
3212 let link = InverseLink::Standard(StandardLink::Log);
3213 let h = 1.0e-5;
3214 for eta in [
3215 LOG_LINK_SOLVER_ETA_MIN + 1.0,
3216 0.0,
3217 LOG_LINK_SOLVER_ETA_MAX - 1.0,
3218 ] {
3219 let jet = inverse_link_jet_for_inverse_link(&link, eta).expect("interior jet");
3220 let eta_plus = eta + h;
3221 let eta_minus = eta - h;
3222 let mu_plus = inverse_link_jet_for_inverse_link(&link, eta_plus)
3223 .expect("plus jet")
3224 .mu;
3225 let mu_minus = inverse_link_jet_for_inverse_link(&link, eta_minus)
3226 .expect("minus jet")
3227 .mu;
3228 let finite_difference = (mu_plus - mu_minus) / (eta_plus - eta_minus);
3229 let relative_error = ((finite_difference - jet.d1) / jet.d1).abs();
3230 assert!(
3231 relative_error < 5.0e-10,
3232 "log-link value/gradient mismatch at eta={eta}: analytic={}, finite_difference={}, relative_error={relative_error}",
3233 jet.d1,
3234 finite_difference
3235 );
3236 }
3237 }
3238
3239 #[test]
3240 fn subnormal_inverse_link_derivatives_are_preserved_not_plateaued() {
3241 let left_eta = -743.0_f64;
3242 let left_scale = left_eta.exp();
3243 assert!(left_scale > 0.0 && left_scale < f64::MIN_POSITIVE);
3244 let left = logit_inverse_link_jet5(left_eta);
3245 for (order, derivative) in [left.d1, left.d2, left.d3, left.d4, left.d5]
3246 .into_iter()
3247 .enumerate()
3248 {
3249 assert!(
3250 derivative > 0.0 && derivative < f64::MIN_POSITIVE,
3251 "left-tail logit derivative order {} lost its represented subnormal: {derivative}",
3252 order + 1
3253 );
3254 }
3255
3256 let right_eta = 743.0_f64;
3257 let right_scale = (-right_eta).exp();
3258 assert!(right_scale > 0.0 && right_scale < f64::MIN_POSITIVE);
3259 let right = logit_inverse_link_jet5(right_eta);
3260 for (order, derivative, sign) in [
3261 (1, right.d1, 1.0),
3262 (2, right.d2, -1.0),
3263 (3, right.d3, 1.0),
3264 (4, right.d4, -1.0),
3265 (5, right.d5, 1.0),
3266 ] {
3267 assert_eq!(derivative.signum(), sign, "wrong order-{order} tail sign");
3268 assert!(
3269 derivative.abs() > 0.0 && derivative.abs() < f64::MIN_POSITIVE,
3270 "right-tail logit derivative order {order} lost its represented subnormal: {derivative}"
3271 );
3272 }
3273
3274 let royston_eta = 735.0_f64.ln();
3275 let royston = royston_parmar_inverse_link_jet(royston_eta)
3276 .expect("finite Royston-Parmar subnormal-tail eta");
3277 assert!(
3278 royston.d1 < 0.0 && royston.d1.abs() < f64::MIN_POSITIVE,
3279 "Royston-Parmar exact tail derivative must retain its subnormal: {}",
3280 royston.d1
3281 );
3282 }
3283
3284 #[test]
3285 fn sas_all_derivative_seams_refuse_nonfinite_eta_with_one_typed_contract() {
3286 let state = sas_link_state_from_raw(0.25, -0.35).expect("SAS state");
3287 let link = InverseLink::Sas(state);
3288 for eta in [f64::NEG_INFINITY, f64::INFINITY, f64::NAN] {
3289 assert_finite_eta_domain_error(
3290 sas_inverse_link_jet(eta, state.epsilon, state.log_delta)
3291 .expect_err("SAS full jet must refuse"),
3292 "SAS inverse link",
3293 eta,
3294 );
3295 assert_finite_eta_domain_error(
3296 sas_inverse_link_mu_d1(eta, state.epsilon, state.log_delta)
3297 .expect_err("SAS mu/d1 must refuse"),
3298 "SAS inverse link",
3299 eta,
3300 );
3301 assert_finite_eta_domain_error(
3302 sas_inverse_link_pdfthird_derivative(eta, state.epsilon, state.log_delta)
3303 .expect_err("SAS fourth derivative must refuse"),
3304 "SAS inverse link",
3305 eta,
3306 );
3307 assert_finite_eta_domain_error(
3308 sas_inverse_link_pdffourth_derivative(eta, state.epsilon, state.log_delta)
3309 .expect_err("SAS fifth derivative must refuse"),
3310 "SAS inverse link",
3311 eta,
3312 );
3313 assert_finite_eta_domain_error(
3314 sas_inverse_link_jetwith_param_partials(eta, state.epsilon, state.log_delta)
3315 .expect_err("SAS parameter partials must refuse"),
3316 "SAS inverse link",
3317 eta,
3318 );
3319 assert_finite_eta_domain_error(
3320 inverse_link_jet_for_inverse_link(&link, eta).expect_err("SAS kernel must refuse"),
3321 "SAS inverse link",
3322 eta,
3323 );
3324 assert_finite_eta_domain_error(
3325 inverse_link_mu_d1_for_inverse_link(&link, eta)
3326 .expect_err("SAS fast dispatch must refuse"),
3327 "SAS inverse link",
3328 eta,
3329 );
3330 assert_finite_eta_domain_error(
3331 inverse_link_pdfthird_derivative_for_inverse_link(&link, eta)
3332 .expect_err("SAS fourth-derivative dispatch must refuse"),
3333 "SAS inverse link",
3334 eta,
3335 );
3336 assert_finite_eta_domain_error(
3337 inverse_link_pdffourth_derivative_for_inverse_link(&link, eta)
3338 .expect_err("SAS fifth-derivative dispatch must refuse"),
3339 "SAS inverse link",
3340 eta,
3341 );
3342 }
3343 }
3344
3345 #[test]
3346 fn sas_jets_are_finite_at_both_finite_f64_domain_edges() {
3347 let state = sas_link_state_from_raw(0.25, -0.35).expect("SAS state");
3348 for eta in [-f64::MAX, f64::MAX] {
3349 let jet = sas_inverse_link_jet(eta, state.epsilon, state.log_delta)
3350 .expect("finite SAS boundary jet");
3351 let partials =
3352 sas_inverse_link_jetwith_param_partials(eta, state.epsilon, state.log_delta)
3353 .expect("finite SAS boundary partials");
3354 let h4 = sas_inverse_link_pdfthird_derivative(eta, state.epsilon, state.log_delta)
3355 .expect("finite SAS boundary fourth derivative");
3356 let h5 = sas_inverse_link_pdffourth_derivative(eta, state.epsilon, state.log_delta)
3357 .expect("finite SAS boundary fifth derivative");
3358 for value in [
3359 jet.mu,
3360 jet.d1,
3361 jet.d2,
3362 jet.d3,
3363 partials.jet.mu,
3364 partials.jet.d1,
3365 partials.jet.d2,
3366 partials.jet.d3,
3367 partials.djet_depsilon.mu,
3368 partials.djet_depsilon.d1,
3369 partials.djet_depsilon.d2,
3370 partials.djet_depsilon.d3,
3371 partials.djet_dlog_delta.mu,
3372 partials.djet_dlog_delta.d1,
3373 partials.djet_dlog_delta.d2,
3374 partials.djet_dlog_delta.d3,
3375 h4,
3376 h5,
3377 ] {
3378 assert!(
3379 value.is_finite(),
3380 "non-finite SAS boundary jet at eta={eta}: {value}"
3381 );
3382 }
3383 }
3384 }
3385
3386 #[test]
3387 fn royston_parmar_exact_jet_has_no_former_minus_thirty_plateau() {
3388 let left =
3389 royston_parmar_inverse_link_jet(-30.0 - 1.0e-6).expect("finite Royston-Parmar eta");
3390 let center = royston_parmar_inverse_link_jet(-30.0).expect("finite Royston-Parmar eta");
3391 let right =
3392 royston_parmar_inverse_link_jet(-30.0 + 1.0e-6).expect("finite Royston-Parmar eta");
3393
3394 assert!(
3395 left.d1 < 0.0,
3396 "the exact left tail must not be a constant plateau"
3397 );
3398 assert!(center.d1 < 0.0 && right.d1 < 0.0);
3399 let left_relative = ((left.d1 - center.d1) / center.d1).abs();
3400 let right_relative = ((right.d1 - center.d1) / center.d1).abs();
3401 assert!(
3402 left_relative < 2.0e-6,
3403 "left derivative kink: {left_relative}"
3404 );
3405 assert!(
3406 right_relative < 2.0e-6,
3407 "right derivative kink: {right_relative}"
3408 );
3409
3410 for eta in [-f64::MAX, -40.0, -30.0, 0.0, 7.0, 30.0, 40.0, f64::MAX] {
3411 let jet = royston_parmar_inverse_link_jet(eta).expect("finite Royston-Parmar eta");
3412 for value in [jet.mu, jet.d1, jet.d2, jet.d3] {
3413 assert!(
3414 value.is_finite(),
3415 "non-finite Royston-Parmar jet at eta={eta}: {value}"
3416 );
3417 }
3418 }
3419 }
3420
3421 #[test]
3422 fn royston_parmar_seams_refuse_nonfinite_eta_instead_of_clamping() {
3423 let spec = LikelihoodSpec::new(
3424 ResponseFamily::RoystonParmar,
3425 InverseLink::Standard(StandardLink::Identity),
3426 );
3427 for eta in [f64::NEG_INFINITY, f64::INFINITY, f64::NAN] {
3428 assert_finite_eta_domain_error(
3429 royston_parmar_inverse_link_jet(eta)
3430 .expect_err("direct Royston-Parmar jet must refuse"),
3431 "Royston-Parmar survival inverse link",
3432 eta,
3433 );
3434 assert_finite_eta_domain_error(
3435 inverse_link_jet_for_family(&spec, eta)
3436 .expect_err("solver Royston-Parmar jet must refuse"),
3437 "Royston-Parmar survival inverse link",
3438 eta,
3439 );
3440 assert_finite_eta_domain_error(
3441 inverse_link_jet_for_family_public(&spec, eta)
3442 .expect_err("public Royston-Parmar jet must refuse"),
3443 "Royston-Parmar survival inverse link",
3444 eta,
3445 );
3446 }
3447 }
3448
3449 #[test]
3450 fn softmax_jacobian_matchesfd() {
3451 let rho = Array1::from_vec(vec![0.7, -1.2, 0.4]);
3452 let (pi, jac) = softmaxwith_jacobian_last_fixedzero(&rho);
3453 let h = 1e-6;
3454 for j in 0..rho.len() {
3455 let mut rp = rho.clone();
3456 rp[j] += h;
3457 let mut rm = rho.clone();
3458 rm[j] -= h;
3459 let pp = softmax_last_fixedzero(&rp);
3460 let pm = softmax_last_fixedzero(&rm);
3461 let fd = (&pp - &pm).mapv(|v| v / (2.0 * h));
3462 for k in 0..pi.len() {
3463 let err = (jac[[k, j]] - fd[k]).abs();
3464 assert_eq!(
3465 jac[[k, j]].signum(),
3466 fd[k].signum(),
3467 "jac sign mismatch at ({k},{j}): analytic={} fd={}",
3468 jac[[k, j]],
3469 fd[k]
3470 );
3471 assert!(err < 5e-6, "jac mismatch at ({k},{j}): err={err:e}");
3472 }
3473 }
3474 }
3475
3476 #[test]
3477 fn mixture_jet_rho_partials_matchfd() {
3478 let spec = MixtureLinkSpec {
3479 components: vec![
3480 LinkComponent::Probit,
3481 LinkComponent::Logit,
3482 LinkComponent::CLogLog,
3483 LinkComponent::Cauchit,
3484 ],
3485 initial_rho: Array1::from_vec(vec![0.3, -0.6, 0.2]),
3486 };
3487 let state = state_fromspec(&spec).expect("state");
3488 let eta = 0.35;
3489 let out = mixture_inverse_link_jetwith_rho_partials(&state, eta);
3490 let h = 1e-6;
3491 for j in 0..state.rho.len() {
3492 let mut rp = state.rho.clone();
3493 rp[j] += h;
3494 let sp = MixtureLinkSpec {
3495 components: state.components.clone(),
3496 initial_rho: rp,
3497 };
3498 let jp = mixture_inverse_link_jet(&state_fromspec(&sp).expect("sp"), eta);
3499 let mut rm = state.rho.clone();
3500 rm[j] -= h;
3501 let sm = MixtureLinkSpec {
3502 components: state.components.clone(),
3503 initial_rho: rm,
3504 };
3505 let jm = mixture_inverse_link_jet(&state_fromspec(&sm).expect("sm"), eta);
3506 let fd = InverseLinkJet {
3507 mu: (jp.mu - jm.mu) / (2.0 * h),
3508 d1: (jp.d1 - jm.d1) / (2.0 * h),
3509 d2: (jp.d2 - jm.d2) / (2.0 * h),
3510 d3: (jp.d3 - jm.d3) / (2.0 * h),
3511 };
3512 let an = out.djet_drho[j];
3513 assert_eq!(an.mu.signum(), fd.mu.signum());
3514 assert_eq!(an.d1.signum(), fd.d1.signum());
3515 assert_eq!(an.d2.signum(), fd.d2.signum());
3516 assert_eq!(an.d3.signum(), fd.d3.signum());
3517 assert!((an.mu - fd.mu).abs() < 1e-6);
3518 assert!((an.d1 - fd.d1).abs() < 1e-6);
3519 assert!((an.d2 - fd.d2).abs() < 1e-6);
3520 assert!((an.d3 - fd.d3).abs() < 1e-6);
3521 }
3522 }
3523
3524 #[test]
3531 fn sas_d2d2_param_hessian_matchesfd_of_the_first_partial() {
3532 let eta = 0.37;
3533 let epsilon = -0.12;
3534 let log_delta = 0.21;
3535 let h = 1e-6;
3536 let out = sas_inverse_link_jetwith_param_partials(eta, epsilon, log_delta)
3537 .expect("finite SAS eta");
3538 let at = |e: f64, l: f64| {
3539 sas_inverse_link_jetwith_param_partials(eta, e, l).expect("finite SAS eta")
3540 };
3541 let d_eps = [
3543 (at(epsilon + h, log_delta).djet_depsilon.d2
3544 - at(epsilon - h, log_delta).djet_depsilon.d2)
3545 / (2.0 * h),
3546 (at(epsilon, log_delta + h).djet_depsilon.d2
3547 - at(epsilon, log_delta - h).djet_depsilon.d2)
3548 / (2.0 * h),
3549 ];
3550 let d_ld = [
3551 (at(epsilon + h, log_delta).djet_dlog_delta.d2
3552 - at(epsilon - h, log_delta).djet_dlog_delta.d2)
3553 / (2.0 * h),
3554 (at(epsilon, log_delta + h).djet_dlog_delta.d2
3555 - at(epsilon, log_delta - h).djet_dlog_delta.d2)
3556 / (2.0 * h),
3557 ];
3558 for (idx, reference) in [((0, 0), d_eps[0]), ((0, 1), d_eps[1])] {
3559 let got = out.d2d2_dparams2[[idx.0, idx.1]];
3560 assert!(
3561 (got - reference).abs() < 1e-5 * (1.0 + reference.abs()),
3562 "d2d2_dparams2{idx:?} = {got:e} against FD {reference:e}"
3563 );
3564 }
3565 for (idx, reference) in [((1, 0), d_ld[0]), ((1, 1), d_ld[1])] {
3566 let got = out.d2d2_dparams2[[idx.0, idx.1]];
3567 assert!(
3568 (got - reference).abs() < 1e-5 * (1.0 + reference.abs()),
3569 "d2d2_dparams2{idx:?} = {got:e} against FD {reference:e}"
3570 );
3571 }
3572 assert_eq!(out.d2d2_dparams2[[0, 1]], out.d2d2_dparams2[[1, 0]]);
3573 }
3574
3575 #[test]
3578 fn beta_logistic_d2d2_param_hessian_matchesfd_of_the_first_partial() {
3579 let eta = -0.29;
3580 let epsilon = 0.18;
3581 let log_shape_center = 0.24;
3582 let h = 1e-6;
3583 let out =
3584 beta_logistic_inverse_link_jetwith_param_partials(eta, log_shape_center, epsilon);
3585 let at =
3586 |e: f64, l: f64| beta_logistic_inverse_link_jetwith_param_partials(eta, l, e);
3587 let reference = [
3588 [
3589 (at(epsilon + h, log_shape_center).djet_depsilon.d2
3590 - at(epsilon - h, log_shape_center).djet_depsilon.d2)
3591 / (2.0 * h),
3592 (at(epsilon, log_shape_center + h).djet_depsilon.d2
3593 - at(epsilon, log_shape_center - h).djet_depsilon.d2)
3594 / (2.0 * h),
3595 ],
3596 [
3597 (at(epsilon + h, log_shape_center).djet_dlog_delta.d2
3598 - at(epsilon - h, log_shape_center).djet_dlog_delta.d2)
3599 / (2.0 * h),
3600 (at(epsilon, log_shape_center + h).djet_dlog_delta.d2
3601 - at(epsilon, log_shape_center - h).djet_dlog_delta.d2)
3602 / (2.0 * h),
3603 ],
3604 ];
3605 for j in 0..2 {
3606 for k in 0..2 {
3607 let got = out.d2d2_dparams2[[j, k]];
3608 let want = reference[j][k];
3609 assert!(
3610 (got - want).abs() < 1e-4 * (1.0 + want.abs()),
3611 "beta-logistic d2d2_dparams2[{j},{k}] = {got:e} against FD {want:e}"
3612 );
3613 }
3614 }
3615 }
3616
3617 #[test]
3619 fn mixture_d2d2_rho_hessian_matchesfd_of_the_first_partial() {
3620 let state = state_fromspec(&MixtureLinkSpec {
3621 components: vec![
3622 LinkComponent::Probit,
3623 LinkComponent::Logit,
3624 LinkComponent::CLogLog,
3625 ],
3626 initial_rho: Array1::from_vec(vec![0.31, -0.17]),
3627 })
3628 .expect("valid three-component mixture");
3629 let eta = 0.42;
3630 let h = 1e-6;
3631 let out = mixture_inverse_link_jetwith_rho_partials(&state, eta);
3632 let m = state.rho.len();
3633 let shifted = |j: usize, delta: f64| {
3634 let mut rho = state.rho.clone();
3635 rho[j] += delta;
3636 state_fromspec(&MixtureLinkSpec {
3637 components: state.components.clone(),
3638 initial_rho: rho,
3639 })
3640 .expect("valid shifted mixture")
3641 };
3642 for j in 0..m {
3643 for k in 0..m {
3644 let plus = mixture_inverse_link_jetwith_rho_partials(&shifted(k, h), eta);
3645 let minus = mixture_inverse_link_jetwith_rho_partials(&shifted(k, -h), eta);
3646 let want = (plus.djet_drho[j].d2 - minus.djet_drho[j].d2) / (2.0 * h);
3647 let got = out.d2d2_drho2[[j, k]];
3648 assert!(
3649 (got - want).abs() < 1e-5 * (1.0 + want.abs()),
3650 "mixture d2d2_drho2[{j},{k}] = {got:e} against FD {want:e}"
3651 );
3652 }
3653 }
3654 }
3655
3656 #[test]
3657 fn mixture_second_partials_obey_equal_weight_two_component_identity() {
3658 let state = state_fromspec(&MixtureLinkSpec {
3659 components: vec![LinkComponent::Probit, LinkComponent::Logit],
3660 initial_rho: Array1::from_vec(vec![0.0]),
3661 })
3662 .expect("valid two-component mixture");
3663 let out = mixture_inverse_link_jetwith_rho_partials(&state, 0.37);
3664 assert_eq!(out.d2mu_drho2.dim(), (1, 1));
3669 assert_eq!(out.d2d1_drho2.dim(), (1, 1));
3670 assert_eq!(out.d2mu_drho2[[0, 0]], 0.0);
3671 assert_eq!(out.d2d1_drho2[[0, 0]], 0.0);
3672 }
3673
3674 #[test]
3675 fn sas_param_partials_matchfd() {
3676 let eta = 0.37;
3677 let epsilon = -0.12;
3678 let log_delta = 0.21;
3679 let out = sas_inverse_link_jetwith_param_partials(eta, epsilon, log_delta)
3680 .expect("finite SAS eta");
3681 let h = 1e-6;
3682
3683 let ep_p = sas_inverse_link_jet(eta, epsilon + h, log_delta).expect("finite SAS eta");
3684 let ep_m = sas_inverse_link_jet(eta, epsilon - h, log_delta).expect("finite SAS eta");
3685 let fd_ep = InverseLinkJet {
3686 mu: (ep_p.mu - ep_m.mu) / (2.0 * h),
3687 d1: (ep_p.d1 - ep_m.d1) / (2.0 * h),
3688 d2: (ep_p.d2 - ep_m.d2) / (2.0 * h),
3689 d3: (ep_p.d3 - ep_m.d3) / (2.0 * h),
3690 };
3691 assert_eq!(out.djet_depsilon.mu.signum(), fd_ep.mu.signum());
3692 assert_eq!(out.djet_depsilon.d1.signum(), fd_ep.d1.signum());
3693 assert_eq!(out.djet_depsilon.d2.signum(), fd_ep.d2.signum());
3694 assert_eq!(out.djet_depsilon.d3.signum(), fd_ep.d3.signum());
3695 assert!((out.djet_depsilon.mu - fd_ep.mu).abs() < 5e-5);
3696 assert!((out.djet_depsilon.d1 - fd_ep.d1).abs() < 5e-5);
3697 assert!((out.djet_depsilon.d2 - fd_ep.d2).abs() < 5e-5);
3698 assert!((out.djet_depsilon.d3 - fd_ep.d3).abs() < 5e-4);
3699
3700 let ld_p = sas_inverse_link_jet(eta, epsilon, log_delta + h).expect("finite SAS eta");
3701 let ld_m = sas_inverse_link_jet(eta, epsilon, log_delta - h).expect("finite SAS eta");
3702 let fd_ld = InverseLinkJet {
3703 mu: (ld_p.mu - ld_m.mu) / (2.0 * h),
3704 d1: (ld_p.d1 - ld_m.d1) / (2.0 * h),
3705 d2: (ld_p.d2 - ld_m.d2) / (2.0 * h),
3706 d3: (ld_p.d3 - ld_m.d3) / (2.0 * h),
3707 };
3708 assert_eq!(out.djet_dlog_delta.mu.signum(), fd_ld.mu.signum());
3709 assert_eq!(out.djet_dlog_delta.d1.signum(), fd_ld.d1.signum());
3710 assert_eq!(out.djet_dlog_delta.d2.signum(), fd_ld.d2.signum());
3711 assert_eq!(out.djet_dlog_delta.d3.signum(), fd_ld.d3.signum());
3712 assert!((out.djet_dlog_delta.mu - fd_ld.mu).abs() < 5e-5);
3713 assert!((out.djet_dlog_delta.d1 - fd_ld.d1).abs() < 5e-5);
3714 assert!((out.djet_dlog_delta.d2 - fd_ld.d2).abs() < 5e-5);
3715 assert!((out.djet_dlog_delta.d3 - fd_ld.d3).abs() < 5e-4);
3716 }
3717
3718 #[test]
3719 fn sas_second_partials_have_exact_center_identities() {
3720 let out = sas_inverse_link_jetwith_param_partials(0.0, 0.0, 0.0)
3721 .expect("finite SAS center");
3722 let phi0 = normal_pdf(0.0);
3723 assert_eq!(out.d2mu_dparams2, Array2::<f64>::zeros((2, 2)));
3731 assert_eq!(out.d2d1_dparams2[[0, 1]], out.d2d1_dparams2[[1, 0]]);
3732 assert_eq!(
3733 out.d2d1_dparams2[[0, 0]], 0.0,
3734 "ε–ε ∂²(μ') must be exactly zero at the identity-interior center"
3735 );
3736 assert!((out.d2d1_dparams2[[1, 1]] - phi0).abs() < 1.0e-15);
3737 assert_eq!(out.d2d1_dparams2[[0, 1]], 0.0);
3738 }
3739
3740 #[test]
3761 fn sas_family_score_depsilon_matches_fd_and_reproduces_profile_1876() {
3762 let h = 1e-6;
3763 let mu_at = |eta: f64, eps: f64, ld: f64| {
3764 sas_inverse_link_jet(eta, eps, ld)
3765 .expect("finite SAS eta")
3766 .mu
3767 };
3768 let dmu_deps = |eta: f64, eps: f64, ld: f64| {
3769 sas_inverse_link_jetwith_param_partials(eta, eps, ld)
3770 .expect("finite SAS eta")
3771 .djet_depsilon
3772 .mu
3773 };
3774 let row_ll = |y: f64, w: f64, mu: f64| w * (y * mu.ln() + (1.0 - y) * (1.0 - mu).ln());
3776 let a1 = |y: f64, w: f64, mu: f64| w * (y / mu - (1.0 - y) / (1.0 - mu));
3777
3778 let etas = [-1.0, -0.6, -0.2, 0.15, 0.5, 0.9];
3780 let epsilons = [-0.5, -0.2, 0.0, 0.3, 0.6];
3781 let log_deltas = [-0.3, 0.0, 0.4];
3782 for &eta in &etas {
3783 for &eps in &epsilons {
3784 for &ld in &log_deltas {
3785 let mu0 = mu_at(eta, eps, ld);
3786 if !(0.02..=0.98).contains(&mu0) {
3789 continue;
3790 }
3791 let dmu = dmu_deps(eta, eps, ld);
3792 for &y in &[0.0_f64, 1.0] {
3793 let analytic = a1(y, 1.0, mu0) * dmu; let fd = (row_ll(y, 1.0, mu_at(eta, eps + h, ld))
3795 - row_ll(y, 1.0, mu_at(eta, eps - h, ld)))
3796 / (2.0 * h);
3797 let scale = analytic.abs().max(fd.abs()).max(1.0);
3798 assert_eq!(
3799 analytic.signum(),
3800 fd.signum(),
3801 "∂ℓ/∂ε sign mismatch η={eta} ε={eps} log_δ={ld} y={y}: \
3802 analytic={analytic:e} fd={fd:e}"
3803 );
3804 assert!(
3805 (analytic - fd).abs() < 1e-5 * scale,
3806 "∂ℓ/∂ε magnitude mismatch η={eta} ε={eps} log_δ={ld} y={y}: \
3807 analytic={analytic:e} fd={fd:e}"
3808 );
3809 }
3810 }
3811 }
3812 }
3813
3814 let eps_true = 0.38;
3816 let ld_true = 0.0;
3817 let etas_ds: Vec<f64> = (0..21).map(|i| -1.0 + 0.1 * i as f64).collect();
3818 let y: Vec<f64> = etas_ds
3819 .iter()
3820 .map(|&e| mu_at(e, eps_true, ld_true))
3821 .collect();
3822
3823 let grad_nll = |eps: f64| -> (f64, f64) {
3825 let mut analytic = 0.0;
3826 let mut nll_p = 0.0;
3827 let mut nll_m = 0.0;
3828 for (i, &eta) in etas_ds.iter().enumerate() {
3829 let mu0 = mu_at(eta, eps, ld_true);
3830 analytic += -a1(y[i], 1.0, mu0) * dmu_deps(eta, eps, ld_true);
3831 nll_p += -row_ll(y[i], 1.0, mu_at(eta, eps + h, ld_true));
3832 nll_m += -row_ll(y[i], 1.0, mu_at(eta, eps - h, ld_true));
3833 }
3834 (analytic, (nll_p - nll_m) / (2.0 * h))
3835 };
3836
3837 for &eps in &[0.0, eps_true, 0.6] {
3839 let (analytic, fd) = grad_nll(eps);
3840 let scale = analytic.abs().max(fd.abs()).max(1.0);
3841 assert!(
3842 (analytic - fd).abs() < 1e-4 * scale,
3843 "summed ∂NLL/∂ε analytic≠fd at ε={eps}: {analytic:e} vs {fd:e}"
3844 );
3845 }
3846 let (g_below, _) = grad_nll(0.0);
3848 let (g_at, _) = grad_nll(eps_true);
3849 let (g_above, _) = grad_nll(0.6);
3850 assert!(
3851 g_below < -1.0,
3852 "expected strongly negative ∂NLL/∂ε below ε* (pushes ε up toward truth), got {g_below:e}"
3853 );
3854 assert!(
3855 g_at.abs() < 1e-6,
3856 "expected ≈0 ∂NLL/∂ε at the planted ε* (score identity), got {g_at:e}"
3857 );
3858 assert!(
3859 g_above > 1.0,
3860 "expected strongly positive ∂NLL/∂ε above ε*, got {g_above:e}"
3861 );
3862 assert!(
3863 g_below < g_at && g_at < g_above,
3864 "∂NLL/∂ε must strictly increase through ε* (distinguishes ±ε): \
3865 {g_below:e} < {g_at:e} < {g_above:e}"
3866 );
3867 }
3868
3869 #[test]
3870 fn sas_jet_extreme_inputs_stay_finite() {
3871 let cases = [
3872 (-1e6, 0.0, 0.0),
3873 (1e6, 0.0, 0.0),
3874 (3.0, 12.0, 12.0),
3875 (-3.0, -12.0, -12.0),
3876 (0.5, 40.0, 10.0),
3877 (0.5, -40.0, -10.0),
3878 ];
3879 for (eta, eps, log_delta) in cases {
3880 let j = sas_inverse_link_jet(eta, eps, log_delta).expect("finite SAS eta");
3881 assert!(j.mu.is_finite());
3882 assert!(j.d1.is_finite());
3883 assert!(j.d2.is_finite());
3884 assert!(j.d3.is_finite());
3885 let p = sas_inverse_link_jetwith_param_partials(eta, eps, log_delta)
3886 .expect("finite SAS eta");
3887 assert!(p.djet_depsilon.mu.is_finite());
3888 assert!(p.djet_depsilon.d1.is_finite());
3889 assert!(p.djet_depsilon.d2.is_finite());
3890 assert!(p.djet_depsilon.d3.is_finite());
3891 assert!(p.djet_dlog_delta.mu.is_finite());
3892 assert!(p.djet_dlog_delta.d1.is_finite());
3893 assert!(p.djet_dlog_delta.d2.is_finite());
3894 assert!(p.djet_dlog_delta.d3.is_finite());
3895 }
3896 }
3897
3898 #[test]
3899 fn sas_param_partials_remain_finite_in_extreme_region() {
3900 let eta = 10.0;
3901 let epsilon = -60.0;
3902 let log_delta = 40.0;
3903 let j = sas_inverse_link_jetwith_param_partials(eta, epsilon, log_delta)
3904 .expect("finite SAS eta");
3905 assert!(j.djet_depsilon.mu.is_finite());
3906 assert!(j.djet_depsilon.d1.is_finite());
3907 assert!(j.djet_depsilon.d2.is_finite());
3908 assert!(j.djet_depsilon.d3.is_finite());
3909 assert!(j.djet_dlog_delta.mu.is_finite());
3910 assert!(j.djet_dlog_delta.d1.is_finite());
3911 assert!(j.djet_dlog_delta.d2.is_finite());
3912 assert!(j.djet_dlog_delta.d3.is_finite());
3913 }
3914
3915 #[test]
3916 fn sas_eta_jets_matchfd() {
3917 let eta = -0.43;
3918 let epsilon = 0.27;
3919 let log_delta = -0.31;
3920 let h = 1e-5;
3921 let j0 = sas_inverse_link_jet(eta, epsilon, log_delta).expect("finite SAS eta");
3922 let jp = sas_inverse_link_jet(eta + h, epsilon, log_delta).expect("finite SAS eta");
3923 let jm = sas_inverse_link_jet(eta - h, epsilon, log_delta).expect("finite SAS eta");
3924 let d1fd = (jp.mu - jm.mu) / (2.0 * h);
3925 let d2fd = (jp.d1 - jm.d1) / (2.0 * h);
3926 let d3fd = (jp.d2 - jm.d2) / (2.0 * h);
3927 assert_eq!(j0.d1.signum(), d1fd.signum());
3928 assert_eq!(j0.d2.signum(), d2fd.signum());
3929 assert_eq!(j0.d3.signum(), d3fd.signum());
3930 assert!((j0.d1 - d1fd).abs() < 5e-5);
3931 assert!((j0.d2 - d2fd).abs() < 2e-4);
3932 assert!((j0.d3 - d3fd).abs() < 1e-3);
3933 }
3934
3935 #[test]
3936 fn family_dispatch_resolves_parameterized_links_from_spec() {
3937 let sas_state = sas_link_state_from_raw(0.0, 0.0).expect("sas state");
3941 let expected_sas =
3942 sas_inverse_link_jet(0.1, sas_state.epsilon, sas_state.log_delta).expect("direct SAS");
3943 let sas_spec = gam_problem::LikelihoodSpec {
3944 response: gam_problem::ResponseFamily::Binomial,
3945 link: InverseLink::Sas(sas_state),
3946 };
3947 let sas_jet = inverse_link_jet_for_family(&sas_spec, 0.1).expect("sas jet");
3948 assert_eq!(sas_jet.mu.to_bits(), expected_sas.mu.to_bits());
3949 assert_eq!(sas_jet.d1.to_bits(), expected_sas.d1.to_bits());
3950
3951 let mix_state = MixtureLinkState {
3952 components: vec![LinkComponent::Logit, LinkComponent::Probit],
3953 rho: ndarray::array![0.0],
3954 pi: ndarray::array![0.5, 0.5],
3955 };
3956 let expected_mix = mixture_inverse_link_jet(&mix_state, 0.1);
3957 let mix_spec = gam_problem::LikelihoodSpec {
3958 response: gam_problem::ResponseFamily::Binomial,
3959 link: InverseLink::Mixture(mix_state),
3960 };
3961 let mix_jet = inverse_link_jet_for_family(&mix_spec, 0.1).expect("mix jet");
3962 assert_eq!(mix_jet.mu.to_bits(), expected_mix.mu.to_bits());
3963 assert_eq!(mix_jet.d1.to_bits(), expected_mix.d1.to_bits());
3964 }
3965
3966 #[test]
3967 fn beta_logistic_reduces_to_logit_at_delta0_epsilon0() {
3968 let etas = [-40.0, -30.0, -5.0, 0.42, 5.0, 30.0, 40.0];
3969 for eta in etas {
3970 let j_bl = beta_logistic_inverse_link_jet(eta, 0.0, 0.0);
3971 let expected_mu = gam_linalg::utils::stable_logistic(eta);
3972 let expected_d1 = (-gam_linalg::utils::stable_softplus(-eta)
3973 - gam_linalg::utils::stable_softplus(eta))
3974 .exp();
3975 assert!(
3976 (j_bl.mu - expected_mu).abs() <= 1e-15 * expected_mu.abs().max(1.0),
3977 "mu mismatch at eta={eta}: got {}, expected {}",
3978 j_bl.mu,
3979 expected_mu
3980 );
3981 assert!(
3982 (j_bl.d1 - expected_d1).abs() <= 1e-12 * expected_d1.abs().max(f64::MIN_POSITIVE),
3983 "d1 mismatch at eta={eta}: got {}, expected {}",
3984 j_bl.d1,
3985 expected_d1
3986 );
3987 assert!(j_bl.d1 > 0.0, "d1 should stay positive at eta={eta}");
3988 }
3989
3990 let eta = 0.42;
3991 let j_bl = beta_logistic_inverse_link_jet(eta, 0.0, 0.0);
3992 let j_logit = component_inverse_link_jet(LinkComponent::Logit, eta);
3993 assert!((j_bl.d2 - j_logit.d2).abs() < 1e-10);
3994 assert!((j_bl.d3 - j_logit.d3).abs() < 1e-10);
3995 }
3996
3997 #[test]
3998 fn beta_logistic_eta_jets_matchfd() {
3999 let eta = -0.31;
4000 let delta = 0.27;
4001 let epsilon = -0.19;
4002 let h = 1e-5;
4003 let j0 = beta_logistic_inverse_link_jet(eta, delta, epsilon);
4004 let jp = beta_logistic_inverse_link_jet(eta + h, delta, epsilon);
4005 let jm = beta_logistic_inverse_link_jet(eta - h, delta, epsilon);
4006 let d1fd = (jp.mu - jm.mu) / (2.0 * h);
4007 let d2fd = (jp.d1 - jm.d1) / (2.0 * h);
4008 let d3fd = (jp.d2 - jm.d2) / (2.0 * h);
4009 assert_eq!(j0.d1.signum(), d1fd.signum());
4010 assert_eq!(j0.d2.signum(), d2fd.signum());
4011 assert_eq!(j0.d3.signum(), d3fd.signum());
4012 assert!((j0.d1 - d1fd).abs() < 5e-5);
4013 assert!((j0.d2 - d2fd).abs() < 5e-5);
4014 assert!((j0.d3 - d3fd).abs() < 2e-4);
4015 }
4016
4017 #[test]
4018 fn standard_kernel_structs_match_component_jets() {
4019 let eta = 0.73;
4020 assert_eq!(
4021 ProbitLinkKernel.jet(eta).expect("probit"),
4022 component_inverse_link_jet(LinkComponent::Probit, eta)
4023 );
4024 assert_eq!(
4025 LogitLinkKernel.jet(eta).expect("logit"),
4026 component_inverse_link_jet(LinkComponent::Logit, eta)
4027 );
4028 assert_eq!(
4029 CLogLogLinkKernel.jet(eta).expect("cloglog"),
4030 component_inverse_link_jet(LinkComponent::CLogLog, eta)
4031 );
4032 assert_eq!(
4033 LogLogLinkKernel.jet(eta).expect("loglog"),
4034 component_inverse_link_jet(LinkComponent::LogLog, eta)
4035 );
4036 assert_eq!(
4037 CauchitLinkKernel.jet(eta).expect("cauchit"),
4038 component_inverse_link_jet(LinkComponent::Cauchit, eta)
4039 );
4040 }
4041
4042 #[test]
4043 fn all_component_eta_jets_matchfd() {
4044 let components = [
4045 LinkComponent::Logit,
4046 LinkComponent::Probit,
4047 LinkComponent::CLogLog,
4048 LinkComponent::LogLog,
4049 LinkComponent::Cauchit,
4050 ];
4051 let points = [-3.0, -1.1, -0.2, 0.0, 0.7, 1.8, 3.2];
4052 let h = 1e-5;
4053 for c in components {
4054 for &eta in &points {
4055 let j0 = component_inverse_link_jet(c, eta);
4056 let jp = component_inverse_link_jet(c, eta + h);
4057 let jm = component_inverse_link_jet(c, eta - h);
4058 let d1fd = (jp.mu - jm.mu) / (2.0 * h);
4059 let d2fd = (jp.d1 - jm.d1) / (2.0 * h);
4060 let d3fd = (jp.d2 - jm.d2) / (2.0 * h);
4061 let d1_tol = if matches!(c, LinkComponent::CLogLog | LinkComponent::LogLog) {
4062 1.2e-4
4063 } else {
4064 5e-5
4065 };
4066 let d2_tol = if matches!(c, LinkComponent::CLogLog | LinkComponent::LogLog) {
4067 4e-4
4068 } else {
4069 1.2e-4
4070 };
4071 let d3_tol = if matches!(c, LinkComponent::CLogLog | LinkComponent::LogLog) {
4072 1.2e-3
4073 } else {
4074 4e-4
4075 };
4076 if j0.d1.abs().max(d1fd.abs()) > 1e-10 {
4077 assert_eq!(
4078 j0.d1.signum(),
4079 d1fd.signum(),
4080 "d1 sign mismatch for {c:?} eta={eta}"
4081 );
4082 }
4083 if j0.d2.abs().max(d2fd.abs()) > 1e-10 {
4084 assert_eq!(
4085 j0.d2.signum(),
4086 d2fd.signum(),
4087 "d2 sign mismatch for {c:?} eta={eta}: analytic={} fd={}",
4088 j0.d2,
4089 d2fd
4090 );
4091 }
4092 if j0.d3.abs().max(d3fd.abs()) > 1e-10 {
4093 assert_eq!(
4094 j0.d3.signum(),
4095 d3fd.signum(),
4096 "d3 sign mismatch for {c:?} eta={eta}"
4097 );
4098 }
4099 assert!(
4100 (j0.d1 - d1fd).abs() < d1_tol,
4101 "d1 mismatch for {c:?} eta={eta}: analytic={} fd={}",
4102 j0.d1,
4103 d1fd
4104 );
4105 assert!(
4106 (j0.d2 - d2fd).abs() < d2_tol,
4107 "d2 mismatch for {c:?} eta={eta}: analytic={} fd={}",
4108 j0.d2,
4109 d2fd
4110 );
4111 assert!(
4112 (j0.d3 - d3fd).abs() < d3_tol,
4113 "d3 mismatch for {c:?} eta={eta}: analytic={} fd={}",
4114 j0.d3,
4115 d3fd
4116 );
4117 }
4118 }
4119 }
4120
4121 #[test]
4122 fn sas_center_matches_probit_at_delta1_epsilon0() {
4123 let etas = [-3.0, -1.2, -0.3, 0.0, 0.4, 1.7, 3.0];
4126 for eta in etas {
4127 let sas = sas_inverse_link_jet(eta, 0.0, 0.0).expect("finite SAS eta");
4128 let probit = ProbitLinkKernel.jet(eta).expect("probit");
4129 assert_eq!(sas.mu.to_bits(), probit.mu.to_bits(), "mu at eta={eta}");
4130 assert_eq!(sas.d1.to_bits(), probit.d1.to_bits(), "d1 at eta={eta}");
4131 assert_eq!(sas.d2.to_bits(), probit.d2.to_bits(), "d2 at eta={eta}");
4132 assert_eq!(sas.d3.to_bits(), probit.d3.to_bits(), "d3 at eta={eta}");
4133 }
4134 }
4135
4136 #[test]
4147 fn sas_probit_reduction_is_exact_on_full_path_and_smooth_across_epsilon_zero() {
4148 let etas = [-3.0, -1.2, -0.3, 0.0, 0.4, 1.7, 3.0];
4149 for &eta in &etas {
4150 let probit = ProbitLinkKernel.jet(eta).expect("probit");
4151 for &eps in &[1e-11_f64, -1e-11] {
4155 let sas = sas_inverse_link_jet(eta, eps, 0.0).expect("finite SAS eta");
4156 assert!(
4159 (sas.mu - probit.mu).abs() < 1e-11,
4160 "full-path μ off probit at eta={eta} eps={eps}: {} vs {}",
4161 sas.mu,
4162 probit.mu
4163 );
4164 assert!(
4165 (sas.d1 - probit.d1).abs() < 1e-10,
4166 "full-path d1 off probit at eta={eta} eps={eps}"
4167 );
4168 }
4169 let center = sas_inverse_link_jet(eta, 0.0, 0.0).expect("fast path").mu;
4173 let lo = sas_inverse_link_jet(eta, -1e-11, 0.0).expect("full path").mu;
4174 let hi = sas_inverse_link_jet(eta, 1e-11, 0.0).expect("full path").mu;
4175 assert!(
4176 (hi - center).abs() < 1e-11 && (center - lo).abs() < 1e-11,
4177 "μ cliff across ε=0 at eta={eta}: lo={lo} center={center} hi={hi}"
4178 );
4179 }
4180 }
4181
4182 #[test]
4191 fn smooth_bound_jet_tower_is_c5_and_fd_exact() {
4192 let b = SAS_U_CLAMP;
4193 let a = SPLICE_INTERIOR_FRAC * b; let c = (2.0 - SPLICE_INTERIOR_FRAC) * b; let jet = |x: f64| smooth_bound_jet(x, b);
4196
4197 for &x in &[0.0, 0.5, 17.3, a - 1e-9, a] {
4199 let j = jet(x);
4200 assert_eq!(j.g, x, "interior identity value at x={x}");
4201 assert_eq!(j.d1, 1.0, "interior d1 at x={x}");
4202 assert_eq!((j.d2, j.d3, j.d4, j.d5), (0.0, 0.0, 0.0, 0.0));
4203 }
4204 for &x in &[c, c + 1e-9, 75.0, 1e12, f64::MAX] {
4206 let j = jet(x);
4207 assert_eq!(j.g, b, "saturation value at x={x}");
4208 assert_eq!((j.d1, j.d2, j.d3, j.d4, j.d5), (0.0, 0.0, 0.0, 0.0, 0.0));
4209 }
4210 assert_eq!(jet(c).g, b);
4212
4213 for &x in &[3.0, a - 2.0, 44.0, 50.0, 56.0, 100.0] {
4215 let p = jet(x);
4216 let m = jet(-x);
4217 assert_eq!(m.g, -p.g, "g odd at x={x}");
4218 assert_eq!(m.d1, p.d1, "d1 even at x={x}");
4219 assert_eq!(m.d2, -p.d2, "d2 odd at x={x}");
4220 assert_eq!(m.d3, p.d3, "d3 even at x={x}");
4221 assert_eq!(m.d4, -p.d4, "d4 odd at x={x}");
4222 assert_eq!(m.d5, p.d5, "d5 even at x={x}");
4223 }
4224
4225 for i in 0..=240 {
4227 let x = -80.0 + 160.0 * (i as f64) / 240.0;
4228 let j = jet(x);
4229 assert!((0.0..=1.0).contains(&j.d1), "d1 out of [0,1] at x={x}: {}", j.d1);
4230 assert!(j.g.abs() <= b + 1e-12, "|g| exceeds B at x={x}: {}", j.g);
4231 }
4232
4233 let h = 0.02;
4240 for &x0 in &[45.0, 47.5, 50.0, 52.5, 55.0, -47.5, -52.5] {
4241 let jp = jet(x0 + h);
4242 let jm = jet(x0 - h);
4243 let j0 = jet(x0);
4244 let fd = |hi: f64, lo: f64| (hi - lo) / (2.0 * h);
4245 let checks = [
4246 ("d1", j0.d1, fd(jp.g, jm.g), 2e-6),
4247 ("d2", j0.d2, fd(jp.d1, jm.d1), 2e-5),
4248 ("d3", j0.d3, fd(jp.d2, jm.d2), 2e-4),
4249 ("d4", j0.d4, fd(jp.d3, jm.d3), 2e-3),
4250 ("d5", j0.d5, fd(jp.d4, jm.d4), 2e-2),
4251 ];
4252 for (name, analytic, numeric, tol) in checks {
4253 assert!(
4254 (analytic - numeric).abs() < tol * (1.0 + analytic.abs()),
4255 "smooth_bound {name} FD mismatch at x={x0}: analytic={analytic:e} fd={numeric:e}"
4256 );
4257 }
4258 }
4259 }
4260
4261 #[test]
4275 fn sas_link_complement_mirrors_forward_map_across_finite_domain() {
4276 let params = [
4277 (0.0, 0.0), (0.25, -0.35), (0.0, -6.0), (-0.5, -6.0),
4281 (0.3, 6.0), ];
4283 let etas = [
4284 -f64::MAX,
4285 -1e160, -1e6,
4287 -3.0,
4288 -0.2,
4289 0.0,
4290 0.4,
4291 3.0,
4292 1e6,
4293 1e160,
4294 f64::MAX,
4295 ];
4296 for &(epsilon, log_delta) in ¶ms {
4297 for &eta in &etas {
4298 let mu = sas_inverse_link_jet(eta, epsilon, log_delta)
4299 .expect("finite SAS eta")
4300 .mu;
4301 let complement = sas_link_complement(eta, epsilon, log_delta, mu);
4302 assert!(
4303 complement.is_finite() && (0.0..=1.0).contains(&complement),
4304 "SAS complement out of [0,1] at η={eta} ε={epsilon} log_δ={log_delta}: {complement}"
4305 );
4306 let sum = mu + complement;
4309 assert!(
4310 (sum - 1.0).abs() < 1e-12,
4311 "SAS complement off the forward surface at η={eta} ε={epsilon} \
4312 log_δ={log_delta}: μ={mu} complement={complement} μ+comp={sum}"
4313 );
4314 }
4315 }
4316 }
4317
4318 #[test]
4329 fn beta_logistic_shapes_are_bounded_and_interior_exact_2685() {
4330 for &(center, epsilon) in &[(0.0, 0.0), (0.5, -0.25), (-0.7, 0.3)] {
4332 let (a, b) = beta_logistic_shapes(center, epsilon);
4333 assert_eq!(a.to_bits(), (center - epsilon).exp().to_bits());
4334 assert_eq!(b.to_bits(), (center + epsilon).exp().to_bits());
4335 }
4336
4337 let floor = (-BETA_LOGISTIC_LOG_SHAPE_BOUND).exp();
4338 let ceiling = BETA_LOGISTIC_LOG_SHAPE_BOUND.exp();
4339 for &(center, epsilon) in &[
4341 (-5.246_934_642_530_043_f64, -0.458_486_194_756_360_9_f64),
4342 (5.246_934_642_530_043, 0.458_486_194_756_360_9),
4343 (-40.0, 12.0),
4344 (40.0, -12.0),
4345 ] {
4346 let (a, b) = beta_logistic_shapes(center, epsilon);
4347 assert!(
4348 (floor..=ceiling).contains(&a) && (floor..=ceiling).contains(&b),
4349 "beta-logistic shapes escaped [{floor:e}, {ceiling:e}] at (center={center}, epsilon={epsilon}): a={a:e} b={b:e}"
4350 );
4351 }
4352
4353 let jet = beta_logistic_inverse_link_jet(
4358 0.0,
4359 -5.246_934_642_530_043,
4360 -0.458_486_194_756_360_9,
4361 );
4362 assert!(
4363 jet.d1 > 1.0e-2,
4364 "bounded beta-logistic link sensitivity collapsed at the runaway theta: d1={} (canonical logit is 0.25)",
4365 jet.d1
4366 );
4367 }
4368
4369 #[test]
4375 fn beta_logistic_param_partials_matchfd_inside_the_shape_splice_2685() {
4376 let bound = BETA_LOGISTIC_LOG_SHAPE_BOUND;
4377 let eta = -0.41;
4378 let center = bound;
4381 let epsilon = 0.15 * bound;
4382 assert!((0.8 * bound..1.2 * bound).contains(&(center - epsilon)));
4383 assert!((0.8 * bound..1.2 * bound).contains(&(center + epsilon)));
4384
4385 let out = beta_logistic_inverse_link_jetwith_param_partials(eta, center, epsilon);
4386 let h = 1e-6;
4387 let fd = |plus: InverseLinkJet, minus: InverseLinkJet| InverseLinkJet {
4388 mu: (plus.mu - minus.mu) / (2.0 * h),
4389 d1: (plus.d1 - minus.d1) / (2.0 * h),
4390 d2: (plus.d2 - minus.d2) / (2.0 * h),
4391 d3: (plus.d3 - minus.d3) / (2.0 * h),
4392 };
4393 let fd_center = fd(
4394 beta_logistic_inverse_link_jet(eta, center + h, epsilon),
4395 beta_logistic_inverse_link_jet(eta, center - h, epsilon),
4396 );
4397 let fd_epsilon = fd(
4398 beta_logistic_inverse_link_jet(eta, center, epsilon + h),
4399 beta_logistic_inverse_link_jet(eta, center, epsilon - h),
4400 );
4401 let close = |analytic: f64, numeric: f64, what: &str| {
4404 let tol = 1.0e-7 + 1.0e-5 * analytic.abs().max(numeric.abs());
4405 assert!(
4406 (analytic - numeric).abs() <= tol,
4407 "{what}: analytic={analytic:e} fd={numeric:e} tol={tol:e}"
4408 );
4409 };
4410 close(out.djet_dlog_delta.mu, fd_center.mu, "dmu/dcenter");
4411 close(out.djet_dlog_delta.d1, fd_center.d1, "dd1/dcenter");
4412 close(out.djet_dlog_delta.d2, fd_center.d2, "dd2/dcenter");
4413 close(out.djet_dlog_delta.d3, fd_center.d3, "dd3/dcenter");
4414 close(out.djet_depsilon.mu, fd_epsilon.mu, "dmu/depsilon");
4415 close(out.djet_depsilon.d1, fd_epsilon.d1, "dd1/depsilon");
4416 close(out.djet_depsilon.d2, fd_epsilon.d2, "dd2/depsilon");
4417 close(out.djet_depsilon.d3, fd_epsilon.d3, "dd3/depsilon");
4418
4419 assert!(out.djet_dlog_delta.mu.abs() > 1.0e-6);
4422 assert!(out.djet_depsilon.mu.abs() > 1.0e-6);
4423 }
4424
4425 #[test]
4426 fn beta_logistic_param_partials_matchfd() {
4427 let eta = -0.41;
4428 let delta = 0.23;
4429 let epsilon = -0.17;
4430 let out = beta_logistic_inverse_link_jetwith_param_partials(eta, delta, epsilon);
4431 let h = 1e-6;
4432
4433 let dp = beta_logistic_inverse_link_jet(eta, delta + h, epsilon);
4434 let dm = beta_logistic_inverse_link_jet(eta, delta - h, epsilon);
4435 let fd_delta = InverseLinkJet {
4436 mu: (dp.mu - dm.mu) / (2.0 * h),
4437 d1: (dp.d1 - dm.d1) / (2.0 * h),
4438 d2: (dp.d2 - dm.d2) / (2.0 * h),
4439 d3: (dp.d3 - dm.d3) / (2.0 * h),
4440 };
4441 assert_eq!(out.djet_dlog_delta.mu.signum(), fd_delta.mu.signum());
4442 assert_eq!(out.djet_dlog_delta.d1.signum(), fd_delta.d1.signum());
4443 assert_eq!(out.djet_dlog_delta.d2.signum(), fd_delta.d2.signum());
4444 assert_eq!(out.djet_dlog_delta.d3.signum(), fd_delta.d3.signum());
4445 assert!((out.djet_dlog_delta.mu - fd_delta.mu).abs() < 5e-5);
4446 assert!((out.djet_dlog_delta.d1 - fd_delta.d1).abs() < 5e-5);
4447 assert!((out.djet_dlog_delta.d2 - fd_delta.d2).abs() < 1.2e-4);
4448 assert!((out.djet_dlog_delta.d3 - fd_delta.d3).abs() < 4e-4);
4449
4450 let ep = beta_logistic_inverse_link_jet(eta, delta, epsilon + h);
4451 let em = beta_logistic_inverse_link_jet(eta, delta, epsilon - h);
4452 let fd_epsilon = InverseLinkJet {
4453 mu: (ep.mu - em.mu) / (2.0 * h),
4454 d1: (ep.d1 - em.d1) / (2.0 * h),
4455 d2: (ep.d2 - em.d2) / (2.0 * h),
4456 d3: (ep.d3 - em.d3) / (2.0 * h),
4457 };
4458 assert_eq!(out.djet_depsilon.mu.signum(), fd_epsilon.mu.signum());
4459 assert_eq!(out.djet_depsilon.d1.signum(), fd_epsilon.d1.signum());
4460 assert_eq!(out.djet_depsilon.d2.signum(), fd_epsilon.d2.signum());
4461 assert_eq!(out.djet_depsilon.d3.signum(), fd_epsilon.d3.signum());
4462 assert!((out.djet_depsilon.mu - fd_epsilon.mu).abs() < 5e-5);
4463 assert!((out.djet_depsilon.d1 - fd_epsilon.d1).abs() < 5e-5);
4464 assert!((out.djet_depsilon.d2 - fd_epsilon.d2).abs() < 1.2e-4);
4465 assert!((out.djet_depsilon.d3 - fd_epsilon.d3).abs() < 4e-4);
4466 }
4467
4468 #[test]
4469 fn beta_logistic_second_partials_obey_center_symmetry() {
4470 let out = beta_logistic_inverse_link_jetwith_param_partials(0.0, 0.37, 0.0);
4471 assert_eq!(out.d2mu_dparams2[[0, 1]], out.d2mu_dparams2[[1, 0]]);
4476 assert_eq!(out.d2d1_dparams2[[0, 1]], out.d2d1_dparams2[[1, 0]]);
4477 assert!(out.d2mu_dparams2[[0, 0]].abs() < 1.0e-12);
4478 assert!(out.d2mu_dparams2[[1, 1]].abs() < 1.0e-12);
4479 assert!(out.d2d1_dparams2[[0, 1]].abs() < 1.0e-12);
4480 }
4481
4482 #[test]
4483 fn beta_logistic_left_tail_uses_unclamped_log_space() {
4484 let eta = -40.0_f64;
4485 let delta = 0.2_f64;
4486 let epsilon = -0.1_f64;
4487 let a = (delta - epsilon).exp();
4488 let b = (delta + epsilon).exp();
4489 let expected_mu = beta_reg(a, b, eta.exp());
4490 let out = beta_logistic_inverse_link_jet(eta, delta, epsilon);
4491
4492 assert!(
4493 (out.mu - expected_mu).abs() <= 1e-12 * expected_mu.abs().max(f64::MIN_POSITIVE),
4494 "left-tail mu mismatch: got {}, expected {}",
4495 out.mu,
4496 expected_mu
4497 );
4498 assert!(out.d1 > 0.0);
4499 assert!(out.d2 > 0.0);
4500 assert!(out.d3 > 0.0);
4501 assert!(out.d1 < 1e-20);
4502
4503 let partials = beta_logistic_inverse_link_jetwith_param_partials(eta, delta, epsilon);
4504 assert!(partials.jet.d1 > 0.0);
4505 assert!(partials.jet.d2 > 0.0);
4506 assert!(partials.jet.d3 > 0.0);
4507 assert!(partials.djet_dlog_delta.d1.is_finite());
4508 assert!(partials.djet_depsilon.d1.is_finite());
4509 }
4510
4511 #[test]
4512 fn beta_logistic_mu_is_symmetric_in_logistic_tails() {
4513 let delta = 0.2;
4514 let epsilon = -0.35;
4515 let etas = [-40.0, -30.0, -5.0, -0.42, 0.0, 0.42, 5.0, 30.0, 40.0];
4516 for eta in etas {
4517 let left = beta_logistic_inverse_link_jet(eta, delta, epsilon).mu;
4518 let right = 1.0 - beta_logistic_inverse_link_jet(-eta, delta, -epsilon).mu;
4519 assert!(
4520 (left - right).abs() <= 1e-14,
4521 "symmetry mismatch at eta={eta}: left={left}, right={right}"
4522 );
4523 }
4524 }
4525
4526 #[test]
4527 fn inverse_link_pdfthird_derivative_matches_d3_finite_difference() {
4528 let sas = InverseLink::Sas(sas_link_state_from_raw(-0.25, 0.35).expect("sas state"));
4529 let beta_logistic = InverseLink::BetaLogistic(SasLinkState {
4530 epsilon: 0.18,
4531 log_delta: -0.22,
4532 delta: (-0.22_f64).exp(),
4533 });
4534 let mixture = InverseLink::Mixture(
4535 state_fromspec(&MixtureLinkSpec {
4536 components: vec![
4537 LinkComponent::Probit,
4538 LinkComponent::Logit,
4539 LinkComponent::CLogLog,
4540 LinkComponent::Cauchit,
4541 ],
4542 initial_rho: Array1::from_vec(vec![0.35, -0.45, 0.2]),
4543 })
4544 .expect("mixture state"),
4545 );
4546 let links = [
4547 InverseLink::Standard(StandardLink::Probit),
4548 InverseLink::Standard(StandardLink::Logit),
4549 InverseLink::Standard(StandardLink::CLogLog),
4550 sas,
4551 beta_logistic,
4552 mixture,
4553 ];
4554 let etas = [-1.1, -0.2, 0.6];
4555 let h = 1e-5;
4556
4557 for link in &links {
4558 for &eta in &etas {
4559 let jp = inverse_link_jet_for_inverse_link(link, eta + h).expect("jet+");
4560 let jm = inverse_link_jet_for_inverse_link(link, eta - h).expect("jet-");
4561 let d4fd = (jp.d3 - jm.d3) / (2.0 * h);
4562 let d4 = inverse_link_pdfthird_derivative_for_inverse_link(link, eta)
4563 .expect("analytic d4");
4564 assert_eq!(
4565 d4.signum(),
4566 d4fd.signum(),
4567 "d4 sign mismatch for {:?} at eta={eta}: analytic={} fd={}",
4568 link,
4569 d4,
4570 d4fd
4571 );
4572 assert!(
4573 (d4 - d4fd).abs() < 5e-3,
4574 "d4 mismatch for {:?} at eta={eta}: analytic={} fd={}",
4575 link,
4576 d4,
4577 d4fd
4578 );
4579 }
4580 }
4581 }
4582
4583 #[test]
4584 fn cloglog_large_finite_eta_should_saturate_without_nan_derivatives() {
4585 let eta = 800.0;
4586 let jet = component_inverse_link_jet(LinkComponent::CLogLog, eta);
4587 assert_eq!(jet.mu, 1.0);
4588 assert!(
4589 jet.d1 == 0.0,
4590 "for mu(eta)=1-exp(-exp(eta)), dmu/deta = exp(eta-exp(eta)) and should underflow to 0 at eta={eta}; got d1={}",
4591 jet.d1
4592 );
4593 assert!(
4594 jet.d2 == 0.0,
4595 "the saturated cloglog second derivative should also be 0 at eta={eta}; got d2={}",
4596 jet.d2
4597 );
4598 assert!(
4599 jet.d3 == 0.0,
4600 "the saturated cloglog third derivative should also be 0 at eta={eta}; got d3={}",
4601 jet.d3
4602 );
4603
4604 let d4 = inverse_link_pdfthird_derivative_for_inverse_link(
4605 &InverseLink::Standard(StandardLink::CLogLog),
4606 eta,
4607 )
4608 .expect("cloglog d4");
4609 assert!(
4610 d4 == 0.0,
4611 "the saturated cloglog fourth derivative should also be 0 at eta={eta}; got d4={d4}"
4612 );
4613 }
4614
4615 #[test]
4616 fn loglog_large_negative_finite_eta_should_saturate_without_nan_derivatives() {
4617 let eta = -800.0;
4618 let jet = component_inverse_link_jet(LinkComponent::LogLog, eta);
4619 assert_eq!(jet.mu, 0.0);
4620 assert!(
4621 jet.d1 == 0.0,
4622 "for mu(eta)=exp(-exp(-eta)), dmu/deta = exp(-eta-exp(-eta)) and should underflow to 0 at eta={eta}; got d1={}",
4623 jet.d1
4624 );
4625 assert!(
4626 jet.d2 == 0.0,
4627 "the saturated loglog second derivative should also be 0 at eta={eta}; got d2={}",
4628 jet.d2
4629 );
4630 assert!(
4631 jet.d3 == 0.0,
4632 "the saturated loglog third derivative should also be 0 at eta={eta}; got d3={}",
4633 jet.d3
4634 );
4635
4636 let d4 = inverse_link_pdfthird_derivative_for_inverse_link(
4637 &InverseLink::Mixture(
4638 state_fromspec(&MixtureLinkSpec {
4639 components: vec![LinkComponent::LogLog, LinkComponent::Probit],
4640 initial_rho: Array1::from_vec(vec![12.0]),
4641 })
4642 .expect("mixture state"),
4643 ),
4644 eta,
4645 )
4646 .expect("loglog mixture d4");
4647 assert!(
4648 d4.is_finite(),
4649 "even a nearly pure loglog mixture should not produce NaN fourth derivatives at eta={eta}; got d4={d4}"
4650 );
4651 }
4652
4653 #[test]
4654 fn logit_tail_derivatives_should_match_stable_closed_forms() {
4655 let eta = 50.0_f64;
4656 let z = (-eta).exp();
4657 let denom = 1.0_f64 + z;
4658 let stable_d1 = z / denom.powi(2);
4659 let stable_d2 = z * (z - 1.0) / denom.powi(3);
4660 let stable_d3 = z * (z * z - 4.0 * z + 1.0) / denom.powi(4);
4661 let stable_d4 = z * (z * z * z - 11.0 * z * z + 11.0 * z - 1.0) / denom.powi(5);
4662 let stable_d5 =
4663 z * (z * z * z * z - 26.0 * z * z * z + 66.0 * z * z - 26.0 * z + 1.0) / denom.powi(6);
4664
4665 assert!(stable_d1 > 0.0);
4666 assert!(stable_d2 < 0.0);
4667 assert!(stable_d3 > 0.0);
4668 assert!(stable_d4 < 0.0);
4669 assert!(stable_d5 > 0.0);
4670
4671 let jet = component_inverse_link_jet(LinkComponent::Logit, eta);
4672 assert!(
4673 (jet.d1 - stable_d1).abs() < 1e-30,
4674 "logit d1 should equal the stable tail formula z/(1+z)^2 at eta={eta}; got {} vs {}",
4675 jet.d1,
4676 stable_d1
4677 );
4678 assert!(
4679 (jet.d2 - stable_d2).abs() < 1e-30,
4680 "logit d2 should equal the stable tail formula z(z-1)/(1+z)^3 at eta={eta}; got {} vs {}",
4681 jet.d2,
4682 stable_d2
4683 );
4684 assert!(
4685 (jet.d3 - stable_d3).abs() < 1e-30,
4686 "logit d3 should equal the stable tail formula z(z^2-4z+1)/(1+z)^4 at eta={eta}; got {} vs {}",
4687 jet.d3,
4688 stable_d3
4689 );
4690
4691 let d4 = inverse_link_pdfthird_derivative_for_inverse_link(
4692 &InverseLink::Standard(StandardLink::Logit),
4693 eta,
4694 )
4695 .expect("logit d4");
4696 assert!(
4697 (d4 - stable_d4).abs() < 1e-30,
4698 "logit d4 should equal the stable tail formula z(z^3-11z^2+11z-1)/(1+z)^5 at eta={eta}; got {} vs {}",
4699 d4,
4700 stable_d4
4701 );
4702
4703 let d5 = inverse_link_pdffourth_derivative_for_inverse_link(
4704 &InverseLink::Standard(StandardLink::Logit),
4705 eta,
4706 )
4707 .expect("logit d5");
4708 assert!(
4709 (d5 - stable_d5).abs() < 1e-30,
4710 "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 {}",
4711 d5,
4712 stable_d5
4713 );
4714 }
4715
4716 #[test]
4717 fn cloglog_negative_tail_value_should_match_expm1_form() {
4718 let eta = -50.0_f64;
4719 let t = eta.exp();
4720 let stable_mu = -(-t).exp_m1();
4721 assert!(stable_mu > 0.0);
4722
4723 let jet = component_inverse_link_jet(LinkComponent::CLogLog, eta);
4724 assert!(
4725 (jet.mu - stable_mu).abs() < 1e-30,
4726 "cloglog mu should equal -expm1(-exp(eta)) in the negative tail at eta={eta}; got {} vs {}",
4727 jet.mu,
4728 stable_mu
4729 );
4730 }
4731
4732 #[test]
4733 fn non_logit_probit_fisher_weight_jets_match_finite_differences() {
4734 fn rel_err(a: f64, b: f64) -> f64 {
4735 (a - b).abs() / a.abs().max(b.abs()).max(1.0e-8)
4736 }
4737
4738 let cases = [
4739 (LinkComponent::CLogLog, [-3.0_f64, -0.5, 0.4, 1.5]),
4740 (LinkComponent::LogLog, [-1.5_f64, -0.4, 0.5, 3.0]),
4741 (LinkComponent::Cauchit, [-3.0_f64, -0.7, 0.6, 3.0]),
4742 ];
4743 for (component, etas) in cases {
4744 for eta in etas {
4745 let (w, w1, w2, w3, w4) = component_fisher_weight_jet5(component, eta);
4746 let jet = component_inverse_link_jet(component, eta);
4747 let expected = jet.d1 * jet.d1 / (jet.mu * (1.0 - jet.mu));
4748 assert!(
4749 rel_err(w, expected) < 1.0e-12,
4750 "{component:?} Fisher weight mismatch at eta={eta}: got {w}, expected {expected}"
4751 );
4752
4753 let h = 1.0e-4;
4754 let fd1 = (component_fisher_weight_jet5(component, eta + h).0
4755 - component_fisher_weight_jet5(component, eta - h).0)
4756 / (2.0 * h);
4757 let fd2 = (component_fisher_weight_jet5(component, eta + h).1
4758 - component_fisher_weight_jet5(component, eta - h).1)
4759 / (2.0 * h);
4760 let fd3 = (component_fisher_weight_jet5(component, eta + h).2
4761 - component_fisher_weight_jet5(component, eta - h).2)
4762 / (2.0 * h);
4763 let fd4 = (component_fisher_weight_jet5(component, eta + h).3
4764 - component_fisher_weight_jet5(component, eta - h).3)
4765 / (2.0 * h);
4766
4767 assert!(
4768 rel_err(w1, fd1) < 1.0e-5,
4769 "{component:?} W' mismatch at eta={eta}: {w1} vs {fd1}"
4770 );
4771 assert!(
4772 rel_err(w2, fd2) < 1.0e-5,
4773 "{component:?} W'' mismatch at eta={eta}: {w2} vs {fd2}"
4774 );
4775 assert!(
4776 rel_err(w3, fd3) < 5.0e-5,
4777 "{component:?} W''' mismatch at eta={eta}: {w3} vs {fd3}"
4778 );
4779 assert!(
4780 rel_err(w4, fd4) < 5.0e-4,
4781 "{component:?} W'''' mismatch at eta={eta}: {w4} vs {fd4}"
4782 );
4783 }
4784 }
4785 }
4786
4787 #[test]
4788 fn mixture_fisher_weight_jet_covers_loglog_and_cauchit_components() {
4789 let state = state_fromspec(&MixtureLinkSpec {
4790 components: vec![
4791 LinkComponent::CLogLog,
4792 LinkComponent::LogLog,
4793 LinkComponent::Cauchit,
4794 ],
4795 initial_rho: Array1::from_vec(vec![0.3, -0.2]),
4796 })
4797 .expect("mixture state");
4798 let link = InverseLink::Mixture(state);
4799 assert!(
4800 link.has_fisher_weight_jet(),
4801 "anchored mixtures with loglog/cauchit components must remain eligible for Firth"
4802 );
4803 assert!(
4804 LikelihoodSpec::new(ResponseFamily::Binomial, link.clone()).supports_firth(),
4805 "Firth support should use the mixture inverse-link Fisher jet, not standalone LinkFunction coverage"
4806 );
4807
4808 for eta in [-2.0_f64, -0.25, 0.75, 2.5] {
4809 let (w, w1, w2, w3, w4) =
4810 fisher_weight_jet5_for_inverse_link(&link, eta).expect("mixture Fisher jet");
4811 for value in [w, w1, w2, w3, w4] {
4812 assert!(
4813 value.is_finite(),
4814 "mixture Fisher weight jet should be finite at eta={eta}; got {value}"
4815 );
4816 }
4817 assert!(
4818 w > 0.0,
4819 "mixture Fisher working weight should be positive away from saturated tails at eta={eta}; got {w}"
4820 );
4821 }
4822 }
4823
4824 #[test]
4825 fn loglog_fifth_derivative_should_match_closed_form_sign() {
4826 let eta = 0.0_f64;
4827 let r = (-eta).exp();
4828 let expected =
4829 (-r).exp() * (r - 15.0 * r * r + 25.0 * r.powi(3) - 10.0 * r.powi(4) + r.powi(5));
4830 let d5 = component_inverse_link_pdffourth_derivative(LinkComponent::LogLog, eta);
4831 assert!(
4832 (d5 - expected).abs() < 1e-15,
4833 "loglog d5 should equal exp(-r) * (r - 15r^2 + 25r^3 - 10r^4 + r^5) at eta={eta}; got {d5} vs {expected}"
4834 );
4835 assert!(d5 > 0.0, "loglog d5 should be positive at eta=0; got {d5}");
4836 }
4837}