use chrono::Utc;
use reputation_types::{AgentData, ReputationScore, ConfidenceLevel, ScoreComponents};
use crate::error::{BuilderError, CalculationError, Result};
use crate::validation;
use crate::config::CalculatorConfig;
use crate::ALGORITHM_VERSION;
use rayon::prelude::*;
use std::time::{Duration, Instant};
mod prior;
mod empirical;
mod confidence;
pub mod builder;
pub mod utils;
pub(crate) use self::prior::calculate_prior_detailed;
pub(crate) use self::empirical::calculate_empirical;
pub(crate) use self::confidence::calculate_confidence;
#[derive(Debug)]
pub struct Calculator {
pub(crate) confidence_k: f64,
pub(crate) prior_base: f64,
pub(crate) prior_max: f64,
}
impl Default for Calculator {
fn default() -> Self {
Self {
confidence_k: 15.0,
prior_base: 50.0,
prior_max: 80.0,
}
}
}
impl Calculator {
pub fn confidence_k(&self) -> f64 {
self.confidence_k
}
pub fn prior_base(&self) -> f64 {
self.prior_base
}
pub fn prior_max(&self) -> f64 {
self.prior_max
}
pub fn builder() -> builder::CalculatorBuilder {
builder::CalculatorBuilder::new()
}
pub fn from_config(config: CalculatorConfig) -> Result<Self> {
Self::new(config.confidence_k, config.prior_base, config.prior_max)
}
pub fn new(confidence_k: f64, prior_base: f64, prior_max: f64) -> Result<Self> {
if confidence_k <= 0.0 {
return Err(BuilderError::InvalidConfig("confidence_k must be positive".to_string()).into());
}
if prior_base < 0.0 || prior_base > 100.0 {
return Err(BuilderError::InvalidConfig("prior_base must be between 0 and 100".to_string()).into());
}
if prior_max < prior_base || prior_max > 100.0 {
return Err(BuilderError::InvalidConfig("prior_max must be between prior_base and 100".to_string()).into());
}
Ok(Self {
confidence_k,
prior_base,
prior_max,
})
}
pub fn calculate(&self, agent: &AgentData) -> Result<ReputationScore> {
validation::validate_agent_data(agent)?;
let prior_breakdown = calculate_prior_detailed(agent, self.prior_base, self.prior_max);
let prior_score = prior_breakdown.total;
let empirical_score = calculate_empirical(agent);
let confidence_value = calculate_confidence(agent.total_interactions, self.confidence_k)?;
let confidence_level = ConfidenceLevel::from_confidence(confidence_value);
let prior_weight = 1.0 - confidence_value;
let empirical_weight = confidence_value;
let final_score = prior_weight * prior_score + empirical_weight * empirical_score;
if final_score.is_nan() {
return Err(CalculationError::NaNResult.into());
}
if final_score < 0.0 || final_score > 100.0 {
return Err(CalculationError::ScoreOutOfBounds(final_score).into());
}
let components = ScoreComponents {
prior_score,
prior_breakdown,
empirical_score,
confidence_value,
confidence_level,
prior_weight,
empirical_weight,
};
let data_points = agent.total_interactions.saturating_add(agent.total_reviews);
let is_provisional = confidence_value < 0.2;
Ok(ReputationScore {
score: final_score.clamp(0.0, 100.0),
confidence: confidence_value,
level: confidence_level,
components,
is_provisional,
data_points,
algorithm_version: ALGORITHM_VERSION.to_string(),
calculated_at: Utc::now(),
})
}
pub fn calculate_batch(&self, agents: &[AgentData]) -> Vec<Result<ReputationScore>> {
agents
.par_iter()
.map(|agent| self.calculate(agent))
.collect()
}
pub fn calculate_batch_with_options(
&self,
agents: &[AgentData],
options: BatchOptions,
) -> BatchResult {
let batch_start = Instant::now();
let chunk_size = options.chunk_size.unwrap_or(100);
let calculations: Vec<_> = if let Some(ref callback) = options.progress_callback {
let processed = std::sync::atomic::AtomicUsize::new(0);
let total = agents.len();
agents
.par_chunks(chunk_size)
.flat_map(|chunk| {
chunk.par_iter().map(|agent| {
let start = Instant::now();
let result = self.calculate(agent);
let duration = start.elapsed();
if options.fail_fast && result.is_err() {
}
let calc = BatchCalculation {
agent_id: agent.did.clone(),
result,
duration,
};
let current = processed.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1;
callback(current, total);
calc
})
})
.collect()
} else {
agents
.par_chunks(chunk_size)
.flat_map(|chunk| {
chunk.par_iter().map(|agent| {
let start = Instant::now();
let result = self.calculate(agent);
let duration = start.elapsed();
BatchCalculation {
agent_id: agent.did.clone(),
result,
duration,
}
})
})
.collect()
};
let successful_count = calculations.iter().filter(|c| c.result.is_ok()).count();
let failed_count = calculations.len() - successful_count;
BatchResult {
calculations,
total_duration: batch_start.elapsed(),
successful_count,
failed_count,
}
}
}
pub struct BatchOptions {
pub chunk_size: Option<usize>,
pub fail_fast: bool,
pub progress_callback: Option<Box<dyn Fn(usize, usize) + Send + Sync>>,
}
impl Default for BatchOptions {
fn default() -> Self {
Self {
chunk_size: None,
fail_fast: false,
progress_callback: None,
}
}
}
impl std::fmt::Debug for BatchOptions {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BatchOptions")
.field("chunk_size", &self.chunk_size)
.field("fail_fast", &self.fail_fast)
.field("progress_callback", &self.progress_callback.is_some())
.finish()
}
}
#[derive(Debug)]
pub struct BatchResult {
pub calculations: Vec<BatchCalculation>,
pub total_duration: Duration,
pub successful_count: usize,
pub failed_count: usize,
}
#[derive(Debug)]
pub struct BatchCalculation {
pub agent_id: String,
pub result: Result<ReputationScore>,
pub duration: Duration,
}