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
33#[inline]
34fn latent_cloglog_quadctx() -> &'static crate::quadrature::QuadratureContext {
35 static QUADCTX: OnceLock<crate::quadrature::QuadratureContext> = OnceLock::new();
36 QUADCTX.get_or_init(crate::quadrature::QuadratureContext::new)
37}
38
39#[inline]
40fn latent_cloglog_point_jet(
41 state: &LatentCLogLogState,
42 eta: f64,
43) -> Result<InverseLinkJet, EstimationError> {
44 let jet = latent_cloglog_jet5(latent_cloglog_quadctx(), eta, state.latent_sd)?;
45 Ok(InverseLinkJet {
46 mu: jet.mean,
47 d1: jet.d1,
48 d2: jet.d2,
49 d3: jet.d3,
50 })
51}
52
53#[inline]
54pub(crate) fn log_link_solver_exp(eta: f64) -> Result<f64, EstimationError> {
55 if !(LOG_LINK_SOLVER_ETA_MIN..=LOG_LINK_SOLVER_ETA_MAX).contains(&eta) {
56 return Err(EstimationError::InverseLinkDomainViolation {
57 link: "standard log inverse link",
58 eta,
59 lower: LOG_LINK_SOLVER_ETA_MIN,
60 upper: LOG_LINK_SOLVER_ETA_MAX,
61 });
62 }
63 Ok(eta.exp())
64}
65
66#[inline]
67fn finite_inverse_link_eta(link: &'static str, eta: f64) -> Result<f64, EstimationError> {
68 if !eta.is_finite() {
69 return Err(EstimationError::InverseLinkDomainViolation {
70 link,
71 eta,
72 lower: -f64::MAX,
73 upper: f64::MAX,
74 });
75 }
76 Ok(eta)
77}
78
79#[derive(Clone, Copy)]
80struct AsinhJet5 {
81 value: f64,
82 d1: f64,
83 d2: f64,
84 d3: f64,
85 d4: f64,
86 d5: f64,
87}
88
89#[inline]
92fn asinh_jet5(eta: f64) -> AsinhJet5 {
93 let q = eta.hypot(1.0);
94 let inv_q = q.recip();
95 let inv_q2 = inv_q * inv_q;
96 let inv_q3 = inv_q2 * inv_q;
97 let inv_q4 = inv_q2 * inv_q2;
98 let inv_q5 = inv_q4 * inv_q;
99 let t = eta / q;
100 let t2 = t * t;
101 let t4 = t2 * t2;
102 let value = {
110 let v = eta.asinh();
111 if v.is_finite() {
112 v
113 } else {
114 eta.signum() * (eta.abs().ln() + std::f64::consts::LN_2)
115 }
116 };
117 AsinhJet5 {
118 value,
119 d1: inv_q,
120 d2: -t * inv_q2,
121 d3: (2.0 * t2 - inv_q2) * inv_q3,
122 d4: t * (9.0 * inv_q2 - 6.0 * t2) * inv_q4,
123 d5: (9.0 * inv_q4 - 72.0 * t2 * inv_q2 + 24.0 * t4) * inv_q5,
124 }
125}
126
127#[derive(Clone, Copy, Debug, PartialEq)]
128pub struct InverseLinkJet {
129 pub mu: f64,
130 pub d1: f64,
131 pub d2: f64,
132 pub d3: f64,
133}
134
135#[derive(Clone, Copy, Debug, PartialEq)]
136pub struct LogitJet5 {
137 pub mu: f64,
138 pub d1: f64,
139 pub d2: f64,
140 pub d3: f64,
141 pub d4: f64,
142 pub d5: f64,
143}
144
145#[inline]
146fn canonicalzero(v: f64) -> f64 {
147 if v == 0.0 { 0.0 } else { v }
152}
153
154#[inline]
155fn canonicalize_jet(mut jet: InverseLinkJet) -> InverseLinkJet {
156 jet.d1 = canonicalzero(jet.d1);
157 jet.d2 = canonicalzero(jet.d2);
158 jet.d3 = canonicalzero(jet.d3);
159 jet
160}
161
162#[inline]
163pub fn logit_inverse_link_jet5(eta: f64) -> LogitJet5 {
164 if eta.is_nan() {
165 return LogitJet5 {
166 mu: f64::NAN,
167 d1: f64::NAN,
168 d2: f64::NAN,
169 d3: f64::NAN,
170 d4: f64::NAN,
171 d5: f64::NAN,
172 };
173 }
174 if eta == f64::INFINITY {
175 return LogitJet5 {
176 mu: 1.0,
177 d1: 0.0,
178 d2: 0.0,
179 d3: 0.0,
180 d4: 0.0,
181 d5: 0.0,
182 };
183 }
184 if eta == f64::NEG_INFINITY {
185 return LogitJet5 {
186 mu: 0.0,
187 d1: 0.0,
188 d2: 0.0,
189 d3: 0.0,
190 d4: 0.0,
191 d5: 0.0,
192 };
193 }
194
195 let jet = if eta >= 0.0 {
196 let z = (-eta).exp();
197 let opz = 1.0 + z;
198 let opz2 = opz * opz;
199 let opz3 = opz2 * opz;
200 let opz4 = opz3 * opz;
201 let opz5 = opz4 * opz;
202 let opz6 = opz5 * opz;
203 let z2 = z * z;
204 let z3 = z2 * z;
205 let z4 = z3 * z;
206 LogitJet5 {
207 mu: 1.0 / opz,
208 d1: z / opz2,
209 d2: z * (z - 1.0) / opz3,
210 d3: z * (z2 - 4.0 * z + 1.0) / opz4,
211 d4: z * (z3 - 11.0 * z2 + 11.0 * z - 1.0) / opz5,
212 d5: z * (z4 - 26.0 * z3 + 66.0 * z2 - 26.0 * z + 1.0) / opz6,
213 }
214 } else {
215 let z = eta.exp();
216 let opz = 1.0 + z;
217 let opz2 = opz * opz;
218 let opz3 = opz2 * opz;
219 let opz4 = opz3 * opz;
220 let opz5 = opz4 * opz;
221 let opz6 = opz5 * opz;
222 let z2 = z * z;
223 let z3 = z2 * z;
224 let z4 = z3 * z;
225 LogitJet5 {
226 mu: z / opz,
227 d1: z / opz2,
228 d2: z * (1.0 - z) / opz3,
229 d3: z * (1.0 - 4.0 * z + z2) / opz4,
230 d4: z * (1.0 - 11.0 * z + 11.0 * z2 - z3) / opz5,
231 d5: z * (1.0 - 26.0 * z + 66.0 * z2 - 26.0 * z3 + z4) / opz6,
232 }
233 };
234 LogitJet5 {
235 mu: jet.mu,
236 d1: canonicalzero(jet.d1),
237 d2: canonicalzero(jet.d2),
238 d3: canonicalzero(jet.d3),
239 d4: canonicalzero(jet.d4),
240 d5: canonicalzero(jet.d5),
241 }
242}
243
244#[inline]
245fn probit_jet(eta: f64) -> InverseLinkJet {
246 if eta.is_nan() {
257 return InverseLinkJet {
258 mu: f64::NAN,
259 d1: f64::NAN,
260 d2: f64::NAN,
261 d3: f64::NAN,
262 };
263 }
264 if eta == f64::INFINITY {
265 return InverseLinkJet {
266 mu: 1.0,
267 d1: 0.0,
268 d2: 0.0,
269 d3: 0.0,
270 };
271 }
272 if eta == f64::NEG_INFINITY {
273 return InverseLinkJet {
274 mu: 0.0,
275 d1: 0.0,
276 d2: 0.0,
277 d3: 0.0,
278 };
279 }
280 let x = eta;
281 let phi = normal_pdf(x);
282 if phi == 0.0 {
283 return InverseLinkJet {
284 mu: normal_cdf(x),
285 d1: 0.0,
286 d2: 0.0,
287 d3: 0.0,
288 };
289 }
290 InverseLinkJet {
291 mu: normal_cdf(x),
292 d1: phi,
293 d2: -x * phi,
294 d3: (x * x - 1.0) * phi,
295 }
296}
297
298#[inline]
299fn probit_pdfthird_derivative(eta: f64) -> f64 {
300 if eta.is_nan() {
304 return f64::NAN;
305 }
306 if !eta.is_finite() {
307 return 0.0;
308 }
309 let x = eta;
310 let phi = normal_pdf(x);
311 if phi == 0.0 {
312 return 0.0;
313 }
314 canonicalzero(-(x * x * x - 3.0 * x) * phi)
315}
316
317#[inline]
318fn probit_pdffourth_derivative(eta: f64) -> f64 {
319 if eta.is_nan() {
321 return f64::NAN;
322 }
323 if !eta.is_finite() {
324 return 0.0;
325 }
326 let x = eta;
327 let phi = normal_pdf(x);
328 if phi == 0.0 {
329 return 0.0;
330 }
331 canonicalzero((x * x * x * x - 6.0 * x * x + 3.0) * phi)
332}
333
334#[inline]
337fn taylor5_mul(a: &[f64; 5], b: &[f64; 5]) -> [f64; 5] {
338 let mut c = [0.0_f64; 5];
339 for i in 0..5 {
340 let ai = a[i];
341 if ai == 0.0 {
342 continue;
343 }
344 for j in 0..(5 - i) {
345 c[i + j] += ai * b[j];
346 }
347 }
348 c
349}
350
351#[inline]
353fn taylor5_inv(a: &[f64; 5]) -> [f64; 5] {
354 let mut b = [0.0_f64; 5];
355 b[0] = 1.0 / a[0];
356 for k in 1..5 {
357 let mut s = 0.0_f64;
358 for j in 1..=k {
359 s += a[j] * b[k - j];
360 }
361 b[k] = -s * b[0];
362 }
363 b
364}
365
366pub(crate) fn fisher_weight_jet5(link: StandardLink, eta: f64) -> (f64, f64, f64, f64, f64) {
382 match link {
383 StandardLink::Logit => {
384 let jet = logit_inverse_link_jet5(eta);
385 (jet.d1, jet.d2, jet.d3, jet.d4, jet.d5)
386 }
387 StandardLink::Probit => probit_fisher_weight_jet5(eta),
388 StandardLink::CLogLog => component_fisher_weight_jet5(LinkComponent::CLogLog, eta),
389 StandardLink::LogLog => component_fisher_weight_jet5(LinkComponent::LogLog, eta),
390 StandardLink::Cauchit => component_fisher_weight_jet5(LinkComponent::Cauchit, eta),
391 StandardLink::Identity | StandardLink::Log => (0.0, 0.0, 0.0, 0.0, 0.0),
392 }
393}
394
395pub(crate) fn fisher_weight_jet5_for_inverse_link(
396 link: &InverseLink,
397 eta: f64,
398) -> Result<(f64, f64, f64, f64, f64), EstimationError> {
399 match link {
400 InverseLink::Standard(link) => Ok(fisher_weight_jet5(*link, eta)),
401 InverseLink::LatentCLogLog(_)
402 | InverseLink::Sas(_)
403 | InverseLink::BetaLogistic(_)
404 | InverseLink::Mixture(_) => {
405 let jet = link.jet(eta)?;
406 let d4 = inverse_link_pdfthird_derivative_for_inverse_link(link, eta)?;
407 let d5 = inverse_link_pdffourth_derivative_for_inverse_link(link, eta)?;
408 Ok(fisher_weight_jet5_from_inverse_link_derivatives(
409 jet.mu, jet.d1, jet.d2, jet.d3, d4, d5,
410 ))
411 }
412 }
413}
414
415#[inline]
416fn component_fisher_weight_jet5(component: LinkComponent, eta: f64) -> (f64, f64, f64, f64, f64) {
417 let jet = component_inverse_link_jet(component, eta);
418 let d4 = component_inverse_link_pdfthird_derivative(component, eta);
419 let d5 = component_inverse_link_pdffourth_derivative(component, eta);
420 fisher_weight_jet5_from_inverse_link_derivatives(jet.mu, jet.d1, jet.d2, jet.d3, d4, d5)
421}
422
423#[inline]
424fn fisher_weight_jet5_from_inverse_link_derivatives(
425 mu: f64,
426 d1: f64,
427 d2: f64,
428 d3: f64,
429 d4: f64,
430 d5: f64,
431) -> (f64, f64, f64, f64, f64) {
432 if [mu, d1, d2, d3, d4, d5].iter().any(|v| v.is_nan()) {
433 return (f64::NAN, f64::NAN, f64::NAN, f64::NAN, f64::NAN);
434 }
435 let variance = mu * (1.0 - mu);
436 if !(variance > 0.0) || !variance.is_finite() {
437 return (0.0, 0.0, 0.0, 0.0, 0.0);
438 }
439
440 let factorial = [1.0_f64, 1.0, 2.0, 6.0, 24.0];
441 let mu_d = [mu, d1, d2, d3, d4];
442 let one_minus_mu_d = [1.0 - mu, -d1, -d2, -d3, -d4];
443 let dmu_d = [d1, d2, d3, d4, d5];
444 let mut mu_t = [0.0_f64; 5];
445 let mut one_minus_mu_t = [0.0_f64; 5];
446 let mut dmu_t = [0.0_f64; 5];
447 for k in 0..5 {
448 let inv_fact = 1.0 / factorial[k];
449 mu_t[k] = mu_d[k] * inv_fact;
450 one_minus_mu_t[k] = one_minus_mu_d[k] * inv_fact;
451 dmu_t[k] = dmu_d[k] * inv_fact;
452 }
453 let num_t = taylor5_mul(&dmu_t, &dmu_t);
454 let den_t = taylor5_mul(&mu_t, &one_minus_mu_t);
455 if !(den_t[0] > 0.0) || !den_t[0].is_finite() {
456 return (0.0, 0.0, 0.0, 0.0, 0.0);
457 }
458 let w_t = taylor5_mul(&num_t, &taylor5_inv(&den_t));
459 (
460 canonicalzero(w_t[0] * factorial[0]),
461 canonicalzero(w_t[1] * factorial[1]),
462 canonicalzero(w_t[2] * factorial[2]),
463 canonicalzero(w_t[3] * factorial[3]),
464 canonicalzero(w_t[4] * factorial[4]),
465 )
466}
467
468#[inline]
471fn probit_fisher_weight_jet5(eta: f64) -> (f64, f64, f64, f64, f64) {
472 if eta.is_nan() {
473 return (f64::NAN, f64::NAN, f64::NAN, f64::NAN, f64::NAN);
474 }
475 if !eta.is_finite() {
476 return (0.0, 0.0, 0.0, 0.0, 0.0);
477 }
478 let x = eta;
479 let p = normal_cdf(x);
480 let q = normal_cdf(-x);
484 let phi = normal_pdf(x);
485 if !(p > 0.0) || !(q > 0.0) || p * q <= 0.0 {
488 return (0.0, 0.0, 0.0, 0.0, 0.0);
489 }
490 let phi1 = -x * phi;
492 let phi2 = (x * x - 1.0) * phi;
493 let phi3 = -(x * x * x - 3.0 * x) * phi;
494 let phi4 = (x * x * x * x - 6.0 * x * x + 3.0) * phi;
495 let f_d = [phi, phi1, phi2, phi3, phi4];
498 let p_d = [p, phi, phi1, phi2, phi3];
499 let q_d = [q, -phi, -phi1, -phi2, -phi3];
500 let factorial = [1.0_f64, 1.0, 2.0, 6.0, 24.0];
502 let mut f_t = [0.0_f64; 5];
503 let mut p_t = [0.0_f64; 5];
504 let mut q_t = [0.0_f64; 5];
505 for k in 0..5 {
506 let inv_fact = 1.0 / factorial[k];
507 f_t[k] = f_d[k] * inv_fact;
508 p_t[k] = p_d[k] * inv_fact;
509 q_t[k] = q_d[k] * inv_fact;
510 }
511 let num_t = taylor5_mul(&f_t, &f_t);
512 let den_t = taylor5_mul(&p_t, &q_t);
513 let w_t = taylor5_mul(&num_t, &taylor5_inv(&den_t));
514 (
516 canonicalzero(w_t[0] * factorial[0]),
517 canonicalzero(w_t[1] * factorial[1]),
518 canonicalzero(w_t[2] * factorial[2]),
519 canonicalzero(w_t[3] * factorial[3]),
520 canonicalzero(w_t[4] * factorial[4]),
521 )
522}
523
524#[inline]
525fn chain_inverse_link_jet(base: InverseLinkJet, z1: f64, z2: f64, z3: f64) -> InverseLinkJet {
526 InverseLinkJet {
527 mu: base.mu,
528 d1: base.d1 * z1,
529 d2: base.d2 * z1 * z1 + base.d1 * z2,
530 d3: base.d3 * z1 * z1 * z1 + 3.0 * base.d2 * z1 * z2 + base.d1 * z3,
531 }
532}
533
534#[inline]
535fn component_inverse_link_pdfthird_derivative(component: LinkComponent, eta: f64) -> f64 {
536 match component {
537 LinkComponent::Probit => probit_pdfthird_derivative(eta),
538 LinkComponent::Logit => logit_inverse_link_jet5(eta).d4,
539 LinkComponent::CLogLog => {
540 if eta.is_nan() {
548 return f64::NAN;
549 }
550 if !eta.is_finite() {
551 return 0.0;
552 }
553 let t = eta.exp();
554 canonicalzero(stable_nonnegative_poly_times_exp_neg(
555 t,
556 &[0.0, 1.0, -7.0, 6.0, -1.0],
557 ))
558 }
559 LinkComponent::LogLog => {
560 if eta.is_nan() {
567 return f64::NAN;
568 }
569 if !eta.is_finite() {
570 return 0.0;
571 }
572 let r = (-eta).exp();
573 canonicalzero(stable_nonnegative_poly_times_exp_neg(
574 r,
575 &[0.0, -1.0, 7.0, -6.0, 1.0],
576 ))
577 }
578 LinkComponent::Cauchit => {
579 if eta.is_nan() {
587 return f64::NAN;
588 }
589 if !eta.is_finite() {
590 return 0.0;
591 }
592 let denom = 1.0 + eta * eta;
593 24.0 * eta * (1.0 - eta * eta) / (std::f64::consts::PI * denom.powi(4))
594 }
595 }
596}
597
598#[inline]
601fn component_inverse_link_pdffourth_derivative(component: LinkComponent, eta: f64) -> f64 {
602 match component {
603 LinkComponent::Probit => probit_pdffourth_derivative(eta),
604 LinkComponent::Logit => logit_inverse_link_jet5(eta).d5,
605 LinkComponent::CLogLog => {
606 if eta.is_nan() {
611 return f64::NAN;
612 }
613 if !eta.is_finite() {
614 return 0.0;
615 }
616 let t = eta.exp();
617 canonicalzero(stable_nonnegative_poly_times_exp_neg(
618 t,
619 &[0.0, 1.0, -15.0, 25.0, -10.0, 1.0],
620 ))
621 }
622 LinkComponent::LogLog => {
623 if eta.is_nan() {
628 return f64::NAN;
629 }
630 if !eta.is_finite() {
631 return 0.0;
632 }
633 let r = (-eta).exp();
634 canonicalzero(stable_nonnegative_poly_times_exp_neg(
635 r,
636 &[0.0, 1.0, -15.0, 25.0, -10.0, 1.0],
637 ))
638 }
639 LinkComponent::Cauchit => {
640 if eta.is_nan() {
642 return f64::NAN;
643 }
644 if !eta.is_finite() {
645 return 0.0;
646 }
647 let e2 = eta * eta;
648 let denom = 1.0 + e2;
649 24.0 * (1.0 - 10.0 * e2 + 5.0 * e2 * e2) / (std::f64::consts::PI * denom.powi(5))
650 }
651 }
652}
653
654#[derive(Clone, Debug, PartialEq)]
655pub struct MixtureJetWithRhoPartials {
656 pub jet: InverseLinkJet,
657 pub djet_drho: Vec<InverseLinkJet>,
660 pub d2mu_drho2: Array2<f64>,
662 pub d2d1_drho2: Array2<f64>,
664}
665
666#[derive(Clone, Debug, PartialEq)]
667pub struct SasJetWithParamPartials {
668 pub jet: InverseLinkJet,
669 pub djet_depsilon: InverseLinkJet,
670 pub djet_dlog_delta: InverseLinkJet,
671 pub d2mu_dparams2: Array2<f64>,
675 pub d2d1_dparams2: Array2<f64>,
677}
678
679#[derive(Clone, Debug, PartialEq)]
680pub enum LinkParamPartials {
681 Mixture(MixtureJetWithRhoPartials),
682 Sas(SasJetWithParamPartials),
683}
684
685pub trait InverseLinkKernel {
691 fn jet(&self, eta: f64) -> Result<InverseLinkJet, EstimationError>;
692
693 fn param_partials(&self, eta: f64) -> Result<Option<LinkParamPartials>, EstimationError> {
694 assert!(eta.is_finite(), "eta must be finite");
695 Ok(None)
696 }
697}
698
699#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
700pub struct ProbitLinkKernel;
701
702#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
703pub struct LogitLinkKernel;
704
705#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
706pub struct CLogLogLinkKernel;
707
708#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
709pub struct LogLogLinkKernel;
710
711#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
712pub struct CauchitLinkKernel;
713
714pub fn sas_link_state_from_raw(
722 raw_epsilon: f64,
723 raw_log_delta: f64,
724) -> Result<SasLinkState, String> {
725 if !raw_epsilon.is_finite() || !raw_log_delta.is_finite() {
726 return Err("SAS link parameters must be finite".to_string());
727 }
728 Ok(SasLinkState {
729 epsilon: raw_epsilon,
730 log_delta: raw_log_delta,
731 delta: sas_delta_from_raw_log_delta(raw_log_delta),
732 })
733}
734
735pub fn state_from_sasspec(spec: SasLinkSpec) -> Result<SasLinkState, String> {
736 sas_link_state_from_raw(spec.initial_epsilon, spec.initial_log_delta)
737}
738
739pub fn state_from_beta_logisticspec(spec: SasLinkSpec) -> Result<SasLinkState, String> {
740 if !spec.initial_epsilon.is_finite() || !spec.initial_log_delta.is_finite() {
741 return Err("Beta-Logistic link parameters must be finite".to_string());
742 }
743 let log_shape_center = spec.initial_log_delta;
749 Ok(SasLinkState {
750 epsilon: spec.initial_epsilon,
751 log_delta: log_shape_center,
752 delta: sas_delta_from_raw_log_delta(log_shape_center),
753 })
754}
755
756const SPLICE_INTERIOR_FRAC: f64 = 0.8;
760
761#[derive(Clone, Copy, Debug)]
763struct SmoothBoundJet {
764 g: f64,
765 d1: f64,
766 d2: f64,
767 d3: f64,
768 d4: f64,
769 d5: f64,
770}
771
772#[inline]
801fn smooth_bound_jet(value: f64, bound: f64) -> SmoothBoundJet {
802 let b = bound.max(f64::EPSILON);
803 let a = SPLICE_INTERIOR_FRAC * b; let l = 2.0 * (b - a); let ax = value.abs();
806 if ax <= a {
807 return SmoothBoundJet {
809 g: value,
810 d1: 1.0,
811 d2: 0.0,
812 d3: 0.0,
813 d4: 0.0,
814 d5: 0.0,
815 };
816 }
817 let sign = if value < 0.0 { -1.0 } else { 1.0 };
818 if ax >= a + l {
819 return SmoothBoundJet {
821 g: sign * b,
822 d1: 0.0,
823 d2: 0.0,
824 d3: 0.0,
825 d4: 0.0,
826 d5: 0.0,
827 };
828 }
829 let w = (ax - a) / l;
832 let w2 = w * w;
833 let w3 = w2 * w;
834 let w4 = w3 * w;
835 let w5 = w4 * w;
836 let w6 = w5 * w;
837 let w7 = w6 * w;
838 let w8 = w7 * w;
839 let w9 = w8 * w;
840 let w10 = w9 * w;
841 let s = 70.0 * w9 - 315.0 * w8 + 540.0 * w7 - 420.0 * w6 + 126.0 * w5;
842 let sp = 630.0 * w8 - 2520.0 * w7 + 3780.0 * w6 - 2520.0 * w5 + 630.0 * w4;
843 let spp = 5040.0 * w7 - 17640.0 * w6 + 22680.0 * w5 - 12600.0 * w4 + 2520.0 * w3;
844 let sppp = 35280.0 * w6 - 105840.0 * w5 + 113400.0 * w4 - 50400.0 * w3 + 7560.0 * w2;
845 let spppp = 211680.0 * w5 - 529200.0 * w4 + 453600.0 * w3 - 151200.0 * w2 + 15120.0 * w;
846 let iw = w - 7.0 * w10 + 35.0 * w9 - 67.5 * w8 + 60.0 * w7 - 21.0 * w6;
848 let g0 = a + l * iw;
849 let l2 = l * l;
852 SmoothBoundJet {
853 g: sign * g0,
854 d1: 1.0 - s,
855 d2: sign * (-sp / l),
856 d3: -spp / l2,
857 d4: sign * (-sppp / (l2 * l)),
858 d5: -spppp / (l2 * l2),
859 }
860}
861
862#[inline]
863fn sas_effective_log_delta(raw_log_delta: f64) -> (f64, f64) {
864 let sb = smooth_bound_jet(raw_log_delta, SAS_LOG_DELTA_BOUND);
865 (sb.g, sb.d1)
866}
867
868#[inline]
869fn sas_delta_from_raw_log_delta(raw_log_delta: f64) -> f64 {
870 let (ld_eff, _) = sas_effective_log_delta(raw_log_delta);
871 ld_eff.exp()
872}
873
874pub fn validate_mixturespec(spec: &MixtureLinkSpec) -> Result<(), String> {
875 if spec.components.is_empty() {
876 return Err("mixture link requires at least 1 component".to_string());
877 }
878 if spec.initial_rho.len() + 1 != spec.components.len() {
879 return Err(format!(
880 "mixture link rho length mismatch: expected {}, got {}",
881 spec.components.len() - 1,
882 spec.initial_rho.len()
883 ));
884 }
885 for i in 0..spec.components.len() {
886 for j in (i + 1)..spec.components.len() {
887 if spec.components[i] == spec.components[j] {
888 return Err("mixture link components must be unique".to_string());
889 }
890 }
891 }
892 let has_anchor = spec.components.iter().any(|component| {
907 matches!(
908 component,
909 LinkComponent::Logit | LinkComponent::Probit | LinkComponent::CLogLog
910 )
911 });
912 if !has_anchor && spec.components.len() > 1 {
913 let unsupported: Vec<&str> = spec
914 .components
915 .iter()
916 .map(|component| component.name())
917 .collect();
918 return Err(format!(
919 "mixture link components {{{}}} are unsupported: at least one component \
920 must map to a LinkFunction variant (logit/probit/cloglog) so the mixture's \
921 projected LinkFunction is well defined; cauchit and loglog have no \
922 LinkFunction representative",
923 unsupported.join(", ")
924 ));
925 }
926 Ok(())
927}
928
929pub fn softmax_last_fixedzero(rho: &Array1<f64>) -> Array1<f64> {
930 let k = rho.len() + 1;
931 let mut logits = Vec::with_capacity(k);
932 let mut maxv = 0.0_f64;
933 for &v in rho {
934 maxv = maxv.max(v);
935 logits.push(v);
936 }
937 maxv = maxv.max(0.0);
938 logits.push(0.0);
939
940 let mut sum = 0.0_f64;
941 let mut exps = vec![0.0_f64; k];
942 for i in 0..k {
943 let e = (logits[i] - maxv).exp();
944 exps[i] = e;
945 sum += e;
946 }
947 if !sum.is_finite() || sum <= 0.0 {
948 return Array1::from_elem(k, 1.0 / k as f64);
949 }
950 let inv = 1.0 / sum;
951 Array1::from_iter(exps.into_iter().map(|v| v * inv))
952}
953
954pub fn softmaxwith_jacobian_last_fixedzero(
957 rho: &Array1<f64>,
958) -> (Array1<f64>, ndarray::Array2<f64>) {
959 let pi = softmax_last_fixedzero(rho);
960 let k = pi.len();
961 let m = k.saturating_sub(1);
962 let mut jac = ndarray::Array2::<f64>::zeros((k, m));
963 for j in 0..m {
964 let pi_j = pi[j];
965 for kk in 0..k {
966 let delta = if kk == j { 1.0 } else { 0.0 };
967 jac[[kk, j]] = pi[kk] * (delta - pi_j);
968 }
969 }
970 (pi, jac)
971}
972
973pub fn state_fromspec(spec: &MixtureLinkSpec) -> Result<MixtureLinkState, String> {
974 validate_mixturespec(spec)?;
975 let pi = softmax_last_fixedzero(&spec.initial_rho);
976 Ok(MixtureLinkState {
977 components: spec.components.clone(),
978 rho: spec.initial_rho.clone(),
979 pi,
980 })
981}
982
983#[inline]
984pub fn component_inverse_link_jet(component: LinkComponent, eta: f64) -> InverseLinkJet {
985 canonicalize_jet(match component {
986 LinkComponent::Logit => {
987 let jet = logit_inverse_link_jet5(eta);
988 InverseLinkJet {
989 mu: jet.mu,
990 d1: jet.d1,
991 d2: jet.d2,
992 d3: jet.d3,
993 }
994 }
995 LinkComponent::Probit => probit_jet(eta),
996 LinkComponent::CLogLog => {
997 if eta.is_nan() {
998 return InverseLinkJet {
999 mu: f64::NAN,
1000 d1: f64::NAN,
1001 d2: f64::NAN,
1002 d3: f64::NAN,
1003 };
1004 }
1005 let t = eta.exp();
1006 if !t.is_finite() {
1007 return InverseLinkJet {
1008 mu: 1.0,
1009 d1: 0.0,
1010 d2: 0.0,
1011 d3: 0.0,
1012 };
1013 }
1014 InverseLinkJet {
1015 mu: -(-t).exp_m1(),
1016 d1: stable_nonnegative_poly_times_exp_neg(t, &[0.0, 1.0]),
1017 d2: stable_nonnegative_poly_times_exp_neg(t, &[0.0, 1.0, -1.0]),
1018 d3: stable_nonnegative_poly_times_exp_neg(t, &[0.0, 1.0, -3.0, 1.0]),
1019 }
1020 }
1021 LinkComponent::LogLog => {
1022 if eta.is_nan() {
1023 return InverseLinkJet {
1024 mu: f64::NAN,
1025 d1: f64::NAN,
1026 d2: f64::NAN,
1027 d3: f64::NAN,
1028 };
1029 }
1030 let r = (-eta).exp();
1031 if !r.is_finite() {
1032 return InverseLinkJet {
1033 mu: 0.0,
1034 d1: 0.0,
1035 d2: 0.0,
1036 d3: 0.0,
1037 };
1038 }
1039 InverseLinkJet {
1040 mu: (-r).exp(),
1041 d1: stable_nonnegative_poly_times_exp_neg(r, &[0.0, 1.0]),
1042 d2: stable_nonnegative_poly_times_exp_neg(r, &[0.0, -1.0, 1.0]),
1043 d3: stable_nonnegative_poly_times_exp_neg(r, &[0.0, 1.0, -3.0, 1.0]),
1044 }
1045 }
1046 LinkComponent::Cauchit => {
1047 if eta.is_nan() {
1048 return InverseLinkJet {
1049 mu: f64::NAN,
1050 d1: f64::NAN,
1051 d2: f64::NAN,
1052 d3: f64::NAN,
1053 };
1054 }
1055 let den = 1.0 + eta * eta;
1056 let d1 = if eta.is_finite() {
1057 1.0 / (std::f64::consts::PI * den)
1058 } else {
1059 0.0
1060 };
1061 let d2 = if eta.is_finite() {
1062 -2.0 * eta / (std::f64::consts::PI * den * den)
1063 } else {
1064 0.0
1065 };
1066 let d3 = if eta.is_finite() {
1067 (6.0 * eta * eta - 2.0) / (std::f64::consts::PI * den * den * den)
1068 } else {
1069 0.0
1070 };
1071 InverseLinkJet {
1072 mu: 0.5 + eta.atan() / std::f64::consts::PI,
1073 d1,
1074 d2,
1075 d3,
1076 }
1077 }
1078 })
1079}
1080
1081impl InverseLinkKernel for ProbitLinkKernel {
1082 #[inline]
1083 fn jet(&self, eta: f64) -> Result<InverseLinkJet, EstimationError> {
1084 Ok(component_inverse_link_jet(LinkComponent::Probit, eta))
1085 }
1086}
1087
1088impl InverseLinkKernel for LogitLinkKernel {
1089 #[inline]
1090 fn jet(&self, eta: f64) -> Result<InverseLinkJet, EstimationError> {
1091 Ok(component_inverse_link_jet(LinkComponent::Logit, eta))
1092 }
1093}
1094
1095impl InverseLinkKernel for CLogLogLinkKernel {
1096 #[inline]
1097 fn jet(&self, eta: f64) -> Result<InverseLinkJet, EstimationError> {
1098 Ok(component_inverse_link_jet(LinkComponent::CLogLog, eta))
1099 }
1100}
1101
1102impl InverseLinkKernel for LogLogLinkKernel {
1103 #[inline]
1104 fn jet(&self, eta: f64) -> Result<InverseLinkJet, EstimationError> {
1105 Ok(component_inverse_link_jet(LinkComponent::LogLog, eta))
1106 }
1107}
1108
1109impl InverseLinkKernel for CauchitLinkKernel {
1110 #[inline]
1111 fn jet(&self, eta: f64) -> Result<InverseLinkJet, EstimationError> {
1112 Ok(component_inverse_link_jet(LinkComponent::Cauchit, eta))
1113 }
1114}
1115
1116impl InverseLinkKernel for LinkComponent {
1117 #[inline]
1118 fn jet(&self, eta: f64) -> Result<InverseLinkJet, EstimationError> {
1119 Ok(component_inverse_link_jet(*self, eta))
1120 }
1121}
1122
1123impl InverseLinkKernel for LinkFunction {
1124 fn jet(&self, eta: f64) -> Result<InverseLinkJet, EstimationError> {
1125 match self {
1126 LinkFunction::Logit => LogitLinkKernel.jet(eta),
1127 LinkFunction::Probit => ProbitLinkKernel.jet(eta),
1128 LinkFunction::CLogLog => CLogLogLinkKernel.jet(eta),
1129 LinkFunction::LogLog => LogLogLinkKernel.jet(eta),
1130 LinkFunction::Cauchit => CauchitLinkKernel.jet(eta),
1131 LinkFunction::Identity => Ok(InverseLinkJet {
1132 mu: eta,
1133 d1: 1.0,
1134 d2: 0.0,
1135 d3: 0.0,
1136 }),
1137 LinkFunction::Log => {
1138 let e = log_link_solver_exp(eta)?;
1146 Ok(InverseLinkJet {
1147 mu: e,
1148 d1: e,
1149 d2: e,
1150 d3: e,
1151 })
1152 }
1153 LinkFunction::Sas => Err(EstimationError::InvalidInput(
1154 "LinkFunction::Sas inverse-link requires explicit SAS link state".to_string(),
1155 )),
1156 LinkFunction::BetaLogistic => Err(EstimationError::InvalidInput(
1157 "LinkFunction::BetaLogistic inverse-link requires explicit Beta-Logistic link state"
1158 .to_string(),
1159 )),
1160 }
1161 }
1162}
1163
1164impl InverseLinkKernel for SasLinkState {
1165 fn jet(&self, eta: f64) -> Result<InverseLinkJet, EstimationError> {
1166 sas_inverse_link_jet(eta, self.epsilon, self.log_delta)
1167 }
1168
1169 fn param_partials(&self, eta: f64) -> Result<Option<LinkParamPartials>, EstimationError> {
1170 Ok(Some(LinkParamPartials::Sas(
1171 sas_inverse_link_jetwith_param_partials(eta, self.epsilon, self.log_delta)?,
1172 )))
1173 }
1174}
1175
1176#[derive(Clone, Copy, Debug)]
1177pub struct BetaLogisticKernel {
1178 pub log_shape_center: f64,
1181 pub epsilon: f64,
1182}
1183
1184impl InverseLinkKernel for BetaLogisticKernel {
1185 fn jet(&self, eta: f64) -> Result<InverseLinkJet, EstimationError> {
1186 Ok(beta_logistic_inverse_link_jet(
1187 eta,
1188 self.log_shape_center,
1189 self.epsilon,
1190 ))
1191 }
1192
1193 fn param_partials(&self, eta: f64) -> Result<Option<LinkParamPartials>, EstimationError> {
1194 Ok(Some(LinkParamPartials::Sas(
1195 beta_logistic_inverse_link_jetwith_param_partials(
1196 eta,
1197 self.log_shape_center,
1198 self.epsilon,
1199 ),
1200 )))
1201 }
1202}
1203
1204impl InverseLinkKernel for MixtureLinkState {
1205 fn jet(&self, eta: f64) -> Result<InverseLinkJet, EstimationError> {
1206 Ok(mixture_inverse_link_jet(self, eta))
1207 }
1208
1209 fn param_partials(&self, eta: f64) -> Result<Option<LinkParamPartials>, EstimationError> {
1210 Ok(Some(LinkParamPartials::Mixture(
1211 mixture_inverse_link_jetwith_rho_partials(self, eta),
1212 )))
1213 }
1214}
1215
1216impl InverseLinkKernel for InverseLink {
1217 fn jet(&self, eta: f64) -> Result<InverseLinkJet, EstimationError> {
1218 match self {
1219 InverseLink::Standard(StandardLink::Logit) => LogitLinkKernel.jet(eta),
1220 InverseLink::Standard(StandardLink::Probit) => ProbitLinkKernel.jet(eta),
1221 InverseLink::Standard(StandardLink::CLogLog) => CLogLogLinkKernel.jet(eta),
1222 InverseLink::Standard(StandardLink::LogLog) => LogLogLinkKernel.jet(eta),
1223 InverseLink::Standard(StandardLink::Cauchit) => CauchitLinkKernel.jet(eta),
1224 InverseLink::Standard(StandardLink::Identity) => LinkFunction::Identity.jet(eta),
1225 InverseLink::Standard(StandardLink::Log) => LinkFunction::Log.jet(eta),
1226 InverseLink::LatentCLogLog(state) => latent_cloglog_point_jet(state, eta),
1227 InverseLink::Sas(state) => state.jet(eta),
1228 InverseLink::BetaLogistic(state) => BetaLogisticKernel {
1229 log_shape_center: state.log_delta,
1230 epsilon: state.epsilon,
1231 }
1232 .jet(eta),
1233 InverseLink::Mixture(state) => state.jet(eta),
1234 }
1235 }
1236
1237 fn param_partials(&self, eta: f64) -> Result<Option<LinkParamPartials>, EstimationError> {
1238 match self {
1239 InverseLink::Standard(_) => Ok(None),
1240 InverseLink::LatentCLogLog(_) => Ok(None),
1241 InverseLink::Sas(state) => state.param_partials(eta),
1242 InverseLink::BetaLogistic(state) => BetaLogisticKernel {
1243 log_shape_center: state.log_delta,
1244 epsilon: state.epsilon,
1245 }
1246 .param_partials(eta),
1247 InverseLink::Mixture(state) => state.param_partials(eta),
1248 }
1249 }
1250}
1251
1252pub fn inverse_link_jet_for_inverse_link(
1259 link: &InverseLink,
1260 eta: f64,
1261) -> Result<InverseLinkJet, EstimationError> {
1262 link.jet(eta)
1263}
1264
1265pub fn inverse_link_mu_d1_for_inverse_link(
1276 link: &InverseLink,
1277 eta: f64,
1278) -> Result<(f64, f64), EstimationError> {
1279 match link {
1280 InverseLink::Standard(link_fn) => Ok(link_function_mu_d1(link_fn.as_link_function(), eta)?),
1281 InverseLink::LatentCLogLog(state) => {
1282 let jet = latent_cloglog_point_jet(state, eta)?;
1283 Ok((jet.mu, jet.d1))
1284 }
1285 InverseLink::Sas(state) => sas_inverse_link_mu_d1(eta, state.epsilon, state.log_delta),
1286 InverseLink::BetaLogistic(state) => Ok(beta_logistic_inverse_link_mu_d1(
1287 eta,
1288 state.log_delta,
1289 state.epsilon,
1290 )),
1291 InverseLink::Mixture(state) => Ok(mixture_inverse_link_mu_d1(state, eta)),
1292 }
1293}
1294
1295pub(crate) fn inverse_link_complement_for_inverse_link(
1310 link: &InverseLink,
1311 eta: f64,
1312 mu: f64,
1313) -> f64 {
1314 let raw = match link {
1315 InverseLink::Standard(link_fn) => standard_link_complement(*link_fn, eta, mu),
1316 InverseLink::Sas(state) => sas_link_complement(eta, state.epsilon, state.log_delta, mu),
1317 InverseLink::LatentCLogLog(_) | InverseLink::BetaLogistic(_) | InverseLink::Mixture(_) => {
1320 1.0 - mu
1321 }
1322 };
1323 if raw.is_nan() {
1324 raw
1325 } else {
1326 raw.clamp(0.0, 1.0)
1327 }
1328}
1329
1330#[inline]
1332fn standard_link_complement(link: StandardLink, eta: f64, mu: f64) -> f64 {
1333 match link {
1334 StandardLink::Probit => {
1335 if eta.is_nan() {
1338 f64::NAN
1339 } else if eta == f64::INFINITY {
1340 0.0
1341 } else if eta == f64::NEG_INFINITY {
1342 1.0
1343 } else {
1344 normal_cdf(-eta)
1345 }
1346 }
1347 StandardLink::CLogLog => {
1348 if eta.is_nan() {
1350 f64::NAN
1351 } else {
1352 let t = eta.exp();
1353 if !t.is_finite() { 0.0 } else { (-t).exp() }
1354 }
1355 }
1356 StandardLink::LogLog => {
1357 if eta.is_nan() {
1359 f64::NAN
1360 } else {
1361 let r = (-eta).exp();
1362 if !r.is_finite() { 1.0 } else { -(-r).exp_m1() }
1363 }
1364 }
1365 StandardLink::Cauchit => {
1366 if eta.is_nan() {
1370 f64::NAN
1371 } else if !eta.is_finite() {
1372 if eta > 0.0 { 0.0 } else { 1.0 }
1373 } else if eta > 0.0 {
1374 (1.0 / eta).atan() / std::f64::consts::PI
1375 } else {
1376 0.5 - eta.atan() / std::f64::consts::PI
1377 }
1378 }
1379 StandardLink::Logit | StandardLink::Identity | StandardLink::Log => 1.0 - mu,
1383 }
1384}
1385
1386#[inline]
1404pub(crate) fn sas_link_complement(eta: f64, epsilon: f64, log_delta: f64, mu: f64) -> f64 {
1405 let eta = match finite_inverse_link_eta("SAS inverse link complement", eta) {
1406 Ok(value) => value,
1407 Err(_) => return 1.0 - mu,
1408 };
1409 let delta = sas_delta_from_raw_log_delta(log_delta);
1410 if epsilon.abs() < 1e-12 && (delta - 1.0).abs() < 1e-12 {
1411 return standard_link_complement(StandardLink::Probit, eta, mu);
1412 }
1413 let u_raw = delta * asinh_jet5(eta).value + epsilon;
1414 let u = smooth_bound_jet(u_raw, SAS_U_CLAMP).g;
1415 normal_cdf(-u.sinh())
1416}
1417
1418fn link_function_mu_d1(link: LinkFunction, eta: f64) -> Result<(f64, f64), EstimationError> {
1419 match link {
1420 LinkFunction::Identity => Ok((eta, 1.0)),
1421 LinkFunction::Log => {
1422 let e = log_link_solver_exp(eta)?;
1425 Ok((e, e))
1426 }
1427 LinkFunction::Logit => Ok(component_inverse_link_mu_d1(LinkComponent::Logit, eta)),
1428 LinkFunction::Probit => Ok(component_inverse_link_mu_d1(LinkComponent::Probit, eta)),
1429 LinkFunction::CLogLog => Ok(component_inverse_link_mu_d1(LinkComponent::CLogLog, eta)),
1430 LinkFunction::LogLog => Ok(component_inverse_link_mu_d1(LinkComponent::LogLog, eta)),
1431 LinkFunction::Cauchit => Ok(component_inverse_link_mu_d1(LinkComponent::Cauchit, eta)),
1432 LinkFunction::Sas => Err(EstimationError::InvalidInput(
1433 "LinkFunction::Sas inverse-link requires explicit SAS link state".to_string(),
1434 )),
1435 LinkFunction::BetaLogistic => Err(EstimationError::InvalidInput(
1436 "LinkFunction::BetaLogistic inverse-link requires explicit Beta-Logistic link state"
1437 .to_string(),
1438 )),
1439 }
1440}
1441
1442#[inline]
1443fn component_inverse_link_mu_d1(component: LinkComponent, eta: f64) -> (f64, f64) {
1444 match component {
1450 LinkComponent::Logit => {
1451 let jet = logit_inverse_link_jet5(eta);
1452 (jet.mu, canonicalzero(jet.d1))
1453 }
1454 LinkComponent::Probit => {
1455 if eta.is_nan() {
1456 return (f64::NAN, f64::NAN);
1457 }
1458 if eta == f64::INFINITY {
1459 return (1.0, 0.0);
1460 }
1461 if eta == f64::NEG_INFINITY {
1462 return (0.0, 0.0);
1463 }
1464 let phi = normal_pdf(eta);
1465 (normal_cdf(eta), canonicalzero(phi))
1466 }
1467 LinkComponent::CLogLog => {
1468 if eta.is_nan() {
1469 return (f64::NAN, f64::NAN);
1470 }
1471 let t = eta.exp();
1472 if !t.is_finite() {
1473 return (1.0, 0.0);
1474 }
1475 (
1476 -(-t).exp_m1(),
1477 canonicalzero(stable_nonnegative_poly_times_exp_neg(t, &[0.0, 1.0])),
1478 )
1479 }
1480 LinkComponent::LogLog => {
1481 if eta.is_nan() {
1482 return (f64::NAN, f64::NAN);
1483 }
1484 let r = (-eta).exp();
1485 if !r.is_finite() {
1486 return (0.0, 0.0);
1487 }
1488 (
1489 (-r).exp(),
1490 canonicalzero(stable_nonnegative_poly_times_exp_neg(r, &[0.0, 1.0])),
1491 )
1492 }
1493 LinkComponent::Cauchit => {
1494 if eta.is_nan() {
1495 return (f64::NAN, f64::NAN);
1496 }
1497 let den = 1.0 + eta * eta;
1498 let d1 = if eta.is_finite() {
1499 1.0 / (std::f64::consts::PI * den)
1500 } else {
1501 0.0
1502 };
1503 (0.5 + eta.atan() / std::f64::consts::PI, canonicalzero(d1))
1504 }
1505 }
1506}
1507
1508fn sas_inverse_link_mu_d1(
1509 eta: f64,
1510 epsilon: f64,
1511 log_delta: f64,
1512) -> Result<(f64, f64), EstimationError> {
1513 let eta = finite_inverse_link_eta("SAS inverse link", eta)?;
1514 let delta_id = sas_delta_from_raw_log_delta(log_delta);
1515 if epsilon.abs() < 1e-12 && (delta_id - 1.0).abs() < 1e-12 {
1516 return Ok(component_inverse_link_mu_d1(LinkComponent::Probit, eta));
1517 }
1518 let asinh = asinh_jet5(eta);
1519 let delta = delta_id;
1520 let u_raw = delta * asinh.value + epsilon;
1521 let sb = smooth_bound_jet(u_raw, SAS_U_CLAMP);
1522 let u = sb.g;
1523 let g1 = sb.d1;
1524 let s = u.sinh();
1525 let c = u.cosh();
1526 let z = s;
1527 let r1 = delta * asinh.d1;
1528 let u1 = g1 * r1;
1529 let z1 = c * u1;
1530 let base = probit_jet(z);
1533 Ok((base.mu, canonicalzero(base.d1 * z1)))
1534}
1535
1536fn beta_logistic_inverse_link_mu_d1(eta: f64, delta: f64, epsilon: f64) -> (f64, f64) {
1537 let logistic = logistic_uwith_derivatives(eta);
1538 let a = (delta - epsilon).exp();
1539 let b = (delta + epsilon).exp();
1540 let mu = beta_reg_logistic(a, b, logistic);
1541 let log_d1 = beta_logistic_log_d1(a, b, logistic);
1542 (mu, log_d1.exp())
1543}
1544
1545fn mixture_inverse_link_mu_d1(state: &MixtureLinkState, eta: f64) -> (f64, f64) {
1546 let mut mu = 0.0_f64;
1547 let mut d1 = 0.0_f64;
1548 let k = state.components.len().min(state.pi.len());
1549 for i in 0..k {
1550 let (mu_i, d1_i) = component_inverse_link_mu_d1(state.components[i], eta);
1551 let w = state.pi[i];
1552 mu += w * mu_i;
1553 d1 += w * d1_i;
1554 }
1555 (mu, d1)
1556}
1557
1558#[derive(Clone, Copy)]
1559enum PdfDerivativeOrder {
1560 Third,
1561 Fourth,
1562}
1563
1564impl PdfDerivativeOrder {
1565 fn probit(self, eta: f64) -> f64 {
1566 match self {
1567 Self::Third => probit_pdfthird_derivative(eta),
1568 Self::Fourth => probit_pdffourth_derivative(eta),
1569 }
1570 }
1571
1572 fn component(self, component: LinkComponent, eta: f64) -> f64 {
1573 match self {
1574 Self::Third => component_inverse_link_pdfthird_derivative(component, eta),
1575 Self::Fourth => component_inverse_link_pdffourth_derivative(component, eta),
1576 }
1577 }
1578
1579 fn latent_cloglog(self, eta: f64, latent_sd: f64) -> Result<f64, EstimationError> {
1580 let jet = latent_cloglog_jet5(latent_cloglog_quadctx(), eta, latent_sd)?;
1581 Ok(match self {
1582 Self::Third => jet.d4,
1583 Self::Fourth => jet.d5,
1584 })
1585 }
1586
1587 fn sas(self, eta: f64, epsilon: f64, log_delta: f64) -> Result<f64, EstimationError> {
1588 match self {
1589 Self::Third => sas_inverse_link_pdfthird_derivative(eta, epsilon, log_delta),
1590 Self::Fourth => sas_inverse_link_pdffourth_derivative(eta, epsilon, log_delta),
1591 }
1592 }
1593
1594 fn beta_logistic(self, eta: f64, log_shape_center: f64, epsilon: f64) -> f64 {
1595 match self {
1596 Self::Third => {
1597 beta_logistic_inverse_link_pdfthird_derivative(eta, log_shape_center, epsilon)
1598 }
1599 Self::Fourth => {
1600 beta_logistic_inverse_link_pdffourth_derivative(eta, log_shape_center, epsilon)
1601 }
1602 }
1603 }
1604}
1605
1606fn inverse_link_pdf_derivative_for_inverse_link(
1607 link: &InverseLink,
1608 eta: f64,
1609 order: PdfDerivativeOrder,
1610) -> Result<f64, EstimationError> {
1611 match link {
1612 InverseLink::Standard(StandardLink::Identity) => Ok(0.0),
1613 InverseLink::Standard(StandardLink::Log) => log_link_solver_exp(eta),
1614 InverseLink::Standard(StandardLink::Probit) => Ok(order.probit(eta)),
1615 InverseLink::Standard(StandardLink::Logit) => {
1616 Ok(order.component(LinkComponent::Logit, eta))
1617 }
1618 InverseLink::Standard(StandardLink::CLogLog) => {
1619 Ok(order.component(LinkComponent::CLogLog, eta))
1620 }
1621 InverseLink::Standard(StandardLink::LogLog) => {
1622 Ok(order.component(LinkComponent::LogLog, eta))
1623 }
1624 InverseLink::Standard(StandardLink::Cauchit) => {
1625 Ok(order.component(LinkComponent::Cauchit, eta))
1626 }
1627 InverseLink::LatentCLogLog(state) => order.latent_cloglog(eta, state.latent_sd),
1628 InverseLink::Sas(state) => order.sas(eta, state.epsilon, state.log_delta),
1629 InverseLink::BetaLogistic(state) => {
1630 Ok(order.beta_logistic(eta, state.log_delta, state.epsilon))
1631 }
1632 InverseLink::Mixture(state) => Ok(state
1633 .components
1634 .iter()
1635 .zip(state.pi.iter())
1636 .map(|(&component, &weight)| weight * order.component(component, eta))
1637 .sum()),
1638 }
1639}
1640
1641pub fn inverse_link_pdfthird_derivative_for_inverse_link(
1642 link: &InverseLink,
1643 eta: f64,
1644) -> Result<f64, EstimationError> {
1645 inverse_link_pdf_derivative_for_inverse_link(link, eta, PdfDerivativeOrder::Third)
1663}
1664
1665pub fn inverse_link_pdffourth_derivative_for_inverse_link(
1672 link: &InverseLink,
1673 eta: f64,
1674) -> Result<f64, EstimationError> {
1675 inverse_link_pdf_derivative_for_inverse_link(link, eta, PdfDerivativeOrder::Fourth)
1676}
1677
1678#[inline]
1679fn royston_parmar_inverse_link_jet(eta: f64) -> Result<InverseLinkJet, EstimationError> {
1683 let eta = finite_inverse_link_eta("Royston-Parmar survival inverse link", eta)?;
1684 let hazard = eta.exp();
1685 let survival = (-hazard).exp();
1686 let d1 = -stable_nonnegative_poly_times_exp_neg(hazard, &[0.0, 1.0]);
1691 let d2 = stable_nonnegative_poly_times_exp_neg(hazard, &[0.0, -1.0, 1.0]);
1692 let d3 = stable_nonnegative_poly_times_exp_neg(hazard, &[0.0, -1.0, 3.0, -1.0]);
1693 Ok(InverseLinkJet {
1694 mu: survival,
1695 d1: canonicalzero(d1),
1696 d2: canonicalzero(d2),
1697 d3: canonicalzero(d3),
1698 })
1699}
1700
1701pub fn inverse_link_jet_for_family(
1702 spec: &LikelihoodSpec,
1703 eta: f64,
1704) -> Result<InverseLinkJet, EstimationError> {
1705 if matches!(spec.response, ResponseFamily::RoystonParmar) {
1708 return royston_parmar_inverse_link_jet(eta);
1709 }
1710 spec.link.jet(eta)
1711}
1712
1713#[inline]
1721fn log_inverse_link_jet_exact(eta: f64) -> InverseLinkJet {
1722 let e = eta.exp();
1723 InverseLinkJet {
1724 mu: e,
1725 d1: e,
1726 d2: e,
1727 d3: e,
1728 }
1729}
1730
1731pub fn inverse_link_jet_for_family_public(
1743 spec: &LikelihoodSpec,
1744 eta: f64,
1745) -> Result<InverseLinkJet, EstimationError> {
1746 if matches!(spec.response, ResponseFamily::RoystonParmar) {
1747 return royston_parmar_inverse_link_jet(eta);
1748 }
1749 if let InverseLink::Standard(StandardLink::Log) = spec.link {
1750 return Ok(log_inverse_link_jet_exact(eta));
1751 }
1752 spec.link.jet(eta)
1753}
1754
1755#[inline]
1756pub fn mixture_inverse_link_jet(state: &MixtureLinkState, eta: f64) -> InverseLinkJet {
1757 let mut mu = 0.0_f64;
1758 let mut d1 = 0.0_f64;
1759 let mut d2 = 0.0_f64;
1760 let mut d3 = 0.0_f64;
1761 let k = state.components.len().min(state.pi.len());
1762 for i in 0..k {
1763 let jet = component_inverse_link_jet(state.components[i], eta);
1764 let w = state.pi[i];
1765 mu += w * jet.mu;
1766 d1 += w * jet.d1;
1767 d2 += w * jet.d2;
1768 d3 += w * jet.d3;
1769 }
1770 InverseLinkJet { mu, d1, d2, d3 }
1771}
1772
1773pub fn mixture_inverse_link_jetwith_rho_partials(
1781 state: &MixtureLinkState,
1782 eta: f64,
1783) -> MixtureJetWithRhoPartials {
1784 let k = state.components.len().min(state.pi.len());
1785 let m = k.saturating_sub(1);
1786 let mut djet_drho = vec![
1787 InverseLinkJet {
1788 mu: 0.0,
1789 d1: 0.0,
1790 d2: 0.0,
1791 d3: 0.0,
1792 };
1793 m
1794 ];
1795 let jet = mixture_inverse_link_jetwith_rho_partials_into(state, eta, &mut djet_drho);
1796 let mut d2mu_drho2 = Array2::<f64>::zeros((m, m));
1806 let mut d2d1_drho2 = Array2::<f64>::zeros((m, m));
1807 for j in 0..m {
1808 for k in j..m {
1809 let diagonal = if j == k { 1.0 } else { 0.0 };
1810 let mu = (diagonal - state.pi[k]) * djet_drho[j].mu
1811 - state.pi[j] * djet_drho[k].mu;
1812 let d1 = (diagonal - state.pi[k]) * djet_drho[j].d1
1813 - state.pi[j] * djet_drho[k].d1;
1814 d2mu_drho2[[j, k]] = mu;
1815 d2mu_drho2[[k, j]] = mu;
1816 d2d1_drho2[[j, k]] = d1;
1817 d2d1_drho2[[k, j]] = d1;
1818 }
1819 }
1820 MixtureJetWithRhoPartials {
1821 jet,
1822 djet_drho,
1823 d2mu_drho2,
1824 d2d1_drho2,
1825 }
1826}
1827
1828pub fn mixture_inverse_link_jetwith_rho_partials_into(
1831 state: &MixtureLinkState,
1832 eta: f64,
1833 out: &mut [InverseLinkJet],
1834) -> InverseLinkJet {
1835 let k = state.components.len().min(state.pi.len());
1836 let m = k.saturating_sub(1);
1837 assert!(
1838 out.len() >= m,
1839 "rho-partial output buffer too small: got {}, need {}",
1840 out.len(),
1841 m
1842 );
1843 let mut mixed = InverseLinkJet {
1844 mu: 0.0,
1845 d1: 0.0,
1846 d2: 0.0,
1847 d3: 0.0,
1848 };
1849 for i in 0..k {
1850 let jet_i = component_inverse_link_jet(state.components[i], eta);
1851 let w = state.pi[i];
1852 mixed.mu += w * jet_i.mu;
1853 mixed.d1 += w * jet_i.d1;
1854 mixed.d2 += w * jet_i.d2;
1855 mixed.d3 += w * jet_i.d3;
1856 if i < m {
1859 out[i] = jet_i;
1860 }
1861 }
1862 for j in 0..m {
1863 let pi_j = state.pi[j];
1864 let cj = out[j];
1865 out[j] = InverseLinkJet {
1866 mu: pi_j * (cj.mu - mixed.mu),
1867 d1: pi_j * (cj.d1 - mixed.d1),
1868 d2: pi_j * (cj.d2 - mixed.d2),
1869 d3: pi_j * (cj.d3 - mixed.d3),
1870 };
1871 }
1872 mixed
1873}
1874
1875#[derive(Clone, Copy)]
1876struct LogisticU {
1877 u: f64,
1878 one_minus_u: f64,
1879 ln_u: f64,
1880 ln_one_minus_u: f64,
1881 du: f64,
1882 use_upper_tail: bool,
1883}
1884
1885#[inline]
1886fn logistic_uwith_derivatives(eta: f64) -> LogisticU {
1887 let ln_u = -gam_linalg::utils::stable_softplus(-eta);
1888 let ln_one_minus_u = -gam_linalg::utils::stable_softplus(eta);
1889 let u = ln_u.exp();
1890 let one_minus_u = ln_one_minus_u.exp();
1891 let du = (ln_u + ln_one_minus_u).exp();
1892 LogisticU {
1893 u,
1894 one_minus_u,
1895 ln_u,
1896 ln_one_minus_u,
1897 du,
1898 use_upper_tail: eta >= 0.0,
1899 }
1900}
1901
1902#[inline]
1903fn beta_reg_logistic(a: f64, b: f64, logistic: LogisticU) -> f64 {
1904 if logistic.ln_u.is_nan() || logistic.ln_one_minus_u.is_nan() {
1905 return f64::NAN;
1906 }
1907 if logistic.ln_u == f64::NEG_INFINITY {
1908 return 0.0;
1909 }
1910 if logistic.ln_one_minus_u == f64::NEG_INFINITY {
1911 return 1.0;
1912 }
1913 if logistic.use_upper_tail {
1914 1.0 - beta_reg(b, a, logistic.one_minus_u)
1915 } else {
1916 beta_reg(a, b, logistic.u)
1917 }
1918}
1919
1920#[derive(Clone, Copy)]
1921struct BetaShapePartials {
1922 value: f64,
1923 da: f64,
1924 db: f64,
1925 daa: f64,
1926 dab: f64,
1927 dbb: f64,
1928}
1929
1930impl BetaShapePartials {
1931 #[inline]
1932 fn constant(value: f64) -> Self {
1933 Self {
1934 value,
1935 da: 0.0,
1936 db: 0.0,
1937 daa: 0.0,
1938 dab: 0.0,
1939 dbb: 0.0,
1940 }
1941 }
1942}
1943
1944#[inline]
1945fn beta_reg_with_shape_partials_logistic(
1946 a: f64,
1947 b: f64,
1948 logistic: LogisticU,
1949) -> BetaShapePartials {
1950 if logistic.ln_u.is_nan() || logistic.ln_one_minus_u.is_nan() {
1951 return BetaShapePartials {
1952 value: f64::NAN,
1953 da: f64::NAN,
1954 db: f64::NAN,
1955 daa: f64::NAN,
1956 dab: f64::NAN,
1957 dbb: f64::NAN,
1958 };
1959 }
1960 if logistic.use_upper_tail {
1961 let tail = beta_reg_with_shape_partials(b, a, logistic.one_minus_u);
1962 BetaShapePartials {
1963 value: 1.0 - tail.value,
1964 da: -tail.db,
1965 db: -tail.da,
1966 daa: -tail.dbb,
1967 dab: -tail.dab,
1968 dbb: -tail.daa,
1969 }
1970 } else {
1971 beta_reg_with_shape_partials(a, b, logistic.u)
1972 }
1973}
1974
1975#[inline]
1976fn beta_logistic_log_d1(a: f64, b: f64, logistic: LogisticU) -> f64 {
1977 a * logistic.ln_u + b * logistic.ln_one_minus_u - ln_beta(a, b)
1978}
1979
1980#[derive(Clone, Copy)]
1981struct ShapeDual {
1982 v: f64,
1983 da: f64,
1984 db: f64,
1985 daa: f64,
1986 dab: f64,
1987 dbb: f64,
1988}
1989
1990impl ShapeDual {
1991 #[inline]
1992 fn constant(v: f64) -> Self {
1993 Self {
1994 v,
1995 da: 0.0,
1996 db: 0.0,
1997 daa: 0.0,
1998 dab: 0.0,
1999 dbb: 0.0,
2000 }
2001 }
2002
2003 #[inline]
2004 fn from_value_partials(v: f64, da: f64, db: f64) -> Self {
2005 Self {
2006 v,
2007 da,
2008 db,
2009 daa: 0.0,
2010 dab: 0.0,
2011 dbb: 0.0,
2012 }
2013 }
2014
2015 #[inline]
2016 fn clamp_small(self, floor: f64) -> Self {
2017 if self.v.abs() < floor {
2018 Self::constant(floor)
2019 } else {
2020 self
2021 }
2022 }
2023}
2024
2025impl std::ops::Add for ShapeDual {
2026 type Output = Self;
2027
2028 #[inline]
2029 fn add(self, rhs: Self) -> Self {
2030 Self {
2031 v: self.v + rhs.v,
2032 da: self.da + rhs.da,
2033 db: self.db + rhs.db,
2034 daa: self.daa + rhs.daa,
2035 dab: self.dab + rhs.dab,
2036 dbb: self.dbb + rhs.dbb,
2037 }
2038 }
2039}
2040
2041impl std::ops::Sub for ShapeDual {
2042 type Output = Self;
2043
2044 #[inline]
2045 fn sub(self, rhs: Self) -> Self {
2046 Self {
2047 v: self.v - rhs.v,
2048 da: self.da - rhs.da,
2049 db: self.db - rhs.db,
2050 daa: self.daa - rhs.daa,
2051 dab: self.dab - rhs.dab,
2052 dbb: self.dbb - rhs.dbb,
2053 }
2054 }
2055}
2056
2057impl std::ops::Mul for ShapeDual {
2058 type Output = Self;
2059
2060 #[inline]
2061 fn mul(self, rhs: Self) -> Self {
2062 Self {
2063 v: self.v * rhs.v,
2064 da: self.da * rhs.v + self.v * rhs.da,
2065 db: self.db * rhs.v + self.v * rhs.db,
2066 daa: self.daa * rhs.v + 2.0 * self.da * rhs.da + self.v * rhs.daa,
2067 dab: self.dab * rhs.v
2068 + self.da * rhs.db
2069 + self.db * rhs.da
2070 + self.v * rhs.dab,
2071 dbb: self.dbb * rhs.v + 2.0 * self.db * rhs.db + self.v * rhs.dbb,
2072 }
2073 }
2074}
2075
2076impl std::ops::Div for ShapeDual {
2077 type Output = Self;
2078
2079 #[inline]
2080 fn div(self, rhs: Self) -> Self {
2081 let inv = 1.0 / rhs.v;
2082 let inv2 = inv * inv;
2083 let inv3 = inv2 * inv;
2084 let reciprocal = Self {
2085 v: inv,
2086 da: -rhs.da * inv2,
2087 db: -rhs.db * inv2,
2088 daa: 2.0 * rhs.da * rhs.da * inv3 - rhs.daa * inv2,
2089 dab: 2.0 * rhs.da * rhs.db * inv3 - rhs.dab * inv2,
2090 dbb: 2.0 * rhs.db * rhs.db * inv3 - rhs.dbb * inv2,
2091 };
2092 self * reciprocal
2093 }
2094}
2095
2096impl std::ops::Neg for ShapeDual {
2097 type Output = Self;
2098
2099 #[inline]
2100 fn neg(self) -> Self {
2101 ShapeDual {
2102 v: -self.v,
2103 da: -self.da,
2104 db: -self.db,
2105 daa: -self.daa,
2106 dab: -self.dab,
2107 dbb: -self.dbb,
2108 }
2109 }
2110}
2111
2112#[inline]
2113fn shape_dual(v: f64) -> ShapeDual {
2114 ShapeDual::constant(v)
2115}
2116
2117fn beta_reg_with_shape_partials(a0: f64, b0: f64, x0: f64) -> BetaShapePartials {
2121 if x0 <= 0.0 {
2122 return BetaShapePartials::constant(0.0);
2123 }
2124 if x0 >= 1.0 {
2125 return BetaShapePartials::constant(1.0);
2126 }
2127
2128 let symm_transform = x0 >= (a0 + 1.0) / (a0 + b0 + 2.0);
2129 let (a, b, x) = if symm_transform {
2130 (
2131 ShapeDual::from_value_partials(b0, 0.0, 1.0),
2132 ShapeDual::from_value_partials(a0, 1.0, 0.0),
2133 1.0 - x0,
2134 )
2135 } else {
2136 (
2137 ShapeDual::from_value_partials(a0, 1.0, 0.0),
2138 ShapeDual::from_value_partials(b0, 0.0, 1.0),
2139 x0,
2140 )
2141 };
2142
2143 let ln_x = x.ln();
2144 let ln_1mx = (1.0 - x).ln();
2145 let psi_ab = digamma(a.v + b.v);
2146 let log_bt = statrs::function::gamma::ln_gamma(a.v + b.v)
2147 - statrs::function::gamma::ln_gamma(a.v)
2148 - statrs::function::gamma::ln_gamma(b.v)
2149 + a.v * ln_x
2150 + b.v * ln_1mx;
2151 let bt_v = log_bt.exp();
2152 let log_bt_a = psi_ab - digamma(a.v) + ln_x;
2153 let log_bt_b = psi_ab - digamma(b.v) + ln_1mx;
2154 let trigamma_ab = trigamma(a.v + b.v);
2155 let log_bt_aa = trigamma_ab - trigamma(a.v);
2156 let log_bt_ab = trigamma_ab;
2157 let log_bt_bb = trigamma_ab - trigamma(b.v);
2158 let log_bt_da = log_bt_a * a.da + log_bt_b * b.da;
2159 let log_bt_db = log_bt_a * a.db + log_bt_b * b.db;
2160 let log_bt_daa = log_bt_aa * a.da * a.da
2161 + 2.0 * log_bt_ab * a.da * b.da
2162 + log_bt_bb * b.da * b.da
2163 + log_bt_a * a.daa
2164 + log_bt_b * b.daa;
2165 let log_bt_dab = log_bt_aa * a.da * a.db
2166 + log_bt_ab * (a.da * b.db + b.da * a.db)
2167 + log_bt_bb * b.da * b.db
2168 + log_bt_a * a.dab
2169 + log_bt_b * b.dab;
2170 let log_bt_dbb = log_bt_aa * a.db * a.db
2171 + 2.0 * log_bt_ab * a.db * b.db
2172 + log_bt_bb * b.db * b.db
2173 + log_bt_a * a.dbb
2174 + log_bt_b * b.dbb;
2175 let bt = ShapeDual {
2176 v: bt_v,
2177 da: bt_v * log_bt_da,
2178 db: bt_v * log_bt_db,
2179 daa: bt_v * (log_bt_da * log_bt_da + log_bt_daa),
2180 dab: bt_v * (log_bt_da * log_bt_db + log_bt_dab),
2181 dbb: bt_v * (log_bt_db * log_bt_db + log_bt_dbb),
2182 };
2183
2184 let eps = 0.00000000000000011102230246251565;
2185 let fpmin = f64::MIN_POSITIVE / eps;
2186 let one = shape_dual(1.0);
2187 let qab = a + b;
2188 let qap = a + one;
2189 let qam = a - one;
2190 let mut c = one;
2191 let mut d = (one - qab * shape_dual(x) / qap).clamp_small(fpmin);
2192 d = one / d;
2193 let mut h = d;
2194
2195 for m in 1..141 {
2196 let mf = f64::from(m);
2197 let m2 = mf * 2.0;
2198 let md = shape_dual(mf);
2199 let m2d = shape_dual(m2);
2200 let mut aa = md * (b - md) * shape_dual(x) / ((qam + m2d) * (a + m2d));
2201 d = (one + aa * d).clamp_small(fpmin);
2202 c = (one + aa / c).clamp_small(fpmin);
2203 d = one / d;
2204 h = h * d * c;
2205
2206 aa = (a + md).neg() * (qab + md) * shape_dual(x) / ((a + m2d) * (qap + m2d));
2207 d = (one + aa * d).clamp_small(fpmin);
2208 c = (one + aa / c).clamp_small(fpmin);
2209 d = one / d;
2210 let del = d * c;
2211 h = h * del;
2212
2213 if (del.v - 1.0).abs() <= eps {
2214 let reg = bt * h / a;
2215 return if symm_transform {
2216 BetaShapePartials {
2217 value: 1.0 - reg.v,
2218 da: -reg.da,
2219 db: -reg.db,
2220 daa: -reg.daa,
2221 dab: -reg.dab,
2222 dbb: -reg.dbb,
2223 }
2224 } else {
2225 BetaShapePartials {
2226 value: reg.v,
2227 da: reg.da,
2228 db: reg.db,
2229 daa: reg.daa,
2230 dab: reg.dab,
2231 dbb: reg.dbb,
2232 }
2233 };
2234 }
2235 }
2236 let reg = bt * h / a;
2237 if symm_transform {
2238 BetaShapePartials {
2239 value: 1.0 - reg.v,
2240 da: -reg.da,
2241 db: -reg.db,
2242 daa: -reg.daa,
2243 dab: -reg.dab,
2244 dbb: -reg.dbb,
2245 }
2246 } else {
2247 BetaShapePartials {
2248 value: reg.v,
2249 da: reg.da,
2250 db: reg.db,
2251 daa: reg.daa,
2252 dab: reg.dab,
2253 dbb: reg.dbb,
2254 }
2255 }
2256}
2257
2258pub fn beta_logistic_inverse_link_jet(
2268 eta: f64,
2269 log_shape_center: f64,
2270 epsilon: f64,
2271) -> InverseLinkJet {
2272 let logistic = logistic_uwith_derivatives(eta);
2273 let a = (log_shape_center - epsilon).exp();
2274 let b = (log_shape_center + epsilon).exp();
2275 let mu = beta_reg_logistic(a, b, logistic);
2276 let log_d1 = beta_logistic_log_d1(a, b, logistic);
2277 let d1 = log_d1.exp();
2278 let t = a * logistic.one_minus_u - b * logistic.u;
2279 let d2 = d1 * t;
2280 let d3 = d1 * (t * t - (a + b) * logistic.du);
2281 InverseLinkJet { mu, d1, d2, d3 }
2282}
2283
2284pub fn beta_logistic_inverse_link_pdfthird_derivative(
2285 eta: f64,
2286 log_shape_center: f64,
2287 epsilon: f64,
2288) -> f64 {
2289 let logistic = logistic_uwith_derivatives(eta);
2312 let a = (log_shape_center - epsilon).exp();
2313 let b = (log_shape_center + epsilon).exp();
2314 let log_d1 = beta_logistic_log_d1(a, b, logistic);
2315 let d1 = log_d1.exp();
2316 let c = a + b;
2317 let t = a * logistic.one_minus_u - b * logistic.u;
2318 let u2 = logistic.du * (logistic.one_minus_u - logistic.u);
2319 d1 * (t * t * t - 3.0 * c * t * logistic.du - c * u2)
2320}
2321
2322pub fn beta_logistic_inverse_link_pdffourth_derivative(
2330 eta: f64,
2331 log_shape_center: f64,
2332 epsilon: f64,
2333) -> f64 {
2334 let logistic = logistic_uwith_derivatives(eta);
2335 let a = (log_shape_center - epsilon).exp();
2336 let b = (log_shape_center + epsilon).exp();
2337 let log_d1 = beta_logistic_log_d1(a, b, logistic);
2338 let d1 = log_d1.exp();
2339 let c = a + b;
2340 let t = a * logistic.one_minus_u - b * logistic.u;
2341 let u2 = logistic.du * (logistic.one_minus_u - logistic.u);
2342 let u3 = u2 * (logistic.one_minus_u - logistic.u) - 2.0 * logistic.du * logistic.du;
2343 let t2 = t * t;
2344 d1 * (t2 * t2 - 6.0 * c * t2 * logistic.du - 4.0 * c * t * u2
2345 + 3.0 * c * c * logistic.du * logistic.du
2346 - c * u3)
2347}
2348
2349pub fn beta_logistic_inverse_link_jetwith_param_partials(
2350 eta: f64,
2351 log_shape_center: f64,
2352 epsilon: f64,
2353) -> SasJetWithParamPartials {
2354 let logistic = logistic_uwith_derivatives(eta);
2355 let a = (log_shape_center - epsilon).exp();
2356 let b = (log_shape_center + epsilon).exp();
2357 let shape = beta_reg_with_shape_partials_logistic(a, b, logistic);
2358 let mu = shape.value;
2359 let dmu_dlog_shape_center = a * shape.da + b * shape.db;
2360 let dmu_depsilon = -a * shape.da + b * shape.db;
2361 let log_d1 = beta_logistic_log_d1(a, b, logistic);
2362 let d1 = log_d1.exp();
2363 let t = a * logistic.one_minus_u - b * logistic.u;
2364 let d2 = d1 * t;
2365 let k = t * t - (a + b) * logistic.du;
2366 let d3 = d1 * k;
2367 let jet = InverseLinkJet { mu, d1, d2, d3 };
2368
2369 let psi_a = digamma(a);
2370 let psi_b = digamma(b);
2371 let psi_ab = digamma(a + b);
2372 let la = logistic.ln_u - psi_a + psi_ab;
2373 let lb = logistic.ln_one_minus_u - psi_b + psi_ab;
2374
2375 let partials_for = |a_p: f64, b_p: f64, dmu: f64| -> InverseLinkJet {
2376 let logd1_p = a_p * la + b_p * lb;
2377 let d1_p = d1 * logd1_p;
2378 let t_p = a_p * logistic.one_minus_u - b_p * logistic.u;
2379 let d2_p = d1_p * t + d1 * t_p;
2380 let k_p = 2.0 * t * t_p - (a_p + b_p) * logistic.du;
2381 let d3_p = d1_p * k + d1 * k_p;
2382 InverseLinkJet {
2383 mu: dmu,
2384 d1: d1_p,
2385 d2: d2_p,
2386 d3: d3_p,
2387 }
2388 };
2389 let djet_dlog_shape_center = partials_for(a, b, dmu_dlog_shape_center);
2390 let djet_depsilon = partials_for(-a, b, dmu_depsilon);
2391 let a_first = [-a, a];
2397 let b_first = [b, b];
2398 let a_second = [[a, -a], [-a, a]];
2399 let b_second = [[b, b], [b, b]];
2400 let logd1_first = [
2401 a_first[0] * la + b_first[0] * lb,
2402 a_first[1] * la + b_first[1] * lb,
2403 ];
2404 let trigamma_ab = trigamma(a + b);
2405 let la_a = trigamma_ab - trigamma(a);
2406 let la_b = trigamma_ab;
2407 let lb_a = trigamma_ab;
2408 let lb_b = trigamma_ab - trigamma(b);
2409 let mut d2mu_dparams2 = Array2::<f64>::zeros((2, 2));
2410 let mut d2d1_dparams2 = Array2::<f64>::zeros((2, 2));
2411 for j in 0..2 {
2412 for k in j..2 {
2413 let mu_jk = shape.daa * a_first[j] * a_first[k]
2414 + shape.dab
2415 * (a_first[j] * b_first[k] + b_first[j] * a_first[k])
2416 + shape.dbb * b_first[j] * b_first[k]
2417 + shape.da * a_second[j][k]
2418 + shape.db * b_second[j][k];
2419 let logd1_jk = la_a * a_first[j] * a_first[k]
2420 + la_b * a_first[j] * b_first[k]
2421 + lb_a * b_first[j] * a_first[k]
2422 + lb_b * b_first[j] * b_first[k]
2423 + la * a_second[j][k]
2424 + lb * b_second[j][k];
2425 let d1_jk = d1 * (logd1_first[j] * logd1_first[k] + logd1_jk);
2426 d2mu_dparams2[[j, k]] = mu_jk;
2427 d2mu_dparams2[[k, j]] = mu_jk;
2428 d2d1_dparams2[[j, k]] = d1_jk;
2429 d2d1_dparams2[[k, j]] = d1_jk;
2430 }
2431 }
2432 SasJetWithParamPartials {
2433 jet,
2434 djet_depsilon,
2435 djet_dlog_delta: djet_dlog_shape_center,
2436 d2mu_dparams2,
2437 d2d1_dparams2,
2438 }
2439}
2440
2441pub fn sas_inverse_link_jet(
2453 eta: f64,
2454 epsilon: f64,
2455 log_delta: f64,
2456) -> Result<InverseLinkJet, EstimationError> {
2457 let eta = finite_inverse_link_eta("SAS inverse link", eta)?;
2458 let delta_id = sas_delta_from_raw_log_delta(log_delta);
2459 if epsilon.abs() < 1e-12 && (delta_id - 1.0).abs() < 1e-12 {
2460 return Ok(component_inverse_link_jet(LinkComponent::Probit, eta));
2461 }
2462 let asinh = asinh_jet5(eta);
2463 let delta = delta_id;
2464 let u_raw = delta * asinh.value + epsilon;
2465 let sb = smooth_bound_jet(u_raw, SAS_U_CLAMP);
2466 let u = sb.g;
2467 let g1 = sb.d1;
2468 let g2 = sb.d2;
2469 let g3 = sb.d3;
2470 let s = u.sinh();
2471 let c = u.cosh();
2472 let z = s;
2473 let r1 = delta * asinh.d1;
2474 let r2 = delta * asinh.d2;
2475 let r3 = delta * asinh.d3;
2476 let u1 = g1 * r1;
2477 let u2 = g2 * r1 * r1 + g1 * r2;
2478 let u3 = g3 * r1 * r1 * r1 + 3.0 * g2 * r1 * r2 + g1 * r3;
2479 let z1 = c * u1;
2480 let z2 = s * u1 * u1 + c * u2;
2481 let z3 = c * u1 * u1 * u1 + 3.0 * s * u1 * u2 + c * u3;
2482 let base = probit_jet(z);
2483 Ok(chain_inverse_link_jet(base, z1, z2, z3))
2484}
2485
2486pub fn sas_inverse_link_pdfthird_derivative(
2489 eta: f64,
2490 epsilon: f64,
2491 log_delta: f64,
2492) -> Result<f64, EstimationError> {
2493 let eta = finite_inverse_link_eta("SAS inverse link", eta)?;
2529 let asinh = asinh_jet5(eta);
2530 let delta = sas_delta_from_raw_log_delta(log_delta);
2531 let u_raw = delta * asinh.value + epsilon;
2532 let sb = smooth_bound_jet(u_raw, SAS_U_CLAMP);
2533 let u = sb.g;
2534 let g1 = sb.d1;
2535 let g2 = sb.d2;
2536 let g3 = sb.d3;
2537 let g4 = sb.d4;
2538 let s = u.sinh();
2539 let c = u.cosh();
2540 let z = s;
2541 let base = probit_jet(z);
2542 let r1 = delta * asinh.d1;
2543 let r2 = delta * asinh.d2;
2544 let r3 = delta * asinh.d3;
2545 let r4 = delta * asinh.d4;
2546 let u1 = g1 * r1;
2547 let u2 = g2 * r1 * r1 + g1 * r2;
2548 let u3 = g3 * r1 * r1 * r1 + 3.0 * g2 * r1 * r2 + g1 * r3;
2549 let u4 = g4 * r1.powi(4)
2550 + 6.0 * g3 * r1 * r1 * r2
2551 + 3.0 * g2 * r2 * r2
2552 + 4.0 * g2 * r1 * r3
2553 + g1 * r4;
2554 let z1 = c * u1;
2555 let z2 = s * u1 * u1 + c * u2;
2556 let z3 = c * u1 * u1 * u1 + 3.0 * s * u1 * u2 + c * u3;
2557 let z4 =
2558 s * u1.powi(4) + 6.0 * c * u1 * u1 * u2 + 3.0 * s * u2 * u2 + 4.0 * s * u1 * u3 + c * u4;
2559 let base4 = probit_pdfthird_derivative(z);
2560 let out = base4 * z1.powi(4)
2561 + 6.0 * base.d3 * z1 * z1 * z2
2562 + 3.0 * base.d2 * z2 * z2
2563 + 4.0 * base.d2 * z1 * z3
2564 + base.d1 * z4;
2565 Ok(canonicalzero(out))
2566}
2567
2568pub fn sas_inverse_link_pdffourth_derivative(
2587 eta: f64,
2588 epsilon: f64,
2589 log_delta: f64,
2590) -> Result<f64, EstimationError> {
2591 let eta = finite_inverse_link_eta("SAS inverse link", eta)?;
2592 let asinh = asinh_jet5(eta);
2593 let delta = sas_delta_from_raw_log_delta(log_delta);
2594 let u_raw = delta * asinh.value + epsilon;
2595 let sb = smooth_bound_jet(u_raw, SAS_U_CLAMP);
2596 let u = sb.g;
2597 let g1 = sb.d1;
2598 let g2 = sb.d2;
2599 let g3 = sb.d3;
2600 let g4 = sb.d4;
2601 let g5 = sb.d5;
2602 let s = u.sinh();
2603 let c = u.cosh();
2604 let z = s;
2605
2606 let base = probit_jet(z);
2608 let phi3 = probit_pdfthird_derivative(z); let phi4 = probit_pdffourth_derivative(z); let r1 = delta * asinh.d1;
2612 let r2 = delta * asinh.d2;
2613 let r3 = delta * asinh.d3;
2614 let r4 = delta * asinh.d4;
2615 let r5 = delta * asinh.d5;
2616
2617 let u1 = g1 * r1;
2619 let u2 = g2 * r1 * r1 + g1 * r2;
2620 let u3 = g3 * r1 * r1 * r1 + 3.0 * g2 * r1 * r2 + g1 * r3;
2621 let u4 = g4 * r1.powi(4)
2622 + 6.0 * g3 * r1 * r1 * r2
2623 + 3.0 * g2 * r2 * r2
2624 + 4.0 * g2 * r1 * r3
2625 + g1 * r4;
2626 let u5 = g5 * r1.powi(5)
2627 + 10.0 * g4 * r1 * r1 * r1 * r2
2628 + 15.0 * g3 * r1 * r2 * r2
2629 + 10.0 * g3 * r1 * r1 * r3
2630 + 10.0 * g2 * r2 * r3
2631 + 5.0 * g2 * r1 * r4
2632 + g1 * r5;
2633
2634 let z1 = c * u1;
2636 let z2 = s * u1 * u1 + c * u2;
2637 let z3 = c * u1 * u1 * u1 + 3.0 * s * u1 * u2 + c * u3;
2638 let z4 =
2639 s * u1.powi(4) + 6.0 * c * u1 * u1 * u2 + 3.0 * s * u2 * u2 + 4.0 * s * u1 * u3 + c * u4;
2640 let z5 = c * u1.powi(5)
2641 + 10.0 * s * u1 * u1 * u1 * u2
2642 + 15.0 * c * u1 * u2 * u2
2643 + 10.0 * c * u1 * u1 * u3
2644 + 10.0 * s * u2 * u3
2645 + 5.0 * s * u1 * u4
2646 + c * u5;
2647
2648 let out = phi4 * z1.powi(5)
2651 + 10.0 * phi3 * z1 * z1 * z1 * z2
2652 + 15.0 * base.d3 * z1 * z2 * z2
2653 + 10.0 * base.d3 * z1 * z1 * z3
2654 + 10.0 * base.d2 * z2 * z3
2655 + 5.0 * base.d2 * z1 * z4
2656 + base.d1 * z5;
2657 Ok(canonicalzero(out))
2658}
2659
2660pub fn sas_inverse_link_jetwith_param_partials(
2664 eta: f64,
2665 epsilon: f64,
2666 log_delta: f64,
2667) -> Result<SasJetWithParamPartials, EstimationError> {
2668 let eta = finite_inverse_link_eta("SAS inverse link", eta)?;
2669 let asinh = asinh_jet5(eta);
2670 let ld_sb = smooth_bound_jet(log_delta, SAS_LOG_DELTA_BOUND);
2671 let (ld_eff, dld_eff_draw) = (ld_sb.g, ld_sb.d1);
2672 let d2ld_eff_draw2 = ld_sb.d2;
2673 let delta = ld_eff.exp();
2674 let ddelta_draw = delta * dld_eff_draw;
2675 let d2delta_draw2 = delta * (dld_eff_draw * dld_eff_draw + d2ld_eff_draw2);
2676 let u_raw = delta * asinh.value + epsilon;
2677 let sb = smooth_bound_jet(u_raw, SAS_U_CLAMP);
2678 let u = sb.g;
2679 let g1 = sb.d1;
2680 let g2 = sb.d2;
2681 let g3 = sb.d3;
2682 let g4 = sb.d4;
2683 let s = u.sinh();
2684 let c = u.cosh();
2685 let z = s;
2686 let a1 = asinh.d1;
2687 let a2 = asinh.d2;
2688 let a3 = asinh.d3;
2689 let r1 = delta * a1;
2690 let r2 = delta * a2;
2691 let r3 = delta * a3;
2692 let u1 = g1 * r1;
2693 let u2 = g2 * r1 * r1 + g1 * r2;
2694 let u3 = g3 * r1 * r1 * r1 + 3.0 * g2 * r1 * r2 + g1 * r3;
2695 let z1 = c * u1;
2696 let z2 = s * u1 * u1 + c * u2;
2697 let z3 = c * u1 * u1 * u1 + 3.0 * s * u1 * u2 + c * u3;
2698
2699 let base = probit_jet(z);
2700 let jet = chain_inverse_link_jet(base, z1, z2, z3);
2701
2702 let param_partials = |u_t: f64, u1_t: f64, u2_t: f64, u3_t: f64| -> InverseLinkJet {
2705 let z_t = c * u_t;
2706 let z1_t = s * u_t * u1 + c * u1_t;
2707 let z2_t = c * u_t * u1 * u1 + 2.0 * s * u1 * u1_t + s * u_t * u2 + c * u2_t;
2708 let z3_t = s * u_t * u1 * u1 * u1
2709 + 3.0 * c * u1 * u1 * u1_t
2710 + 3.0 * c * u_t * u1 * u2
2711 + 3.0 * s * (u1_t * u2 + u1 * u2_t)
2712 + s * u_t * u3
2713 + c * u3_t;
2714
2715 InverseLinkJet {
2716 mu: base.d1 * z_t,
2717 d1: base.d2 * z_t * z1 + base.d1 * z1_t,
2718 d2: base.d3 * z_t * z1 * z1
2719 + 2.0 * base.d2 * z1 * z1_t
2720 + base.d2 * z_t * z2
2721 + base.d1 * z2_t,
2722 d3: probit_pdfthird_derivative(z) * z_t * z1.powi(3)
2723 + 3.0 * base.d3 * z1 * z1 * z1_t
2724 + 3.0 * base.d3 * z_t * z1 * z2
2725 + 3.0 * base.d2 * (z1_t * z2 + z1 * z2_t)
2726 + base.d2 * z_t * z3
2727 + base.d1 * z3_t,
2728 }
2729 };
2730
2731 let rt_eps = 1.0;
2733 let r1t_eps = 0.0;
2734 let r2t_eps = 0.0;
2735 let r3t_eps = 0.0;
2736 let u_eps = g1 * rt_eps;
2737 let u1_eps = g2 * rt_eps * r1 + g1 * r1t_eps;
2738 let u2_eps = g3 * rt_eps * r1 * r1 + 2.0 * g2 * r1 * r1t_eps + g2 * rt_eps * r2 + g1 * r2t_eps;
2739 let u3_eps = g4 * rt_eps * r1 * r1 * r1
2740 + 3.0 * g3 * r1 * r1 * r1t_eps
2741 + 3.0 * g3 * rt_eps * r1 * r2
2742 + 3.0 * g2 * (r1t_eps * r2 + r1 * r2t_eps)
2743 + g2 * rt_eps * r3
2744 + g1 * r3t_eps;
2745 let djet_depsilon = param_partials(u_eps, u1_eps, u2_eps, u3_eps);
2746
2747 let rt_ld = ddelta_draw * asinh.value;
2749 let r1t_ld = ddelta_draw * a1;
2750 let r2t_ld = ddelta_draw * a2;
2751 let r3t_ld = ddelta_draw * a3;
2752 let u_ld = g1 * rt_ld;
2753 let u1_ld = g2 * rt_ld * r1 + g1 * r1t_ld;
2754 let u2_ld = g3 * rt_ld * r1 * r1 + 2.0 * g2 * r1 * r1t_ld + g2 * rt_ld * r2 + g1 * r2t_ld;
2755 let u3_ld = g4 * rt_ld * r1 * r1 * r1
2756 + 3.0 * g3 * r1 * r1 * r1t_ld
2757 + 3.0 * g3 * rt_ld * r1 * r2
2758 + 3.0 * g2 * (r1t_ld * r2 + r1 * r2t_ld)
2759 + g2 * rt_ld * r3
2760 + g1 * r3t_ld;
2761 let djet_dlog_delta = param_partials(u_ld, u1_ld, u2_ld, u3_ld);
2762
2763 let r_t = [rt_eps, rt_ld];
2768 let r1_t = [r1t_eps, r1t_ld];
2769 let r_tt = [[0.0, 0.0], [0.0, d2delta_draw2 * asinh.value]];
2770 let r1_tt = [[0.0, 0.0], [0.0, d2delta_draw2 * a1]];
2771 let u_t = [u_eps, u_ld];
2772 let u1_t = [u1_eps, u1_ld];
2773 let z_t = [c * u_t[0], c * u_t[1]];
2774 let z1_t = [
2775 s * u_t[0] * u1 + c * u1_t[0],
2776 s * u_t[1] * u1 + c * u1_t[1],
2777 ];
2778 let mut d2mu_dparams2 = Array2::<f64>::zeros((2, 2));
2779 let mut d2d1_dparams2 = Array2::<f64>::zeros((2, 2));
2780 for j in 0..2 {
2781 for k in j..2 {
2782 let u_jk = g2 * r_t[j] * r_t[k] + g1 * r_tt[j][k];
2783 let u1_jk = g3 * r_t[j] * r_t[k] * r1
2784 + g2 * r_tt[j][k] * r1
2785 + g2 * r_t[j] * r1_t[k]
2786 + g2 * r_t[k] * r1_t[j]
2787 + g1 * r1_tt[j][k];
2788 let z_jk = s * u_t[j] * u_t[k] + c * u_jk;
2789 let z1_jk = c * u_t[j] * u_t[k] * u1
2790 + s * u_jk * u1
2791 + s * u_t[j] * u1_t[k]
2792 + s * u_t[k] * u1_t[j]
2793 + c * u1_jk;
2794 let mu_jk = base.d2 * z_t[j] * z_t[k] + base.d1 * z_jk;
2795 let d1_jk = base.d3 * z_t[j] * z_t[k] * z1
2796 + base.d2 * z_jk * z1
2797 + base.d2 * z_t[j] * z1_t[k]
2798 + base.d2 * z_t[k] * z1_t[j]
2799 + base.d1 * z1_jk;
2800 d2mu_dparams2[[j, k]] = mu_jk;
2801 d2mu_dparams2[[k, j]] = mu_jk;
2802 d2d1_dparams2[[j, k]] = d1_jk;
2803 d2d1_dparams2[[k, j]] = d1_jk;
2804 }
2805 }
2806
2807 Ok(SasJetWithParamPartials {
2808 jet,
2809 djet_depsilon,
2810 djet_dlog_delta,
2811 d2mu_dparams2,
2812 d2d1_dparams2,
2813 })
2814}
2815
2816#[cfg(test)]
2817mod tests {
2818 use super::*;
2819 use gam_problem::{InverseLink, LikelihoodSpec, LinkComponent, MixtureLinkSpec, SasLinkState};
2820
2821 fn assert_log_link_domain_error(error: EstimationError, eta: f64) {
2822 match error {
2823 EstimationError::InverseLinkDomainViolation {
2824 link,
2825 eta: rejected,
2826 lower,
2827 upper,
2828 } => {
2829 assert_eq!(link, "standard log inverse link");
2830 if eta.is_nan() {
2831 assert!(rejected.is_nan());
2832 } else {
2833 assert_eq!(rejected, eta);
2834 }
2835 assert_eq!(lower, LOG_LINK_SOLVER_ETA_MIN);
2836 assert_eq!(upper, LOG_LINK_SOLVER_ETA_MAX);
2837 }
2838 other => panic!("expected typed log-link domain refusal, got {other}"),
2839 }
2840 }
2841
2842 fn assert_finite_eta_domain_error(
2843 error: EstimationError,
2844 expected_link: &'static str,
2845 eta: f64,
2846 ) {
2847 match error {
2848 EstimationError::InverseLinkDomainViolation {
2849 link,
2850 eta: rejected,
2851 lower,
2852 upper,
2853 } => {
2854 assert_eq!(link, expected_link);
2855 if eta.is_nan() {
2856 assert!(rejected.is_nan());
2857 } else {
2858 assert_eq!(rejected, eta);
2859 }
2860 assert_eq!(lower, -f64::MAX);
2861 assert_eq!(upper, f64::MAX);
2862 }
2863 other => panic!("expected typed finite-eta domain refusal, got {other}"),
2864 }
2865 }
2866
2867 #[test]
2868 fn log_link_solver_boundaries_are_inclusive_exact_exp_jets() {
2869 let link = InverseLink::Standard(StandardLink::Log);
2870 let spec = LikelihoodSpec::poisson_log();
2871 for eta in [LOG_LINK_SOLVER_ETA_MIN, LOG_LINK_SOLVER_ETA_MAX] {
2872 let expected = eta.exp();
2873 assert!(expected.is_finite() && expected > 0.0);
2874
2875 let jet = inverse_link_jet_for_inverse_link(&link, eta).expect("boundary jet");
2876 assert_eq!(
2877 LinkFunction::Log.jet(eta).expect("kernel boundary jet"),
2878 jet
2879 );
2880 assert_eq!(jet.mu, expected);
2881 assert_eq!(jet.d1, expected);
2882 assert_eq!(jet.d2, expected);
2883 assert_eq!(jet.d3, expected);
2884
2885 assert_eq!(
2886 inverse_link_mu_d1_for_inverse_link(&link, eta).expect("boundary mu/d1"),
2887 (expected, expected)
2888 );
2889 assert_eq!(
2890 inverse_link_pdfthird_derivative_for_inverse_link(&link, eta)
2891 .expect("boundary fourth derivative"),
2892 expected
2893 );
2894 assert_eq!(
2895 inverse_link_pdffourth_derivative_for_inverse_link(&link, eta)
2896 .expect("boundary fifth derivative"),
2897 expected
2898 );
2899 assert_eq!(
2900 inverse_link_jet_for_family(&spec, eta).expect("boundary family jet"),
2901 jet
2902 );
2903 }
2904 }
2905
2906 #[test]
2907 fn log_link_solver_seams_refuse_every_eta_outside_the_declared_domain() {
2908 let link = InverseLink::Standard(StandardLink::Log);
2909 let spec = LikelihoodSpec::poisson_log();
2910 let just_below = f64::from_bits(LOG_LINK_SOLVER_ETA_MIN.to_bits() + 1);
2911 let just_above = f64::from_bits(LOG_LINK_SOLVER_ETA_MAX.to_bits() + 1);
2912
2913 for eta in [
2914 just_below,
2915 just_above,
2916 f64::NEG_INFINITY,
2917 f64::INFINITY,
2918 f64::NAN,
2919 ] {
2920 assert_log_link_domain_error(
2921 inverse_link_jet_for_inverse_link(&link, eta).expect_err("full jet must refuse"),
2922 eta,
2923 );
2924 assert_log_link_domain_error(
2925 LinkFunction::Log
2926 .jet(eta)
2927 .expect_err("kernel jet must refuse"),
2928 eta,
2929 );
2930 assert_log_link_domain_error(
2931 inverse_link_mu_d1_for_inverse_link(&link, eta)
2932 .expect_err("mu/d1 seam must refuse"),
2933 eta,
2934 );
2935 assert_log_link_domain_error(
2936 inverse_link_pdfthird_derivative_for_inverse_link(&link, eta)
2937 .expect_err("fourth derivative seam must refuse"),
2938 eta,
2939 );
2940 assert_log_link_domain_error(
2941 inverse_link_pdffourth_derivative_for_inverse_link(&link, eta)
2942 .expect_err("fifth derivative seam must refuse"),
2943 eta,
2944 );
2945 assert_log_link_domain_error(
2946 inverse_link_jet_for_family(&spec, eta).expect_err("family jet seam must refuse"),
2947 eta,
2948 );
2949 }
2950 }
2951
2952 #[test]
2953 fn log_link_solver_value_gradient_is_consistent_near_both_domain_edges() {
2954 let link = InverseLink::Standard(StandardLink::Log);
2955 let h = 1.0e-5;
2956 for eta in [
2957 LOG_LINK_SOLVER_ETA_MIN + 1.0,
2958 0.0,
2959 LOG_LINK_SOLVER_ETA_MAX - 1.0,
2960 ] {
2961 let jet = inverse_link_jet_for_inverse_link(&link, eta).expect("interior jet");
2962 let eta_plus = eta + h;
2963 let eta_minus = eta - h;
2964 let mu_plus = inverse_link_jet_for_inverse_link(&link, eta_plus)
2965 .expect("plus jet")
2966 .mu;
2967 let mu_minus = inverse_link_jet_for_inverse_link(&link, eta_minus)
2968 .expect("minus jet")
2969 .mu;
2970 let finite_difference = (mu_plus - mu_minus) / (eta_plus - eta_minus);
2971 let relative_error = ((finite_difference - jet.d1) / jet.d1).abs();
2972 assert!(
2973 relative_error < 5.0e-10,
2974 "log-link value/gradient mismatch at eta={eta}: analytic={}, finite_difference={}, relative_error={relative_error}",
2975 jet.d1,
2976 finite_difference
2977 );
2978 }
2979 }
2980
2981 #[test]
2982 fn subnormal_inverse_link_derivatives_are_preserved_not_plateaued() {
2983 let left_eta = -743.0_f64;
2984 let left_scale = left_eta.exp();
2985 assert!(left_scale > 0.0 && left_scale < f64::MIN_POSITIVE);
2986 let left = logit_inverse_link_jet5(left_eta);
2987 for (order, derivative) in [left.d1, left.d2, left.d3, left.d4, left.d5]
2988 .into_iter()
2989 .enumerate()
2990 {
2991 assert!(
2992 derivative > 0.0 && derivative < f64::MIN_POSITIVE,
2993 "left-tail logit derivative order {} lost its represented subnormal: {derivative}",
2994 order + 1
2995 );
2996 }
2997
2998 let right_eta = 743.0_f64;
2999 let right_scale = (-right_eta).exp();
3000 assert!(right_scale > 0.0 && right_scale < f64::MIN_POSITIVE);
3001 let right = logit_inverse_link_jet5(right_eta);
3002 for (order, derivative, sign) in [
3003 (1, right.d1, 1.0),
3004 (2, right.d2, -1.0),
3005 (3, right.d3, 1.0),
3006 (4, right.d4, -1.0),
3007 (5, right.d5, 1.0),
3008 ] {
3009 assert_eq!(derivative.signum(), sign, "wrong order-{order} tail sign");
3010 assert!(
3011 derivative.abs() > 0.0 && derivative.abs() < f64::MIN_POSITIVE,
3012 "right-tail logit derivative order {order} lost its represented subnormal: {derivative}"
3013 );
3014 }
3015
3016 let royston_eta = 735.0_f64.ln();
3017 let royston = royston_parmar_inverse_link_jet(royston_eta)
3018 .expect("finite Royston-Parmar subnormal-tail eta");
3019 assert!(
3020 royston.d1 < 0.0 && royston.d1.abs() < f64::MIN_POSITIVE,
3021 "Royston-Parmar exact tail derivative must retain its subnormal: {}",
3022 royston.d1
3023 );
3024 }
3025
3026 #[test]
3027 fn sas_all_derivative_seams_refuse_nonfinite_eta_with_one_typed_contract() {
3028 let state = sas_link_state_from_raw(0.25, -0.35).expect("SAS state");
3029 let link = InverseLink::Sas(state);
3030 for eta in [f64::NEG_INFINITY, f64::INFINITY, f64::NAN] {
3031 assert_finite_eta_domain_error(
3032 sas_inverse_link_jet(eta, state.epsilon, state.log_delta)
3033 .expect_err("SAS full jet must refuse"),
3034 "SAS inverse link",
3035 eta,
3036 );
3037 assert_finite_eta_domain_error(
3038 sas_inverse_link_mu_d1(eta, state.epsilon, state.log_delta)
3039 .expect_err("SAS mu/d1 must refuse"),
3040 "SAS inverse link",
3041 eta,
3042 );
3043 assert_finite_eta_domain_error(
3044 sas_inverse_link_pdfthird_derivative(eta, state.epsilon, state.log_delta)
3045 .expect_err("SAS fourth derivative must refuse"),
3046 "SAS inverse link",
3047 eta,
3048 );
3049 assert_finite_eta_domain_error(
3050 sas_inverse_link_pdffourth_derivative(eta, state.epsilon, state.log_delta)
3051 .expect_err("SAS fifth derivative must refuse"),
3052 "SAS inverse link",
3053 eta,
3054 );
3055 assert_finite_eta_domain_error(
3056 sas_inverse_link_jetwith_param_partials(eta, state.epsilon, state.log_delta)
3057 .expect_err("SAS parameter partials must refuse"),
3058 "SAS inverse link",
3059 eta,
3060 );
3061 assert_finite_eta_domain_error(
3062 inverse_link_jet_for_inverse_link(&link, eta).expect_err("SAS kernel must refuse"),
3063 "SAS inverse link",
3064 eta,
3065 );
3066 assert_finite_eta_domain_error(
3067 inverse_link_mu_d1_for_inverse_link(&link, eta)
3068 .expect_err("SAS fast dispatch must refuse"),
3069 "SAS inverse link",
3070 eta,
3071 );
3072 assert_finite_eta_domain_error(
3073 inverse_link_pdfthird_derivative_for_inverse_link(&link, eta)
3074 .expect_err("SAS fourth-derivative dispatch must refuse"),
3075 "SAS inverse link",
3076 eta,
3077 );
3078 assert_finite_eta_domain_error(
3079 inverse_link_pdffourth_derivative_for_inverse_link(&link, eta)
3080 .expect_err("SAS fifth-derivative dispatch must refuse"),
3081 "SAS inverse link",
3082 eta,
3083 );
3084 }
3085 }
3086
3087 #[test]
3088 fn sas_jets_are_finite_at_both_finite_f64_domain_edges() {
3089 let state = sas_link_state_from_raw(0.25, -0.35).expect("SAS state");
3090 for eta in [-f64::MAX, f64::MAX] {
3091 let jet = sas_inverse_link_jet(eta, state.epsilon, state.log_delta)
3092 .expect("finite SAS boundary jet");
3093 let partials =
3094 sas_inverse_link_jetwith_param_partials(eta, state.epsilon, state.log_delta)
3095 .expect("finite SAS boundary partials");
3096 let h4 = sas_inverse_link_pdfthird_derivative(eta, state.epsilon, state.log_delta)
3097 .expect("finite SAS boundary fourth derivative");
3098 let h5 = sas_inverse_link_pdffourth_derivative(eta, state.epsilon, state.log_delta)
3099 .expect("finite SAS boundary fifth derivative");
3100 for value in [
3101 jet.mu,
3102 jet.d1,
3103 jet.d2,
3104 jet.d3,
3105 partials.jet.mu,
3106 partials.jet.d1,
3107 partials.jet.d2,
3108 partials.jet.d3,
3109 partials.djet_depsilon.mu,
3110 partials.djet_depsilon.d1,
3111 partials.djet_depsilon.d2,
3112 partials.djet_depsilon.d3,
3113 partials.djet_dlog_delta.mu,
3114 partials.djet_dlog_delta.d1,
3115 partials.djet_dlog_delta.d2,
3116 partials.djet_dlog_delta.d3,
3117 h4,
3118 h5,
3119 ] {
3120 assert!(
3121 value.is_finite(),
3122 "non-finite SAS boundary jet at eta={eta}: {value}"
3123 );
3124 }
3125 }
3126 }
3127
3128 #[test]
3129 fn royston_parmar_exact_jet_has_no_former_minus_thirty_plateau() {
3130 let left =
3131 royston_parmar_inverse_link_jet(-30.0 - 1.0e-6).expect("finite Royston-Parmar eta");
3132 let center = royston_parmar_inverse_link_jet(-30.0).expect("finite Royston-Parmar eta");
3133 let right =
3134 royston_parmar_inverse_link_jet(-30.0 + 1.0e-6).expect("finite Royston-Parmar eta");
3135
3136 assert!(
3137 left.d1 < 0.0,
3138 "the exact left tail must not be a constant plateau"
3139 );
3140 assert!(center.d1 < 0.0 && right.d1 < 0.0);
3141 let left_relative = ((left.d1 - center.d1) / center.d1).abs();
3142 let right_relative = ((right.d1 - center.d1) / center.d1).abs();
3143 assert!(
3144 left_relative < 2.0e-6,
3145 "left derivative kink: {left_relative}"
3146 );
3147 assert!(
3148 right_relative < 2.0e-6,
3149 "right derivative kink: {right_relative}"
3150 );
3151
3152 for eta in [-f64::MAX, -40.0, -30.0, 0.0, 7.0, 30.0, 40.0, f64::MAX] {
3153 let jet = royston_parmar_inverse_link_jet(eta).expect("finite Royston-Parmar eta");
3154 for value in [jet.mu, jet.d1, jet.d2, jet.d3] {
3155 assert!(
3156 value.is_finite(),
3157 "non-finite Royston-Parmar jet at eta={eta}: {value}"
3158 );
3159 }
3160 }
3161 }
3162
3163 #[test]
3164 fn royston_parmar_seams_refuse_nonfinite_eta_instead_of_clamping() {
3165 let spec = LikelihoodSpec::new(
3166 ResponseFamily::RoystonParmar,
3167 InverseLink::Standard(StandardLink::Identity),
3168 );
3169 for eta in [f64::NEG_INFINITY, f64::INFINITY, f64::NAN] {
3170 assert_finite_eta_domain_error(
3171 royston_parmar_inverse_link_jet(eta)
3172 .expect_err("direct Royston-Parmar jet must refuse"),
3173 "Royston-Parmar survival inverse link",
3174 eta,
3175 );
3176 assert_finite_eta_domain_error(
3177 inverse_link_jet_for_family(&spec, eta)
3178 .expect_err("solver Royston-Parmar jet must refuse"),
3179 "Royston-Parmar survival inverse link",
3180 eta,
3181 );
3182 assert_finite_eta_domain_error(
3183 inverse_link_jet_for_family_public(&spec, eta)
3184 .expect_err("public Royston-Parmar jet must refuse"),
3185 "Royston-Parmar survival inverse link",
3186 eta,
3187 );
3188 }
3189 }
3190
3191 #[test]
3192 fn softmax_jacobian_matchesfd() {
3193 let rho = Array1::from_vec(vec![0.7, -1.2, 0.4]);
3194 let (pi, jac) = softmaxwith_jacobian_last_fixedzero(&rho);
3195 let h = 1e-6;
3196 for j in 0..rho.len() {
3197 let mut rp = rho.clone();
3198 rp[j] += h;
3199 let mut rm = rho.clone();
3200 rm[j] -= h;
3201 let pp = softmax_last_fixedzero(&rp);
3202 let pm = softmax_last_fixedzero(&rm);
3203 let fd = (&pp - &pm).mapv(|v| v / (2.0 * h));
3204 for k in 0..pi.len() {
3205 let err = (jac[[k, j]] - fd[k]).abs();
3206 assert_eq!(
3207 jac[[k, j]].signum(),
3208 fd[k].signum(),
3209 "jac sign mismatch at ({k},{j}): analytic={} fd={}",
3210 jac[[k, j]],
3211 fd[k]
3212 );
3213 assert!(err < 5e-6, "jac mismatch at ({k},{j}): err={err:e}");
3214 }
3215 }
3216 }
3217
3218 #[test]
3219 fn mixture_jet_rho_partials_matchfd() {
3220 let spec = MixtureLinkSpec {
3221 components: vec![
3222 LinkComponent::Probit,
3223 LinkComponent::Logit,
3224 LinkComponent::CLogLog,
3225 LinkComponent::Cauchit,
3226 ],
3227 initial_rho: Array1::from_vec(vec![0.3, -0.6, 0.2]),
3228 };
3229 let state = state_fromspec(&spec).expect("state");
3230 let eta = 0.35;
3231 let out = mixture_inverse_link_jetwith_rho_partials(&state, eta);
3232 let h = 1e-6;
3233 for j in 0..state.rho.len() {
3234 let mut rp = state.rho.clone();
3235 rp[j] += h;
3236 let sp = MixtureLinkSpec {
3237 components: state.components.clone(),
3238 initial_rho: rp,
3239 };
3240 let jp = mixture_inverse_link_jet(&state_fromspec(&sp).expect("sp"), eta);
3241 let mut rm = state.rho.clone();
3242 rm[j] -= h;
3243 let sm = MixtureLinkSpec {
3244 components: state.components.clone(),
3245 initial_rho: rm,
3246 };
3247 let jm = mixture_inverse_link_jet(&state_fromspec(&sm).expect("sm"), eta);
3248 let fd = InverseLinkJet {
3249 mu: (jp.mu - jm.mu) / (2.0 * h),
3250 d1: (jp.d1 - jm.d1) / (2.0 * h),
3251 d2: (jp.d2 - jm.d2) / (2.0 * h),
3252 d3: (jp.d3 - jm.d3) / (2.0 * h),
3253 };
3254 let an = out.djet_drho[j];
3255 assert_eq!(an.mu.signum(), fd.mu.signum());
3256 assert_eq!(an.d1.signum(), fd.d1.signum());
3257 assert_eq!(an.d2.signum(), fd.d2.signum());
3258 assert_eq!(an.d3.signum(), fd.d3.signum());
3259 assert!((an.mu - fd.mu).abs() < 1e-6);
3260 assert!((an.d1 - fd.d1).abs() < 1e-6);
3261 assert!((an.d2 - fd.d2).abs() < 1e-6);
3262 assert!((an.d3 - fd.d3).abs() < 1e-6);
3263 }
3264 }
3265
3266 #[test]
3267 fn mixture_second_partials_obey_equal_weight_two_component_identity() {
3268 let state = state_fromspec(&MixtureLinkSpec {
3269 components: vec![LinkComponent::Probit, LinkComponent::Logit],
3270 initial_rho: Array1::from_vec(vec![0.0]),
3271 })
3272 .expect("valid two-component mixture");
3273 let out = mixture_inverse_link_jetwith_rho_partials(&state, 0.37);
3274 assert_eq!(out.d2mu_drho2.dim(), (1, 1));
3279 assert_eq!(out.d2d1_drho2.dim(), (1, 1));
3280 assert_eq!(out.d2mu_drho2[[0, 0]], 0.0);
3281 assert_eq!(out.d2d1_drho2[[0, 0]], 0.0);
3282 }
3283
3284 #[test]
3285 fn sas_param_partials_matchfd() {
3286 let eta = 0.37;
3287 let epsilon = -0.12;
3288 let log_delta = 0.21;
3289 let out = sas_inverse_link_jetwith_param_partials(eta, epsilon, log_delta)
3290 .expect("finite SAS eta");
3291 let h = 1e-6;
3292
3293 let ep_p = sas_inverse_link_jet(eta, epsilon + h, log_delta).expect("finite SAS eta");
3294 let ep_m = sas_inverse_link_jet(eta, epsilon - h, log_delta).expect("finite SAS eta");
3295 let fd_ep = InverseLinkJet {
3296 mu: (ep_p.mu - ep_m.mu) / (2.0 * h),
3297 d1: (ep_p.d1 - ep_m.d1) / (2.0 * h),
3298 d2: (ep_p.d2 - ep_m.d2) / (2.0 * h),
3299 d3: (ep_p.d3 - ep_m.d3) / (2.0 * h),
3300 };
3301 assert_eq!(out.djet_depsilon.mu.signum(), fd_ep.mu.signum());
3302 assert_eq!(out.djet_depsilon.d1.signum(), fd_ep.d1.signum());
3303 assert_eq!(out.djet_depsilon.d2.signum(), fd_ep.d2.signum());
3304 assert_eq!(out.djet_depsilon.d3.signum(), fd_ep.d3.signum());
3305 assert!((out.djet_depsilon.mu - fd_ep.mu).abs() < 5e-5);
3306 assert!((out.djet_depsilon.d1 - fd_ep.d1).abs() < 5e-5);
3307 assert!((out.djet_depsilon.d2 - fd_ep.d2).abs() < 5e-5);
3308 assert!((out.djet_depsilon.d3 - fd_ep.d3).abs() < 5e-4);
3309
3310 let ld_p = sas_inverse_link_jet(eta, epsilon, log_delta + h).expect("finite SAS eta");
3311 let ld_m = sas_inverse_link_jet(eta, epsilon, log_delta - h).expect("finite SAS eta");
3312 let fd_ld = InverseLinkJet {
3313 mu: (ld_p.mu - ld_m.mu) / (2.0 * h),
3314 d1: (ld_p.d1 - ld_m.d1) / (2.0 * h),
3315 d2: (ld_p.d2 - ld_m.d2) / (2.0 * h),
3316 d3: (ld_p.d3 - ld_m.d3) / (2.0 * h),
3317 };
3318 assert_eq!(out.djet_dlog_delta.mu.signum(), fd_ld.mu.signum());
3319 assert_eq!(out.djet_dlog_delta.d1.signum(), fd_ld.d1.signum());
3320 assert_eq!(out.djet_dlog_delta.d2.signum(), fd_ld.d2.signum());
3321 assert_eq!(out.djet_dlog_delta.d3.signum(), fd_ld.d3.signum());
3322 assert!((out.djet_dlog_delta.mu - fd_ld.mu).abs() < 5e-5);
3323 assert!((out.djet_dlog_delta.d1 - fd_ld.d1).abs() < 5e-5);
3324 assert!((out.djet_dlog_delta.d2 - fd_ld.d2).abs() < 5e-5);
3325 assert!((out.djet_dlog_delta.d3 - fd_ld.d3).abs() < 5e-4);
3326 }
3327
3328 #[test]
3329 fn sas_second_partials_have_exact_center_identities() {
3330 let out = sas_inverse_link_jetwith_param_partials(0.0, 0.0, 0.0)
3331 .expect("finite SAS center");
3332 let phi0 = normal_pdf(0.0);
3333 assert_eq!(out.d2mu_dparams2, Array2::<f64>::zeros((2, 2)));
3341 assert_eq!(out.d2d1_dparams2[[0, 1]], out.d2d1_dparams2[[1, 0]]);
3342 assert_eq!(
3343 out.d2d1_dparams2[[0, 0]], 0.0,
3344 "ε–ε ∂²(μ') must be exactly zero at the identity-interior center"
3345 );
3346 assert!((out.d2d1_dparams2[[1, 1]] - phi0).abs() < 1.0e-15);
3347 assert_eq!(out.d2d1_dparams2[[0, 1]], 0.0);
3348 }
3349
3350 #[test]
3371 fn sas_family_score_depsilon_matches_fd_and_reproduces_profile_1876() {
3372 let h = 1e-6;
3373 let mu_at = |eta: f64, eps: f64, ld: f64| {
3374 sas_inverse_link_jet(eta, eps, ld)
3375 .expect("finite SAS eta")
3376 .mu
3377 };
3378 let dmu_deps = |eta: f64, eps: f64, ld: f64| {
3379 sas_inverse_link_jetwith_param_partials(eta, eps, ld)
3380 .expect("finite SAS eta")
3381 .djet_depsilon
3382 .mu
3383 };
3384 let row_ll = |y: f64, w: f64, mu: f64| w * (y * mu.ln() + (1.0 - y) * (1.0 - mu).ln());
3386 let a1 = |y: f64, w: f64, mu: f64| w * (y / mu - (1.0 - y) / (1.0 - mu));
3387
3388 let etas = [-1.0, -0.6, -0.2, 0.15, 0.5, 0.9];
3390 let epsilons = [-0.5, -0.2, 0.0, 0.3, 0.6];
3391 let log_deltas = [-0.3, 0.0, 0.4];
3392 for &eta in &etas {
3393 for &eps in &epsilons {
3394 for &ld in &log_deltas {
3395 let mu0 = mu_at(eta, eps, ld);
3396 if !(0.02..=0.98).contains(&mu0) {
3399 continue;
3400 }
3401 let dmu = dmu_deps(eta, eps, ld);
3402 for &y in &[0.0_f64, 1.0] {
3403 let analytic = a1(y, 1.0, mu0) * dmu; let fd = (row_ll(y, 1.0, mu_at(eta, eps + h, ld))
3405 - row_ll(y, 1.0, mu_at(eta, eps - h, ld)))
3406 / (2.0 * h);
3407 let scale = analytic.abs().max(fd.abs()).max(1.0);
3408 assert_eq!(
3409 analytic.signum(),
3410 fd.signum(),
3411 "∂ℓ/∂ε sign mismatch η={eta} ε={eps} log_δ={ld} y={y}: \
3412 analytic={analytic:e} fd={fd:e}"
3413 );
3414 assert!(
3415 (analytic - fd).abs() < 1e-5 * scale,
3416 "∂ℓ/∂ε magnitude mismatch η={eta} ε={eps} log_δ={ld} y={y}: \
3417 analytic={analytic:e} fd={fd:e}"
3418 );
3419 }
3420 }
3421 }
3422 }
3423
3424 let eps_true = 0.38;
3426 let ld_true = 0.0;
3427 let etas_ds: Vec<f64> = (0..21).map(|i| -1.0 + 0.1 * i as f64).collect();
3428 let y: Vec<f64> = etas_ds
3429 .iter()
3430 .map(|&e| mu_at(e, eps_true, ld_true))
3431 .collect();
3432
3433 let grad_nll = |eps: f64| -> (f64, f64) {
3435 let mut analytic = 0.0;
3436 let mut nll_p = 0.0;
3437 let mut nll_m = 0.0;
3438 for (i, &eta) in etas_ds.iter().enumerate() {
3439 let mu0 = mu_at(eta, eps, ld_true);
3440 analytic += -a1(y[i], 1.0, mu0) * dmu_deps(eta, eps, ld_true);
3441 nll_p += -row_ll(y[i], 1.0, mu_at(eta, eps + h, ld_true));
3442 nll_m += -row_ll(y[i], 1.0, mu_at(eta, eps - h, ld_true));
3443 }
3444 (analytic, (nll_p - nll_m) / (2.0 * h))
3445 };
3446
3447 for &eps in &[0.0, eps_true, 0.6] {
3449 let (analytic, fd) = grad_nll(eps);
3450 let scale = analytic.abs().max(fd.abs()).max(1.0);
3451 assert!(
3452 (analytic - fd).abs() < 1e-4 * scale,
3453 "summed ∂NLL/∂ε analytic≠fd at ε={eps}: {analytic:e} vs {fd:e}"
3454 );
3455 }
3456 let (g_below, _) = grad_nll(0.0);
3458 let (g_at, _) = grad_nll(eps_true);
3459 let (g_above, _) = grad_nll(0.6);
3460 assert!(
3461 g_below < -1.0,
3462 "expected strongly negative ∂NLL/∂ε below ε* (pushes ε up toward truth), got {g_below:e}"
3463 );
3464 assert!(
3465 g_at.abs() < 1e-6,
3466 "expected ≈0 ∂NLL/∂ε at the planted ε* (score identity), got {g_at:e}"
3467 );
3468 assert!(
3469 g_above > 1.0,
3470 "expected strongly positive ∂NLL/∂ε above ε*, got {g_above:e}"
3471 );
3472 assert!(
3473 g_below < g_at && g_at < g_above,
3474 "∂NLL/∂ε must strictly increase through ε* (distinguishes ±ε): \
3475 {g_below:e} < {g_at:e} < {g_above:e}"
3476 );
3477 }
3478
3479 #[test]
3480 fn sas_jet_extreme_inputs_stay_finite() {
3481 let cases = [
3482 (-1e6, 0.0, 0.0),
3483 (1e6, 0.0, 0.0),
3484 (3.0, 12.0, 12.0),
3485 (-3.0, -12.0, -12.0),
3486 (0.5, 40.0, 10.0),
3487 (0.5, -40.0, -10.0),
3488 ];
3489 for (eta, eps, log_delta) in cases {
3490 let j = sas_inverse_link_jet(eta, eps, log_delta).expect("finite SAS eta");
3491 assert!(j.mu.is_finite());
3492 assert!(j.d1.is_finite());
3493 assert!(j.d2.is_finite());
3494 assert!(j.d3.is_finite());
3495 let p = sas_inverse_link_jetwith_param_partials(eta, eps, log_delta)
3496 .expect("finite SAS eta");
3497 assert!(p.djet_depsilon.mu.is_finite());
3498 assert!(p.djet_depsilon.d1.is_finite());
3499 assert!(p.djet_depsilon.d2.is_finite());
3500 assert!(p.djet_depsilon.d3.is_finite());
3501 assert!(p.djet_dlog_delta.mu.is_finite());
3502 assert!(p.djet_dlog_delta.d1.is_finite());
3503 assert!(p.djet_dlog_delta.d2.is_finite());
3504 assert!(p.djet_dlog_delta.d3.is_finite());
3505 }
3506 }
3507
3508 #[test]
3509 fn sas_param_partials_remain_finite_in_extreme_region() {
3510 let eta = 10.0;
3511 let epsilon = -60.0;
3512 let log_delta = 40.0;
3513 let j = sas_inverse_link_jetwith_param_partials(eta, epsilon, log_delta)
3514 .expect("finite SAS eta");
3515 assert!(j.djet_depsilon.mu.is_finite());
3516 assert!(j.djet_depsilon.d1.is_finite());
3517 assert!(j.djet_depsilon.d2.is_finite());
3518 assert!(j.djet_depsilon.d3.is_finite());
3519 assert!(j.djet_dlog_delta.mu.is_finite());
3520 assert!(j.djet_dlog_delta.d1.is_finite());
3521 assert!(j.djet_dlog_delta.d2.is_finite());
3522 assert!(j.djet_dlog_delta.d3.is_finite());
3523 }
3524
3525 #[test]
3526 fn sas_eta_jets_matchfd() {
3527 let eta = -0.43;
3528 let epsilon = 0.27;
3529 let log_delta = -0.31;
3530 let h = 1e-5;
3531 let j0 = sas_inverse_link_jet(eta, epsilon, log_delta).expect("finite SAS eta");
3532 let jp = sas_inverse_link_jet(eta + h, epsilon, log_delta).expect("finite SAS eta");
3533 let jm = sas_inverse_link_jet(eta - h, epsilon, log_delta).expect("finite SAS eta");
3534 let d1fd = (jp.mu - jm.mu) / (2.0 * h);
3535 let d2fd = (jp.d1 - jm.d1) / (2.0 * h);
3536 let d3fd = (jp.d2 - jm.d2) / (2.0 * h);
3537 assert_eq!(j0.d1.signum(), d1fd.signum());
3538 assert_eq!(j0.d2.signum(), d2fd.signum());
3539 assert_eq!(j0.d3.signum(), d3fd.signum());
3540 assert!((j0.d1 - d1fd).abs() < 5e-5);
3541 assert!((j0.d2 - d2fd).abs() < 2e-4);
3542 assert!((j0.d3 - d3fd).abs() < 1e-3);
3543 }
3544
3545 #[test]
3546 fn family_dispatch_resolves_parameterized_links_from_spec() {
3547 let sas_state = sas_link_state_from_raw(0.0, 0.0).expect("sas state");
3551 let expected_sas =
3552 sas_inverse_link_jet(0.1, sas_state.epsilon, sas_state.log_delta).expect("direct SAS");
3553 let sas_spec = gam_problem::LikelihoodSpec {
3554 response: gam_problem::ResponseFamily::Binomial,
3555 link: InverseLink::Sas(sas_state),
3556 };
3557 let sas_jet = inverse_link_jet_for_family(&sas_spec, 0.1).expect("sas jet");
3558 assert_eq!(sas_jet.mu.to_bits(), expected_sas.mu.to_bits());
3559 assert_eq!(sas_jet.d1.to_bits(), expected_sas.d1.to_bits());
3560
3561 let mix_state = MixtureLinkState {
3562 components: vec![LinkComponent::Logit, LinkComponent::Probit],
3563 rho: ndarray::array![0.0],
3564 pi: ndarray::array![0.5, 0.5],
3565 };
3566 let expected_mix = mixture_inverse_link_jet(&mix_state, 0.1);
3567 let mix_spec = gam_problem::LikelihoodSpec {
3568 response: gam_problem::ResponseFamily::Binomial,
3569 link: InverseLink::Mixture(mix_state),
3570 };
3571 let mix_jet = inverse_link_jet_for_family(&mix_spec, 0.1).expect("mix jet");
3572 assert_eq!(mix_jet.mu.to_bits(), expected_mix.mu.to_bits());
3573 assert_eq!(mix_jet.d1.to_bits(), expected_mix.d1.to_bits());
3574 }
3575
3576 #[test]
3577 fn beta_logistic_reduces_to_logit_at_delta0_epsilon0() {
3578 let etas = [-40.0, -30.0, -5.0, 0.42, 5.0, 30.0, 40.0];
3579 for eta in etas {
3580 let j_bl = beta_logistic_inverse_link_jet(eta, 0.0, 0.0);
3581 let expected_mu = gam_linalg::utils::stable_logistic(eta);
3582 let expected_d1 = (-gam_linalg::utils::stable_softplus(-eta)
3583 - gam_linalg::utils::stable_softplus(eta))
3584 .exp();
3585 assert!(
3586 (j_bl.mu - expected_mu).abs() <= 1e-15 * expected_mu.abs().max(1.0),
3587 "mu mismatch at eta={eta}: got {}, expected {}",
3588 j_bl.mu,
3589 expected_mu
3590 );
3591 assert!(
3592 (j_bl.d1 - expected_d1).abs() <= 1e-12 * expected_d1.abs().max(f64::MIN_POSITIVE),
3593 "d1 mismatch at eta={eta}: got {}, expected {}",
3594 j_bl.d1,
3595 expected_d1
3596 );
3597 assert!(j_bl.d1 > 0.0, "d1 should stay positive at eta={eta}");
3598 }
3599
3600 let eta = 0.42;
3601 let j_bl = beta_logistic_inverse_link_jet(eta, 0.0, 0.0);
3602 let j_logit = component_inverse_link_jet(LinkComponent::Logit, eta);
3603 assert!((j_bl.d2 - j_logit.d2).abs() < 1e-10);
3604 assert!((j_bl.d3 - j_logit.d3).abs() < 1e-10);
3605 }
3606
3607 #[test]
3608 fn beta_logistic_eta_jets_matchfd() {
3609 let eta = -0.31;
3610 let delta = 0.27;
3611 let epsilon = -0.19;
3612 let h = 1e-5;
3613 let j0 = beta_logistic_inverse_link_jet(eta, delta, epsilon);
3614 let jp = beta_logistic_inverse_link_jet(eta + h, delta, epsilon);
3615 let jm = beta_logistic_inverse_link_jet(eta - h, delta, epsilon);
3616 let d1fd = (jp.mu - jm.mu) / (2.0 * h);
3617 let d2fd = (jp.d1 - jm.d1) / (2.0 * h);
3618 let d3fd = (jp.d2 - jm.d2) / (2.0 * h);
3619 assert_eq!(j0.d1.signum(), d1fd.signum());
3620 assert_eq!(j0.d2.signum(), d2fd.signum());
3621 assert_eq!(j0.d3.signum(), d3fd.signum());
3622 assert!((j0.d1 - d1fd).abs() < 5e-5);
3623 assert!((j0.d2 - d2fd).abs() < 5e-5);
3624 assert!((j0.d3 - d3fd).abs() < 2e-4);
3625 }
3626
3627 #[test]
3628 fn standard_kernel_structs_match_component_jets() {
3629 let eta = 0.73;
3630 assert_eq!(
3631 ProbitLinkKernel.jet(eta).expect("probit"),
3632 component_inverse_link_jet(LinkComponent::Probit, eta)
3633 );
3634 assert_eq!(
3635 LogitLinkKernel.jet(eta).expect("logit"),
3636 component_inverse_link_jet(LinkComponent::Logit, eta)
3637 );
3638 assert_eq!(
3639 CLogLogLinkKernel.jet(eta).expect("cloglog"),
3640 component_inverse_link_jet(LinkComponent::CLogLog, eta)
3641 );
3642 assert_eq!(
3643 LogLogLinkKernel.jet(eta).expect("loglog"),
3644 component_inverse_link_jet(LinkComponent::LogLog, eta)
3645 );
3646 assert_eq!(
3647 CauchitLinkKernel.jet(eta).expect("cauchit"),
3648 component_inverse_link_jet(LinkComponent::Cauchit, eta)
3649 );
3650 }
3651
3652 #[test]
3653 fn all_component_eta_jets_matchfd() {
3654 let components = [
3655 LinkComponent::Logit,
3656 LinkComponent::Probit,
3657 LinkComponent::CLogLog,
3658 LinkComponent::LogLog,
3659 LinkComponent::Cauchit,
3660 ];
3661 let points = [-3.0, -1.1, -0.2, 0.0, 0.7, 1.8, 3.2];
3662 let h = 1e-5;
3663 for c in components {
3664 for &eta in &points {
3665 let j0 = component_inverse_link_jet(c, eta);
3666 let jp = component_inverse_link_jet(c, eta + h);
3667 let jm = component_inverse_link_jet(c, eta - h);
3668 let d1fd = (jp.mu - jm.mu) / (2.0 * h);
3669 let d2fd = (jp.d1 - jm.d1) / (2.0 * h);
3670 let d3fd = (jp.d2 - jm.d2) / (2.0 * h);
3671 let d1_tol = if matches!(c, LinkComponent::CLogLog | LinkComponent::LogLog) {
3672 1.2e-4
3673 } else {
3674 5e-5
3675 };
3676 let d2_tol = if matches!(c, LinkComponent::CLogLog | LinkComponent::LogLog) {
3677 4e-4
3678 } else {
3679 1.2e-4
3680 };
3681 let d3_tol = if matches!(c, LinkComponent::CLogLog | LinkComponent::LogLog) {
3682 1.2e-3
3683 } else {
3684 4e-4
3685 };
3686 if j0.d1.abs().max(d1fd.abs()) > 1e-10 {
3687 assert_eq!(
3688 j0.d1.signum(),
3689 d1fd.signum(),
3690 "d1 sign mismatch for {c:?} eta={eta}"
3691 );
3692 }
3693 if j0.d2.abs().max(d2fd.abs()) > 1e-10 {
3694 assert_eq!(
3695 j0.d2.signum(),
3696 d2fd.signum(),
3697 "d2 sign mismatch for {c:?} eta={eta}: analytic={} fd={}",
3698 j0.d2,
3699 d2fd
3700 );
3701 }
3702 if j0.d3.abs().max(d3fd.abs()) > 1e-10 {
3703 assert_eq!(
3704 j0.d3.signum(),
3705 d3fd.signum(),
3706 "d3 sign mismatch for {c:?} eta={eta}"
3707 );
3708 }
3709 assert!(
3710 (j0.d1 - d1fd).abs() < d1_tol,
3711 "d1 mismatch for {c:?} eta={eta}: analytic={} fd={}",
3712 j0.d1,
3713 d1fd
3714 );
3715 assert!(
3716 (j0.d2 - d2fd).abs() < d2_tol,
3717 "d2 mismatch for {c:?} eta={eta}: analytic={} fd={}",
3718 j0.d2,
3719 d2fd
3720 );
3721 assert!(
3722 (j0.d3 - d3fd).abs() < d3_tol,
3723 "d3 mismatch for {c:?} eta={eta}: analytic={} fd={}",
3724 j0.d3,
3725 d3fd
3726 );
3727 }
3728 }
3729 }
3730
3731 #[test]
3732 fn sas_center_matches_probit_at_delta1_epsilon0() {
3733 let etas = [-3.0, -1.2, -0.3, 0.0, 0.4, 1.7, 3.0];
3736 for eta in etas {
3737 let sas = sas_inverse_link_jet(eta, 0.0, 0.0).expect("finite SAS eta");
3738 let probit = ProbitLinkKernel.jet(eta).expect("probit");
3739 assert_eq!(sas.mu.to_bits(), probit.mu.to_bits(), "mu at eta={eta}");
3740 assert_eq!(sas.d1.to_bits(), probit.d1.to_bits(), "d1 at eta={eta}");
3741 assert_eq!(sas.d2.to_bits(), probit.d2.to_bits(), "d2 at eta={eta}");
3742 assert_eq!(sas.d3.to_bits(), probit.d3.to_bits(), "d3 at eta={eta}");
3743 }
3744 }
3745
3746 #[test]
3757 fn sas_probit_reduction_is_exact_on_full_path_and_smooth_across_epsilon_zero() {
3758 let etas = [-3.0, -1.2, -0.3, 0.0, 0.4, 1.7, 3.0];
3759 for &eta in &etas {
3760 let probit = ProbitLinkKernel.jet(eta).expect("probit");
3761 for &eps in &[1e-11_f64, -1e-11] {
3765 let sas = sas_inverse_link_jet(eta, eps, 0.0).expect("finite SAS eta");
3766 assert!(
3769 (sas.mu - probit.mu).abs() < 1e-11,
3770 "full-path μ off probit at eta={eta} eps={eps}: {} vs {}",
3771 sas.mu,
3772 probit.mu
3773 );
3774 assert!(
3775 (sas.d1 - probit.d1).abs() < 1e-10,
3776 "full-path d1 off probit at eta={eta} eps={eps}"
3777 );
3778 }
3779 let center = sas_inverse_link_jet(eta, 0.0, 0.0).expect("fast path").mu;
3783 let lo = sas_inverse_link_jet(eta, -1e-11, 0.0).expect("full path").mu;
3784 let hi = sas_inverse_link_jet(eta, 1e-11, 0.0).expect("full path").mu;
3785 assert!(
3786 (hi - center).abs() < 1e-11 && (center - lo).abs() < 1e-11,
3787 "μ cliff across ε=0 at eta={eta}: lo={lo} center={center} hi={hi}"
3788 );
3789 }
3790 }
3791
3792 #[test]
3801 fn smooth_bound_jet_tower_is_c5_and_fd_exact() {
3802 let b = SAS_U_CLAMP;
3803 let a = SPLICE_INTERIOR_FRAC * b; let c = (2.0 - SPLICE_INTERIOR_FRAC) * b; let jet = |x: f64| smooth_bound_jet(x, b);
3806
3807 for &x in &[0.0, 0.5, 17.3, a - 1e-9, a] {
3809 let j = jet(x);
3810 assert_eq!(j.g, x, "interior identity value at x={x}");
3811 assert_eq!(j.d1, 1.0, "interior d1 at x={x}");
3812 assert_eq!((j.d2, j.d3, j.d4, j.d5), (0.0, 0.0, 0.0, 0.0));
3813 }
3814 for &x in &[c, c + 1e-9, 75.0, 1e12, f64::MAX] {
3816 let j = jet(x);
3817 assert_eq!(j.g, b, "saturation value at x={x}");
3818 assert_eq!((j.d1, j.d2, j.d3, j.d4, j.d5), (0.0, 0.0, 0.0, 0.0, 0.0));
3819 }
3820 assert_eq!(jet(c).g, b);
3822
3823 for &x in &[3.0, a - 2.0, 44.0, 50.0, 56.0, 100.0] {
3825 let p = jet(x);
3826 let m = jet(-x);
3827 assert_eq!(m.g, -p.g, "g odd at x={x}");
3828 assert_eq!(m.d1, p.d1, "d1 even at x={x}");
3829 assert_eq!(m.d2, -p.d2, "d2 odd at x={x}");
3830 assert_eq!(m.d3, p.d3, "d3 even at x={x}");
3831 assert_eq!(m.d4, -p.d4, "d4 odd at x={x}");
3832 assert_eq!(m.d5, p.d5, "d5 even at x={x}");
3833 }
3834
3835 for i in 0..=240 {
3837 let x = -80.0 + 160.0 * (i as f64) / 240.0;
3838 let j = jet(x);
3839 assert!((0.0..=1.0).contains(&j.d1), "d1 out of [0,1] at x={x}: {}", j.d1);
3840 assert!(j.g.abs() <= b + 1e-12, "|g| exceeds B at x={x}: {}", j.g);
3841 }
3842
3843 let h = 0.02;
3850 for &x0 in &[45.0, 47.5, 50.0, 52.5, 55.0, -47.5, -52.5] {
3851 let jp = jet(x0 + h);
3852 let jm = jet(x0 - h);
3853 let j0 = jet(x0);
3854 let fd = |hi: f64, lo: f64| (hi - lo) / (2.0 * h);
3855 let checks = [
3856 ("d1", j0.d1, fd(jp.g, jm.g), 2e-6),
3857 ("d2", j0.d2, fd(jp.d1, jm.d1), 2e-5),
3858 ("d3", j0.d3, fd(jp.d2, jm.d2), 2e-4),
3859 ("d4", j0.d4, fd(jp.d3, jm.d3), 2e-3),
3860 ("d5", j0.d5, fd(jp.d4, jm.d4), 2e-2),
3861 ];
3862 for (name, analytic, numeric, tol) in checks {
3863 assert!(
3864 (analytic - numeric).abs() < tol * (1.0 + analytic.abs()),
3865 "smooth_bound {name} FD mismatch at x={x0}: analytic={analytic:e} fd={numeric:e}"
3866 );
3867 }
3868 }
3869 }
3870
3871 #[test]
3885 fn sas_link_complement_mirrors_forward_map_across_finite_domain() {
3886 let params = [
3887 (0.0, 0.0), (0.25, -0.35), (0.0, -6.0), (-0.5, -6.0),
3891 (0.3, 6.0), ];
3893 let etas = [
3894 -f64::MAX,
3895 -1e160, -1e6,
3897 -3.0,
3898 -0.2,
3899 0.0,
3900 0.4,
3901 3.0,
3902 1e6,
3903 1e160,
3904 f64::MAX,
3905 ];
3906 for &(epsilon, log_delta) in ¶ms {
3907 for &eta in &etas {
3908 let mu = sas_inverse_link_jet(eta, epsilon, log_delta)
3909 .expect("finite SAS eta")
3910 .mu;
3911 let complement = sas_link_complement(eta, epsilon, log_delta, mu);
3912 assert!(
3913 complement.is_finite() && (0.0..=1.0).contains(&complement),
3914 "SAS complement out of [0,1] at η={eta} ε={epsilon} log_δ={log_delta}: {complement}"
3915 );
3916 let sum = mu + complement;
3919 assert!(
3920 (sum - 1.0).abs() < 1e-12,
3921 "SAS complement off the forward surface at η={eta} ε={epsilon} \
3922 log_δ={log_delta}: μ={mu} complement={complement} μ+comp={sum}"
3923 );
3924 }
3925 }
3926 }
3927
3928 #[test]
3929 fn beta_logistic_param_partials_matchfd() {
3930 let eta = -0.41;
3931 let delta = 0.23;
3932 let epsilon = -0.17;
3933 let out = beta_logistic_inverse_link_jetwith_param_partials(eta, delta, epsilon);
3934 let h = 1e-6;
3935
3936 let dp = beta_logistic_inverse_link_jet(eta, delta + h, epsilon);
3937 let dm = beta_logistic_inverse_link_jet(eta, delta - h, epsilon);
3938 let fd_delta = InverseLinkJet {
3939 mu: (dp.mu - dm.mu) / (2.0 * h),
3940 d1: (dp.d1 - dm.d1) / (2.0 * h),
3941 d2: (dp.d2 - dm.d2) / (2.0 * h),
3942 d3: (dp.d3 - dm.d3) / (2.0 * h),
3943 };
3944 assert_eq!(out.djet_dlog_delta.mu.signum(), fd_delta.mu.signum());
3945 assert_eq!(out.djet_dlog_delta.d1.signum(), fd_delta.d1.signum());
3946 assert_eq!(out.djet_dlog_delta.d2.signum(), fd_delta.d2.signum());
3947 assert_eq!(out.djet_dlog_delta.d3.signum(), fd_delta.d3.signum());
3948 assert!((out.djet_dlog_delta.mu - fd_delta.mu).abs() < 5e-5);
3949 assert!((out.djet_dlog_delta.d1 - fd_delta.d1).abs() < 5e-5);
3950 assert!((out.djet_dlog_delta.d2 - fd_delta.d2).abs() < 1.2e-4);
3951 assert!((out.djet_dlog_delta.d3 - fd_delta.d3).abs() < 4e-4);
3952
3953 let ep = beta_logistic_inverse_link_jet(eta, delta, epsilon + h);
3954 let em = beta_logistic_inverse_link_jet(eta, delta, epsilon - h);
3955 let fd_epsilon = InverseLinkJet {
3956 mu: (ep.mu - em.mu) / (2.0 * h),
3957 d1: (ep.d1 - em.d1) / (2.0 * h),
3958 d2: (ep.d2 - em.d2) / (2.0 * h),
3959 d3: (ep.d3 - em.d3) / (2.0 * h),
3960 };
3961 assert_eq!(out.djet_depsilon.mu.signum(), fd_epsilon.mu.signum());
3962 assert_eq!(out.djet_depsilon.d1.signum(), fd_epsilon.d1.signum());
3963 assert_eq!(out.djet_depsilon.d2.signum(), fd_epsilon.d2.signum());
3964 assert_eq!(out.djet_depsilon.d3.signum(), fd_epsilon.d3.signum());
3965 assert!((out.djet_depsilon.mu - fd_epsilon.mu).abs() < 5e-5);
3966 assert!((out.djet_depsilon.d1 - fd_epsilon.d1).abs() < 5e-5);
3967 assert!((out.djet_depsilon.d2 - fd_epsilon.d2).abs() < 1.2e-4);
3968 assert!((out.djet_depsilon.d3 - fd_epsilon.d3).abs() < 4e-4);
3969 }
3970
3971 #[test]
3972 fn beta_logistic_second_partials_obey_center_symmetry() {
3973 let out = beta_logistic_inverse_link_jetwith_param_partials(0.0, 0.37, 0.0);
3974 assert_eq!(out.d2mu_dparams2[[0, 1]], out.d2mu_dparams2[[1, 0]]);
3979 assert_eq!(out.d2d1_dparams2[[0, 1]], out.d2d1_dparams2[[1, 0]]);
3980 assert!(out.d2mu_dparams2[[0, 0]].abs() < 1.0e-12);
3981 assert!(out.d2mu_dparams2[[1, 1]].abs() < 1.0e-12);
3982 assert!(out.d2d1_dparams2[[0, 1]].abs() < 1.0e-12);
3983 }
3984
3985 #[test]
3986 fn beta_logistic_left_tail_uses_unclamped_log_space() {
3987 let eta = -40.0_f64;
3988 let delta = 0.2_f64;
3989 let epsilon = -0.1_f64;
3990 let a = (delta - epsilon).exp();
3991 let b = (delta + epsilon).exp();
3992 let expected_mu = beta_reg(a, b, eta.exp());
3993 let out = beta_logistic_inverse_link_jet(eta, delta, epsilon);
3994
3995 assert!(
3996 (out.mu - expected_mu).abs() <= 1e-12 * expected_mu.abs().max(f64::MIN_POSITIVE),
3997 "left-tail mu mismatch: got {}, expected {}",
3998 out.mu,
3999 expected_mu
4000 );
4001 assert!(out.d1 > 0.0);
4002 assert!(out.d2 > 0.0);
4003 assert!(out.d3 > 0.0);
4004 assert!(out.d1 < 1e-20);
4005
4006 let partials = beta_logistic_inverse_link_jetwith_param_partials(eta, delta, epsilon);
4007 assert!(partials.jet.d1 > 0.0);
4008 assert!(partials.jet.d2 > 0.0);
4009 assert!(partials.jet.d3 > 0.0);
4010 assert!(partials.djet_dlog_delta.d1.is_finite());
4011 assert!(partials.djet_depsilon.d1.is_finite());
4012 }
4013
4014 #[test]
4015 fn beta_logistic_mu_is_symmetric_in_logistic_tails() {
4016 let delta = 0.2;
4017 let epsilon = -0.35;
4018 let etas = [-40.0, -30.0, -5.0, -0.42, 0.0, 0.42, 5.0, 30.0, 40.0];
4019 for eta in etas {
4020 let left = beta_logistic_inverse_link_jet(eta, delta, epsilon).mu;
4021 let right = 1.0 - beta_logistic_inverse_link_jet(-eta, delta, -epsilon).mu;
4022 assert!(
4023 (left - right).abs() <= 1e-14,
4024 "symmetry mismatch at eta={eta}: left={left}, right={right}"
4025 );
4026 }
4027 }
4028
4029 #[test]
4030 fn inverse_link_pdfthird_derivative_matches_d3_finite_difference() {
4031 let sas = InverseLink::Sas(sas_link_state_from_raw(-0.25, 0.35).expect("sas state"));
4032 let beta_logistic = InverseLink::BetaLogistic(SasLinkState {
4033 epsilon: 0.18,
4034 log_delta: -0.22,
4035 delta: (-0.22_f64).exp(),
4036 });
4037 let mixture = InverseLink::Mixture(
4038 state_fromspec(&MixtureLinkSpec {
4039 components: vec![
4040 LinkComponent::Probit,
4041 LinkComponent::Logit,
4042 LinkComponent::CLogLog,
4043 LinkComponent::Cauchit,
4044 ],
4045 initial_rho: Array1::from_vec(vec![0.35, -0.45, 0.2]),
4046 })
4047 .expect("mixture state"),
4048 );
4049 let links = [
4050 InverseLink::Standard(StandardLink::Probit),
4051 InverseLink::Standard(StandardLink::Logit),
4052 InverseLink::Standard(StandardLink::CLogLog),
4053 sas,
4054 beta_logistic,
4055 mixture,
4056 ];
4057 let etas = [-1.1, -0.2, 0.6];
4058 let h = 1e-5;
4059
4060 for link in &links {
4061 for &eta in &etas {
4062 let jp = inverse_link_jet_for_inverse_link(link, eta + h).expect("jet+");
4063 let jm = inverse_link_jet_for_inverse_link(link, eta - h).expect("jet-");
4064 let d4fd = (jp.d3 - jm.d3) / (2.0 * h);
4065 let d4 = inverse_link_pdfthird_derivative_for_inverse_link(link, eta)
4066 .expect("analytic d4");
4067 assert_eq!(
4068 d4.signum(),
4069 d4fd.signum(),
4070 "d4 sign mismatch for {:?} at eta={eta}: analytic={} fd={}",
4071 link,
4072 d4,
4073 d4fd
4074 );
4075 assert!(
4076 (d4 - d4fd).abs() < 5e-3,
4077 "d4 mismatch for {:?} at eta={eta}: analytic={} fd={}",
4078 link,
4079 d4,
4080 d4fd
4081 );
4082 }
4083 }
4084 }
4085
4086 #[test]
4087 fn cloglog_large_finite_eta_should_saturate_without_nan_derivatives() {
4088 let eta = 800.0;
4089 let jet = component_inverse_link_jet(LinkComponent::CLogLog, eta);
4090 assert_eq!(jet.mu, 1.0);
4091 assert!(
4092 jet.d1 == 0.0,
4093 "for mu(eta)=1-exp(-exp(eta)), dmu/deta = exp(eta-exp(eta)) and should underflow to 0 at eta={eta}; got d1={}",
4094 jet.d1
4095 );
4096 assert!(
4097 jet.d2 == 0.0,
4098 "the saturated cloglog second derivative should also be 0 at eta={eta}; got d2={}",
4099 jet.d2
4100 );
4101 assert!(
4102 jet.d3 == 0.0,
4103 "the saturated cloglog third derivative should also be 0 at eta={eta}; got d3={}",
4104 jet.d3
4105 );
4106
4107 let d4 = inverse_link_pdfthird_derivative_for_inverse_link(
4108 &InverseLink::Standard(StandardLink::CLogLog),
4109 eta,
4110 )
4111 .expect("cloglog d4");
4112 assert!(
4113 d4 == 0.0,
4114 "the saturated cloglog fourth derivative should also be 0 at eta={eta}; got d4={d4}"
4115 );
4116 }
4117
4118 #[test]
4119 fn loglog_large_negative_finite_eta_should_saturate_without_nan_derivatives() {
4120 let eta = -800.0;
4121 let jet = component_inverse_link_jet(LinkComponent::LogLog, eta);
4122 assert_eq!(jet.mu, 0.0);
4123 assert!(
4124 jet.d1 == 0.0,
4125 "for mu(eta)=exp(-exp(-eta)), dmu/deta = exp(-eta-exp(-eta)) and should underflow to 0 at eta={eta}; got d1={}",
4126 jet.d1
4127 );
4128 assert!(
4129 jet.d2 == 0.0,
4130 "the saturated loglog second derivative should also be 0 at eta={eta}; got d2={}",
4131 jet.d2
4132 );
4133 assert!(
4134 jet.d3 == 0.0,
4135 "the saturated loglog third derivative should also be 0 at eta={eta}; got d3={}",
4136 jet.d3
4137 );
4138
4139 let d4 = inverse_link_pdfthird_derivative_for_inverse_link(
4140 &InverseLink::Mixture(
4141 state_fromspec(&MixtureLinkSpec {
4142 components: vec![LinkComponent::LogLog, LinkComponent::Probit],
4143 initial_rho: Array1::from_vec(vec![12.0]),
4144 })
4145 .expect("mixture state"),
4146 ),
4147 eta,
4148 )
4149 .expect("loglog mixture d4");
4150 assert!(
4151 d4.is_finite(),
4152 "even a nearly pure loglog mixture should not produce NaN fourth derivatives at eta={eta}; got d4={d4}"
4153 );
4154 }
4155
4156 #[test]
4157 fn logit_tail_derivatives_should_match_stable_closed_forms() {
4158 let eta = 50.0_f64;
4159 let z = (-eta).exp();
4160 let denom = 1.0_f64 + z;
4161 let stable_d1 = z / denom.powi(2);
4162 let stable_d2 = z * (z - 1.0) / denom.powi(3);
4163 let stable_d3 = z * (z * z - 4.0 * z + 1.0) / denom.powi(4);
4164 let stable_d4 = z * (z * z * z - 11.0 * z * z + 11.0 * z - 1.0) / denom.powi(5);
4165 let stable_d5 =
4166 z * (z * z * z * z - 26.0 * z * z * z + 66.0 * z * z - 26.0 * z + 1.0) / denom.powi(6);
4167
4168 assert!(stable_d1 > 0.0);
4169 assert!(stable_d2 < 0.0);
4170 assert!(stable_d3 > 0.0);
4171 assert!(stable_d4 < 0.0);
4172 assert!(stable_d5 > 0.0);
4173
4174 let jet = component_inverse_link_jet(LinkComponent::Logit, eta);
4175 assert!(
4176 (jet.d1 - stable_d1).abs() < 1e-30,
4177 "logit d1 should equal the stable tail formula z/(1+z)^2 at eta={eta}; got {} vs {}",
4178 jet.d1,
4179 stable_d1
4180 );
4181 assert!(
4182 (jet.d2 - stable_d2).abs() < 1e-30,
4183 "logit d2 should equal the stable tail formula z(z-1)/(1+z)^3 at eta={eta}; got {} vs {}",
4184 jet.d2,
4185 stable_d2
4186 );
4187 assert!(
4188 (jet.d3 - stable_d3).abs() < 1e-30,
4189 "logit d3 should equal the stable tail formula z(z^2-4z+1)/(1+z)^4 at eta={eta}; got {} vs {}",
4190 jet.d3,
4191 stable_d3
4192 );
4193
4194 let d4 = inverse_link_pdfthird_derivative_for_inverse_link(
4195 &InverseLink::Standard(StandardLink::Logit),
4196 eta,
4197 )
4198 .expect("logit d4");
4199 assert!(
4200 (d4 - stable_d4).abs() < 1e-30,
4201 "logit d4 should equal the stable tail formula z(z^3-11z^2+11z-1)/(1+z)^5 at eta={eta}; got {} vs {}",
4202 d4,
4203 stable_d4
4204 );
4205
4206 let d5 = inverse_link_pdffourth_derivative_for_inverse_link(
4207 &InverseLink::Standard(StandardLink::Logit),
4208 eta,
4209 )
4210 .expect("logit d5");
4211 assert!(
4212 (d5 - stable_d5).abs() < 1e-30,
4213 "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 {}",
4214 d5,
4215 stable_d5
4216 );
4217 }
4218
4219 #[test]
4220 fn cloglog_negative_tail_value_should_match_expm1_form() {
4221 let eta = -50.0_f64;
4222 let t = eta.exp();
4223 let stable_mu = -(-t).exp_m1();
4224 assert!(stable_mu > 0.0);
4225
4226 let jet = component_inverse_link_jet(LinkComponent::CLogLog, eta);
4227 assert!(
4228 (jet.mu - stable_mu).abs() < 1e-30,
4229 "cloglog mu should equal -expm1(-exp(eta)) in the negative tail at eta={eta}; got {} vs {}",
4230 jet.mu,
4231 stable_mu
4232 );
4233 }
4234
4235 #[test]
4236 fn non_logit_probit_fisher_weight_jets_match_finite_differences() {
4237 fn rel_err(a: f64, b: f64) -> f64 {
4238 (a - b).abs() / a.abs().max(b.abs()).max(1.0e-8)
4239 }
4240
4241 let cases = [
4242 (LinkComponent::CLogLog, [-3.0_f64, -0.5, 0.4, 1.5]),
4243 (LinkComponent::LogLog, [-1.5_f64, -0.4, 0.5, 3.0]),
4244 (LinkComponent::Cauchit, [-3.0_f64, -0.7, 0.6, 3.0]),
4245 ];
4246 for (component, etas) in cases {
4247 for eta in etas {
4248 let (w, w1, w2, w3, w4) = component_fisher_weight_jet5(component, eta);
4249 let jet = component_inverse_link_jet(component, eta);
4250 let expected = jet.d1 * jet.d1 / (jet.mu * (1.0 - jet.mu));
4251 assert!(
4252 rel_err(w, expected) < 1.0e-12,
4253 "{component:?} Fisher weight mismatch at eta={eta}: got {w}, expected {expected}"
4254 );
4255
4256 let h = 1.0e-4;
4257 let fd1 = (component_fisher_weight_jet5(component, eta + h).0
4258 - component_fisher_weight_jet5(component, eta - h).0)
4259 / (2.0 * h);
4260 let fd2 = (component_fisher_weight_jet5(component, eta + h).1
4261 - component_fisher_weight_jet5(component, eta - h).1)
4262 / (2.0 * h);
4263 let fd3 = (component_fisher_weight_jet5(component, eta + h).2
4264 - component_fisher_weight_jet5(component, eta - h).2)
4265 / (2.0 * h);
4266 let fd4 = (component_fisher_weight_jet5(component, eta + h).3
4267 - component_fisher_weight_jet5(component, eta - h).3)
4268 / (2.0 * h);
4269
4270 assert!(
4271 rel_err(w1, fd1) < 1.0e-5,
4272 "{component:?} W' mismatch at eta={eta}: {w1} vs {fd1}"
4273 );
4274 assert!(
4275 rel_err(w2, fd2) < 1.0e-5,
4276 "{component:?} W'' mismatch at eta={eta}: {w2} vs {fd2}"
4277 );
4278 assert!(
4279 rel_err(w3, fd3) < 5.0e-5,
4280 "{component:?} W''' mismatch at eta={eta}: {w3} vs {fd3}"
4281 );
4282 assert!(
4283 rel_err(w4, fd4) < 5.0e-4,
4284 "{component:?} W'''' mismatch at eta={eta}: {w4} vs {fd4}"
4285 );
4286 }
4287 }
4288 }
4289
4290 #[test]
4291 fn mixture_fisher_weight_jet_covers_loglog_and_cauchit_components() {
4292 let state = state_fromspec(&MixtureLinkSpec {
4293 components: vec![
4294 LinkComponent::CLogLog,
4295 LinkComponent::LogLog,
4296 LinkComponent::Cauchit,
4297 ],
4298 initial_rho: Array1::from_vec(vec![0.3, -0.2]),
4299 })
4300 .expect("mixture state");
4301 let link = InverseLink::Mixture(state);
4302 assert!(
4303 link.has_fisher_weight_jet(),
4304 "anchored mixtures with loglog/cauchit components must remain eligible for Firth"
4305 );
4306 assert!(
4307 LikelihoodSpec::new(ResponseFamily::Binomial, link.clone()).supports_firth(),
4308 "Firth support should use the mixture inverse-link Fisher jet, not standalone LinkFunction coverage"
4309 );
4310
4311 for eta in [-2.0_f64, -0.25, 0.75, 2.5] {
4312 let (w, w1, w2, w3, w4) =
4313 fisher_weight_jet5_for_inverse_link(&link, eta).expect("mixture Fisher jet");
4314 for value in [w, w1, w2, w3, w4] {
4315 assert!(
4316 value.is_finite(),
4317 "mixture Fisher weight jet should be finite at eta={eta}; got {value}"
4318 );
4319 }
4320 assert!(
4321 w > 0.0,
4322 "mixture Fisher working weight should be positive away from saturated tails at eta={eta}; got {w}"
4323 );
4324 }
4325 }
4326
4327 #[test]
4328 fn loglog_fifth_derivative_should_match_closed_form_sign() {
4329 let eta = 0.0_f64;
4330 let r = (-eta).exp();
4331 let expected =
4332 (-r).exp() * (r - 15.0 * r * r + 25.0 * r.powi(3) - 10.0 * r.powi(4) + r.powi(5));
4333 let d5 = component_inverse_link_pdffourth_derivative(LinkComponent::LogLog, eta);
4334 assert!(
4335 (d5 - expected).abs() < 1e-15,
4336 "loglog d5 should equal exp(-r) * (r - 15r^2 + 25r^3 - 10r^4 + r^5) at eta={eta}; got {d5} vs {expected}"
4337 );
4338 assert!(d5 > 0.0, "loglog d5 should be positive at eta=0; got {d5}");
4339 }
4340}