const INITIAL_POSITIVE_FLOOR: f64 = 1.0e-12;
const INITIAL_PROBABILITY_FLOOR: f64 = 1.0e-12;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct Identity;
impl Link<f64> for Identity {
#[inline]
fn inverse(eta: f64) -> f64 {
eta
}
#[inline]
fn derivative_inverse(_: f64) -> f64 {
1.0
}
}
impl InitialEtaFromTheta<f64> for Identity {
#[inline]
fn initial_eta_from_theta(theta: f64) -> f64 {
theta
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct Log;
impl Link<f64> for Log {
#[inline]
fn inverse(eta: f64) -> f64 {
eta.exp()
}
#[inline]
fn derivative_inverse(eta: f64) -> f64 {
eta.exp()
}
}
impl PositiveLink<f64> for Log {}
impl InitialEtaFromTheta<f64> for Log {
#[inline]
fn initial_eta_from_theta(theta: f64) -> f64 {
theta.max(INITIAL_POSITIVE_FLOOR).ln()
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct Softplus;
impl Link<f64> for Softplus {
#[inline]
fn inverse(eta: f64) -> f64 {
if eta > 30.0 {
eta
} else if eta < -30.0 {
eta.exp()
} else {
eta.exp().ln_1p()
}
}
#[inline]
fn derivative_inverse(eta: f64) -> f64 {
if eta >= 0.0 {
1.0 / (1.0 + (-eta).exp())
} else {
let exp_eta = eta.exp();
exp_eta / (1.0 + exp_eta)
}
}
}
impl PositiveLink<f64> for Softplus {}
impl InitialEtaFromTheta<f64> for Softplus {
#[inline]
fn initial_eta_from_theta(theta: f64) -> f64 {
let theta = theta.max(INITIAL_POSITIVE_FLOOR);
if theta > 30.0 {
theta
} else {
theta.exp_m1().max(INITIAL_POSITIVE_FLOOR).ln()
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct Logit;
impl Link<f64> for Logit {
#[inline]
fn inverse(eta: f64) -> f64 {
if eta >= 0.0 {
let z = (-eta).exp();
1.0 / (1.0 + z)
} else {
let z = eta.exp();
z / (1.0 + z)
}
}
#[inline]
fn derivative_inverse(eta: f64) -> f64 {
let p = Self::inverse(eta);
p * (1.0 - p)
}
}
impl UnitIntervalLink<f64> for Logit {}
impl InitialEtaFromTheta<f64> for Logit {
#[inline]
fn initial_eta_from_theta(theta: f64) -> f64 {
let probability = theta.clamp(INITIAL_PROBABILITY_FLOOR, 1.0 - INITIAL_PROBABILITY_FLOOR);
probability.ln() - (1.0 - probability).ln()
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct LogPlus<const OFFSET: i64>;
impl<const OFFSET: i64> Link<f64> for LogPlus<OFFSET> {
#[inline]
#[allow(clippy::cast_precision_loss)]
fn inverse(eta: f64) -> f64 {
OFFSET as f64 + eta.exp()
}
#[inline]
fn derivative_inverse(eta: f64) -> f64 {
eta.exp()
}
}
impl<const OFFSET: i64> InitialEtaFromTheta<f64> for LogPlus<OFFSET> {
#[inline]
#[allow(clippy::cast_precision_loss)]
fn initial_eta_from_theta(theta: f64) -> f64 {
(theta - OFFSET as f64).max(INITIAL_POSITIVE_FLOOR).ln()
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct ClampedLog<const MIN: i64, const MAX: i64>;
impl<const MIN: i64, const MAX: i64> Link<f64> for ClampedLog<MIN, MAX> {
#[inline]
#[allow(clippy::cast_precision_loss)]
fn inverse(eta: f64) -> f64 {
let min = MIN as f64;
let max = MAX as f64;
debug_assert!(min <= max);
if eta < min {
min.exp()
} else if eta > max {
max.exp()
} else {
eta.exp()
}
}
#[inline]
#[allow(clippy::cast_precision_loss)]
fn derivative_inverse(eta: f64) -> f64 {
let min = MIN as f64;
let max = MAX as f64;
debug_assert!(min <= max);
if (min..=max).contains(&eta) {
eta.exp()
} else {
0.0
}
}
}
impl<const MIN: i64, const MAX: i64> PositiveLink<f64> for ClampedLog<MIN, MAX> {}
impl<const MIN: i64, const MAX: i64> InitialEtaFromTheta<f64> for ClampedLog<MIN, MAX> {
#[inline]
#[allow(clippy::cast_precision_loss)]
fn initial_eta_from_theta(theta: f64) -> f64 {
let min = MIN as f64;
let max = MAX as f64;
debug_assert!(min <= max);
theta.max(INITIAL_POSITIVE_FLOOR).ln().clamp(min, max)
}
}
pub trait Link<S> {
fn inverse(eta: S) -> S;
fn derivative_inverse(eta: S) -> S;
}
pub trait InitialEtaFromTheta<S>: Link<S> {
fn initial_eta_from_theta(theta: S) -> S;
}
pub trait PositiveLink<S>: Link<S> {}
pub trait UnitIntervalLink<S>: Link<S> {}
#[cfg(test)]
mod tests {
use approx::assert_relative_eq;
use crate::{ClampedLog, InitialEtaFromTheta, Link, Log, LogPlus, Logit, Softplus};
#[test]
#[allow(clippy::float_cmp)]
fn clamped_log_clamps_value_and_derivative() {
type LinkUnderTest = ClampedLog<-2, 2>;
assert_relative_eq!(LinkUnderTest::inverse(-3.0), (-2.0_f64).exp());
assert_relative_eq!(LinkUnderTest::inverse(1.0), 1.0_f64.exp());
assert_relative_eq!(LinkUnderTest::inverse(3.0), 2.0_f64.exp());
assert_eq!(LinkUnderTest::derivative_inverse(-3.0), 0.0);
assert_relative_eq!(LinkUnderTest::derivative_inverse(1.0), 1.0_f64.exp());
assert_eq!(LinkUnderTest::derivative_inverse(3.0), 0.0);
}
#[test]
#[allow(clippy::float_cmp, clippy::items_after_statements)]
fn initial_eta_from_theta_guards_link_boundaries() {
assert!(Log::initial_eta_from_theta(0.0).is_finite());
assert!(Softplus::initial_eta_from_theta(1.0e-300).is_finite());
assert_relative_eq!(
Softplus::inverse(Softplus::initial_eta_from_theta(3.0)),
3.0
);
let low_logit = Logit::initial_eta_from_theta(0.0);
let high_logit = Logit::initial_eta_from_theta(1.0);
assert!(low_logit.is_finite());
assert!(high_logit.is_finite());
assert!(low_logit < 0.0);
assert!(high_logit > 0.0);
type Shifted = LogPlus<2>;
assert!(Shifted::initial_eta_from_theta(2.0).is_finite());
type Clamped = ClampedLog<-2, 2>;
assert_eq!(Clamped::initial_eta_from_theta(1.0e-20), -2.0);
assert_eq!(Clamped::initial_eta_from_theta(1.0e20), 2.0);
}
}