use crate::data::error_model::AssayErrorModels;
use crate::data::event::Observation;
use crate::{Censor, ErrorPoly, PharmsolError};
use super::distributions::{lognormccdf, lognormcdf, lognormpdf};
#[derive(Debug, Clone)]
pub struct Prediction {
pub(crate) time: f64,
pub(crate) observation: Option<f64>,
pub(crate) prediction: f64,
pub(crate) outeq: usize,
pub(crate) errorpoly: Option<ErrorPoly>,
pub(crate) state: Vec<f64>,
pub(crate) occasion: usize,
pub(crate) censoring: Censor,
}
impl Prediction {
pub fn time(&self) -> f64 {
self.time
}
pub fn observation(&self) -> Option<f64> {
self.observation
}
pub fn prediction(&self) -> f64 {
self.prediction
}
pub(crate) fn set_prediction(&mut self, prediction: f64) {
self.prediction = prediction;
}
pub fn outeq(&self) -> usize {
self.outeq
}
pub fn errorpoly(&self) -> Option<ErrorPoly> {
self.errorpoly
}
pub fn prediction_error(&self) -> Option<f64> {
self.observation.map(|obs| self.prediction - obs)
}
pub fn percentage_error(&self) -> Option<f64> {
self.observation
.map(|obs| ((self.prediction - obs) / obs) * 100.0)
}
pub fn absolute_error(&self) -> Option<f64> {
self.observation.map(|obs| (self.prediction - obs).abs())
}
pub fn squared_error(&self) -> Option<f64> {
self.observation.map(|obs| (self.prediction - obs).powi(2))
}
#[inline]
pub fn log_likelihood(&self, error_models: &AssayErrorModels) -> Result<f64, PharmsolError> {
let obs = match self.observation {
Some(obs) => obs,
None => return Ok(0.0),
};
let sigma = error_models.sigma(self)?;
let log_lik = match self.censoring {
Censor::None => lognormpdf(obs, self.prediction, sigma),
Censor::BLOQ => lognormcdf(obs, self.prediction, sigma)?,
Censor::ALOQ => lognormccdf(obs, self.prediction, sigma)?,
};
if log_lik.is_finite() {
Ok(log_lik)
} else {
Err(PharmsolError::NonFiniteLikelihood(log_lik))
}
}
#[deprecated(
since = "0.23.0",
note = "Use log_likelihood() instead for better numerical stability"
)]
pub fn likelihood(&self, error_models: &AssayErrorModels) -> Result<f64, PharmsolError> {
let log_lik = self.log_likelihood(error_models)?;
let lik = log_lik.exp();
if lik.is_finite() {
Ok(lik)
} else if lik == 0.0 {
Err(PharmsolError::ZeroLikelihood)
} else {
Err(PharmsolError::NonFiniteLikelihood(lik))
}
}
pub fn state(&self) -> &[f64] {
&self.state
}
pub fn occasion(&self) -> usize {
self.occasion
}
pub fn mut_occasion(&mut self) -> &mut usize {
&mut self.occasion
}
pub fn censoring(&self) -> Censor {
self.censoring
}
pub fn to_observation(&self) -> Observation {
Observation::new(
self.time,
self.observation,
self.outeq,
self.errorpoly,
self.occasion,
self.censoring,
)
}
}
impl Default for Prediction {
fn default() -> Self {
Self {
time: 0.0,
observation: None,
prediction: 0.0,
outeq: 0,
errorpoly: None,
state: vec![],
occasion: 0,
censoring: Censor::None,
}
}
}
impl std::fmt::Display for Prediction {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let obs_str = match self.observation {
Some(obs) => format!("{:.4}", obs),
None => "NA".to_string(),
};
write!(
f,
"Time: {:.2}\tObs: {:.4}\tPred: {:.4}\tOuteq: {}",
self.time, obs_str, self.prediction, self.outeq
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::data::error_model::{AssayErrorModel, ErrorPoly};
fn create_test_prediction(obs: f64, pred: f64) -> Prediction {
Prediction {
time: 1.0,
observation: Some(obs),
prediction: pred,
outeq: 0,
errorpoly: None,
state: vec![pred],
occasion: 0,
censoring: Censor::None,
}
}
fn create_error_models() -> AssayErrorModels {
AssayErrorModels::new()
.add(
0,
AssayErrorModel::additive(ErrorPoly::new(1.0, 0.0, 0.0, 0.0), 0.0),
)
.unwrap()
}
#[test]
fn test_log_likelihood_basic() {
let prediction = create_test_prediction(10.0, 10.5);
let error_models = create_error_models();
let log_lik = prediction.log_likelihood(&error_models).unwrap();
assert!(log_lik.is_finite());
assert!(log_lik < 0.0); }
#[test]
fn test_log_likelihood_numerical_stability() {
let prediction = create_test_prediction(10.0, 30.0); let error_models = create_error_models();
let log_lik = prediction.log_likelihood(&error_models).unwrap();
assert!(log_lik.is_finite());
assert!(log_lik < -100.0); }
#[test]
fn test_log_likelihood_extreme() {
let prediction = create_test_prediction(10.0, 50.0); let error_models = create_error_models();
let log_lik = prediction.log_likelihood(&error_models).unwrap();
assert!(log_lik.is_finite());
assert!(
log_lik < -700.0 && log_lik > -900.0,
"log_lik ({}) should be approximately -800",
log_lik
);
}
#[test]
fn test_missing_observation_returns_zero() {
let prediction = Prediction {
time: 1.0,
observation: None,
prediction: 10.0,
..Default::default()
};
let error_models = create_error_models();
let result = prediction.log_likelihood(&error_models).unwrap();
assert_eq!(
result, 0.0,
"Missing observation should contribute 0 to log-likelihood"
);
}
#[test]
fn test_error_metrics() {
let prediction = create_test_prediction(10.0, 12.0);
assert_eq!(prediction.prediction_error(), Some(2.0));
assert_eq!(prediction.absolute_error(), Some(2.0));
assert_eq!(prediction.squared_error(), Some(4.0));
assert_eq!(prediction.percentage_error(), Some(20.0));
}
}