use serde::{Deserialize, Serialize};
use crate::error::{Error, Result};
use crate::model::domain::Domain;
use crate::time::SECONDS_PER_DAY;
pub const MAX_SOFT_RELIABILITY: f64 = 0.9999;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Source {
pub name: String,
pub reliability: f64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub half_life_days: Option<f64>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub axiomatic: bool,
}
impl Source {
pub fn reliability_at(&self, age_days: f64) -> f64 {
let mut r = self.reliability;
if !self.axiomatic {
r = r.min(MAX_SOFT_RELIABILITY);
}
if let Some(hl) = self.half_life_days {
if age_days > 0.0 && hl > 0.0 {
r *= 0.5_f64.powf(age_days / hl);
}
}
r.max(0.0)
}
pub fn validate(&self) -> Result<()> {
if self.name.is_empty() {
return Err(Error::Invalid("source needs a non-empty name".into()));
}
if !(self.reliability > 0.0 && self.reliability <= 1.0) {
return Err(Error::Invalid(format!(
"source {:?}: reliability must be in (0, 1]",
self.name
)));
}
if let Some(hl) = self.half_life_days {
if !hl.is_finite() || hl <= 0.0 {
return Err(Error::Invalid(format!(
"source {:?}: half_life_days must be > 0",
self.name
)));
}
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Claim {
Interval { slot: String, lo: f64, hi: f64 },
Value { slot: String, value: String },
NotValue { slot: String, value: String },
}
impl Claim {
pub fn slot(&self) -> &str {
match self {
Claim::Interval { slot, .. }
| Claim::Value { slot, .. }
| Claim::NotValue { slot, .. } => slot,
}
}
pub fn validate(&self, domain: &Domain) -> Result<()> {
match (self, domain) {
(
Claim::Interval { slot, lo, hi },
Domain::Continuous {
lo: dlo, hi: dhi, ..
},
) => {
if lo > hi {
return Err(Error::Invalid(format!("claim on {slot:?}: lo > hi")));
}
if hi < dlo || lo > dhi {
return Err(Error::Invalid(format!(
"claim on {slot:?}: [{lo}, {hi}] does not intersect domain [{dlo}, {dhi}]"
)));
}
Ok(())
}
(Claim::Interval { slot, .. }, Domain::Categorical { .. }) => Err(Error::Invalid(
format!("interval claim on categorical slot {slot:?}"),
)),
(Claim::Value { value, .. }, d) | (Claim::NotValue { value, .. }, d) => {
d.index_of_value(value).map(|_| ())
}
}
}
pub fn likelihood(&self, domain: &Domain, r: f64) -> Vec<f64> {
let n = domain.n();
match (self, domain) {
(
Claim::Interval { lo, hi, .. },
Domain::Continuous {
lo: dlo, hi: dhi, ..
},
) => {
let min_w = domain.bin_width();
let c_lo = *lo;
let c_hi = hi.max(lo + min_w);
let claim_w = c_hi - c_lo;
let domain_w = dhi - dlo;
(0..n)
.map(|i| {
let (a, b) = domain.cell_bounds(i);
let overlap = (b.min(c_hi) - a.max(c_lo)).max(0.0);
r * overlap / claim_w + (1.0 - r) * (b - a) / domain_w
})
.collect()
}
(Claim::Value { value, .. }, Domain::Categorical { values }) => {
let noise = (1.0 - r) / n as f64;
values
.iter()
.map(|v| if v == value { r + noise } else { noise })
.collect()
}
(Claim::NotValue { value, .. }, Domain::Categorical { values }) => {
let noise = (1.0 - r) / n as f64;
let spread = r / (n as f64 - 1.0).max(1.0);
values
.iter()
.map(|v| if v == value { noise } else { spread + noise })
.collect()
}
_ => vec![1.0 / n as f64; n],
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct EvidenceRecord {
pub entity: String,
pub claim: Claim,
pub source: String,
#[serde(alias = "at", deserialize_with = "de_observed_at")]
pub observed_at: i64,
}
fn de_observed_at<'de, D>(d: D) -> std::result::Result<i64, D::Error>
where
D: serde::Deserializer<'de>,
{
struct WhenVisitor;
impl serde::de::Visitor<'_> for WhenVisitor {
type Value = i64;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.write_str("unix seconds or a date string (YYYY-MM-DD[THH:MM[:SS]])")
}
fn visit_i64<E: serde::de::Error>(self, v: i64) -> std::result::Result<i64, E> {
Ok(v)
}
fn visit_u64<E: serde::de::Error>(self, v: u64) -> std::result::Result<i64, E> {
i64::try_from(v).map_err(|_| E::custom("timestamp out of range"))
}
fn visit_str<E: serde::de::Error>(self, v: &str) -> std::result::Result<i64, E> {
crate::time::parse_when(v).map_err(|e| E::custom(e.to_string()))
}
}
d.deserialize_any(WhenVisitor)
}
#[derive(Clone, Debug)]
pub struct Evidence {
pub entity: String,
pub claim: Claim,
pub source: Source,
pub observed_at: i64,
}
impl Evidence {
pub fn reliability_at(&self, as_of: i64) -> f64 {
let age_days = ((as_of - self.observed_at) as f64 / SECONDS_PER_DAY).max(0.0);
self.source.reliability_at(age_days)
}
}