use candle_core::{Result, Tensor};
use crate::candle::sgvb::BlackBoxLikelihood;
pub fn lgamma_approx(x: &Tensor) -> Result<Tensor> {
let x_safe = x.clamp(1e-6f32, f32::MAX)?;
let x_plus_1 = (&x_safe + 1.0)?;
let x_plus_2 = (&x_safe + 2.0)?;
let product = ((&x_safe * &x_plus_1)? * &x_plus_2)?;
let logterm = product.log()?;
let xp3 = (&x_safe + 3.0)?;
let log_xp3 = xp3.log()?;
let recip_term = (xp3.recip()? * 0.0833333)?;
let mult_term = ((&x_safe + 2.5)? * &log_xp3)?;
(((recip_term - 2.081061466)? - &x_safe)? - &logterm)? + &mult_term
}
pub struct NegativeBinomialLikelihood {
y: Tensor,
}
impl NegativeBinomialLikelihood {
pub fn new(y: Tensor) -> Self {
Self { y }
}
}
impl BlackBoxLikelihood for NegativeBinomialLikelihood {
fn log_likelihood(&self, etas: &[&Tensor]) -> Result<Tensor> {
assert!(
etas.len() >= 2,
"NegativeBinomialLikelihood requires 2 etas (log_mean, log_dispersion)"
);
let log_mu_raw = etas[0]; let log_r_raw = etas[1];
let log_mu = log_mu_raw.clamp(-10.0, 10.0)?;
let log_r = log_r_raw.clamp(-10.0, 10.0)?;
let mu = log_mu.exp()?;
let r = log_r.exp()?;
let y_plus_r = self.y.broadcast_add(&r)?;
let r_plus_mu = (&r + &mu)?;
let lgamma_y_plus_r = lgamma_approx(&y_plus_r)?;
let lgamma_r = lgamma_approx(&r)?;
let y_plus_1 = (&self.y + 1.0)?;
let lgamma_y_plus_1 = lgamma_approx(&y_plus_1)?;
let r_log_r = (&r * &log_r)?;
let y_log_mu = self.y.broadcast_mul(&log_mu)?;
let r_plus_y_log_r_plus_mu = (&y_plus_r * r_plus_mu.log()?)?;
let log_prob = lgamma_y_plus_r
.broadcast_sub(&lgamma_r)?
.broadcast_sub(&lgamma_y_plus_1)?
.broadcast_add(&r_log_r)?
.broadcast_add(&y_log_mu)?
.broadcast_sub(&r_plus_y_log_r_plus_mu)?;
log_prob.sum(2)?.sum(1)
}
}
#[cfg(test)]
mod tests {
use super::*;
use candle_core::Device;
#[test]
fn test_negbinom_likelihood() -> Result<()> {
let device = Device::Cpu;
let y = Tensor::from_vec(vec![1.0f32, 2.0, 5.0], (3, 1), &device)?;
let log_mu = Tensor::from_vec(vec![0.0f32, 0.5, 1.5], (1, 3, 1), &device)?;
let log_r = Tensor::ones((1, 3, 1), candle_core::DType::F32, &device)?;
let likelihood = NegativeBinomialLikelihood::new(y.clone());
let log_lik = likelihood.log_likelihood(&[&log_mu, &log_r])?;
let val: f32 = log_lik.get(0)?.to_scalar()?;
assert!(val.is_finite());
println!("NegBinom log_lik: {}", val);
Ok(())
}
#[test]
fn test_lgamma_approx() -> Result<()> {
let device = Device::Cpu;
let x = Tensor::from_vec(vec![1.0f32, 2.0, 5.0, 10.0], (4,), &device)?;
let lg = lgamma_approx(&x)?;
let vals: Vec<f32> = lg.to_vec1()?;
println!("lgamma approx: {:?}", vals);
assert!((vals[0] - 0.0).abs() < 0.1);
assert!((vals[1] - 0.0).abs() < 0.1);
assert!((vals[2] - 3.178).abs() < 0.2);
assert!((vals[3] - 12.802).abs() < 0.5);
Ok(())
}
}