use serde::{Deserialize, Serialize};
use crate::error::{Result, SanghaError, validate_finite, validate_positive};
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Opinion(pub f64);
impl Opinion {
#[inline]
#[must_use]
pub fn new(value: f64) -> Self {
Self(value.clamp(0.0, 1.0))
}
}
#[must_use = "returns the updated opinions without side effects"]
pub fn deffuant_interaction(
opinion1: f64,
opinion2: f64,
threshold: f64,
mu: f64,
) -> Result<(f64, f64)> {
validate_finite(opinion1, "opinion1")?;
validate_finite(opinion2, "opinion2")?;
validate_finite(threshold, "threshold")?;
validate_finite(mu, "mu")?;
let diff = (opinion1 - opinion2).abs();
if diff < threshold {
let new1 = opinion1 + mu * (opinion2 - opinion1);
let new2 = opinion2 + mu * (opinion1 - opinion2);
Ok((new1, new2))
} else {
Ok((opinion1, opinion2))
}
}
#[must_use = "returns the echo chamber index without side effects"]
pub fn echo_chamber_index(opinions: &[f64]) -> Result<f64> {
if opinions.is_empty() {
return Err(SanghaError::ComputationError(
"need at least one opinion".into(),
));
}
let n = opinions.len() as f64;
let mean = opinions.iter().sum::<f64>() / n;
let variance = opinions.iter().map(|&o| (o - mean).powi(2)).sum::<f64>() / n;
Ok((variance / 0.25).min(1.0))
}
#[must_use = "returns whether consensus has been reached without side effects"]
pub fn has_consensus(opinions: &[f64], epsilon: f64) -> Result<bool> {
validate_positive(epsilon, "epsilon")?;
if opinions.is_empty() {
return Ok(true);
}
let n = opinions.len() as f64;
let mean = opinions.iter().sum::<f64>() / n;
Ok(opinions.iter().all(|&o| (o - mean).abs() < epsilon))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_deffuant_within_threshold() {
let (o1, o2) = deffuant_interaction(0.3, 0.5, 0.5, 0.5).unwrap();
assert!((o1 - 0.4).abs() < 1e-10);
assert!((o2 - 0.4).abs() < 1e-10);
}
#[test]
fn test_deffuant_outside_threshold() {
let (o1, o2) = deffuant_interaction(0.1, 0.9, 0.3, 0.5).unwrap();
assert!((o1 - 0.1).abs() < 1e-10);
assert!((o2 - 0.9).abs() < 1e-10);
}
#[test]
fn test_echo_chamber_consensus() {
let opinions = vec![0.5, 0.5, 0.5, 0.5];
let idx = echo_chamber_index(&opinions).unwrap();
assert!(idx.abs() < 1e-10);
}
#[test]
fn test_echo_chamber_polarized() {
let opinions = vec![0.0, 0.0, 1.0, 1.0];
let idx = echo_chamber_index(&opinions).unwrap();
assert!(idx > 0.9);
}
#[test]
fn test_has_consensus_true() {
assert!(has_consensus(&[0.5, 0.5, 0.5], 0.01).unwrap());
}
#[test]
fn test_has_consensus_false() {
assert!(!has_consensus(&[0.1, 0.9], 0.01).unwrap());
}
#[test]
fn test_has_consensus_invalid_epsilon() {
assert!(has_consensus(&[0.5], -1.0).is_err());
assert!(has_consensus(&[0.5], 0.0).is_err());
assert!(has_consensus(&[0.5], f64::NAN).is_err());
}
#[test]
fn test_opinion_serde_roundtrip() {
let o = Opinion::new(0.7);
let json = serde_json::to_string(&o).unwrap();
let back: Opinion = serde_json::from_str(&json).unwrap();
assert!((o.0 - back.0).abs() < 1e-10);
}
#[test]
fn test_deffuant_at_exact_threshold() {
let (o1, o2) = deffuant_interaction(0.3, 0.6, 0.3, 0.5).unwrap();
assert!((o1 - 0.3).abs() < 1e-10);
assert!((o2 - 0.6).abs() < 1e-10);
}
#[test]
fn test_echo_chamber_single_opinion() {
let idx = echo_chamber_index(&[0.5]).unwrap();
assert!(idx.abs() < 1e-10);
}
#[test]
fn test_echo_chamber_empty() {
assert!(echo_chamber_index(&[]).is_err());
}
#[test]
fn test_opinion_clamping() {
assert!((Opinion::new(1.5).0 - 1.0).abs() < 1e-10);
assert!((Opinion::new(-0.5).0 - 0.0).abs() < 1e-10);
}
#[test]
fn test_has_consensus_empty() {
assert!(has_consensus(&[], 0.01).unwrap());
}
#[test]
fn test_deffuant_nan_error() {
assert!(deffuant_interaction(f64::NAN, 0.5, 0.5, 0.5).is_err());
}
}