pub type MlThresholdResult<T> = Result<T, MlThresholdError>;
#[derive(Debug, Clone, PartialEq)]
pub enum MlThresholdError {
InsufficientData { have: usize, need: usize },
UnknownWorkload { name: String },
ModelNotTrained,
FeatureExtractionFailed { reason: String },
LowConfidence { confidence: f64, threshold: f64 },
DriftDetected { metric: String, drift_score: f64 },
}
impl std::fmt::Display for MlThresholdError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InsufficientData { have, need } => {
write!(f, "Insufficient data: have {}, need {}", have, need)
}
Self::UnknownWorkload { name } => write!(f, "Unknown workload: {}", name),
Self::ModelNotTrained => write!(f, "Model not trained"),
Self::FeatureExtractionFailed { reason } => {
write!(f, "Feature extraction failed: {}", reason)
}
Self::LowConfidence {
confidence,
threshold,
} => {
write!(f, "Low confidence {} < {}", confidence, threshold)
}
Self::DriftDetected {
metric,
drift_score,
} => {
write!(f, "Drift detected in {}: score {:.2}", metric, drift_score)
}
}
}
}
impl std::error::Error for MlThresholdError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum WorkloadClass {
Ffn,
Matmul,
Attention,
Quantize,
MemoryBound,
ComputeBound,
Unknown,
}
impl WorkloadClass {
pub fn default_cv_threshold(&self) -> f64 {
match self {
Self::Ffn => 18.0, Self::Matmul => 10.0, Self::Attention => 15.0, Self::Quantize => 12.0, Self::MemoryBound => 20.0, Self::ComputeBound => 8.0, Self::Unknown => 15.0, }
}
pub fn name(&self) -> &'static str {
match self {
Self::Ffn => "FFN",
Self::Matmul => "Matmul",
Self::Attention => "Attention",
Self::Quantize => "Quantize",
Self::MemoryBound => "MemoryBound",
Self::ComputeBound => "ComputeBound",
Self::Unknown => "Unknown",
}
}
pub(super) fn from_name(name: &str) -> Option<Self> {
match name {
"FFN" => Some(WorkloadClass::Ffn),
"Matmul" => Some(WorkloadClass::Matmul),
"Attention" => Some(WorkloadClass::Attention),
"Quantize" => Some(WorkloadClass::Quantize),
"MemoryBound" => Some(WorkloadClass::MemoryBound),
"ComputeBound" => Some(WorkloadClass::ComputeBound),
_ => None,
}
}
}
#[derive(Debug, Clone)]
pub struct TimeSeriesFeatures {
pub mean: f64,
pub std_dev: f64,
pub cv: f64,
pub skewness: f64,
pub kurtosis: f64,
pub autocorr_lag1: f64,
pub trend_slope: f64,
pub sample_count: usize,
}
impl TimeSeriesFeatures {
pub fn extract(values: &[f64]) -> Option<Self> {
if values.len() < 10 {
return None;
}
let n = values.len() as f64;
let mean = values.iter().sum::<f64>() / n;
let variance = values.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / n;
let std_dev = variance.sqrt();
let cv = if mean.abs() > 1e-10 {
(std_dev / mean) * 100.0
} else {
0.0
};
let skewness = if std_dev > 1e-10 {
let m3 = values
.iter()
.map(|x| ((x - mean) / std_dev).powi(3))
.sum::<f64>()
/ n;
m3
} else {
0.0
};
let kurtosis = if std_dev > 1e-10 {
let m4 = values
.iter()
.map(|x| ((x - mean) / std_dev).powi(4))
.sum::<f64>()
/ n;
m4 - 3.0 } else {
0.0
};
let autocorr_lag1 = if values.len() > 1 && std_dev > 1e-10 {
let mut sum = 0.0;
for i in 0..values.len() - 1 {
sum += (values[i] - mean) * (values[i + 1] - mean);
}
sum / ((values.len() - 1) as f64 * variance)
} else {
0.0
};
let trend_slope = {
let x_mean = (values.len() as f64 - 1.0) / 2.0;
let mut num = 0.0;
let mut den = 0.0;
for (i, &y) in values.iter().enumerate() {
let x = i as f64;
num += (x - x_mean) * (y - mean);
den += (x - x_mean).powi(2);
}
if den > 1e-10 {
num / den
} else {
0.0
}
};
Some(Self {
mean,
std_dev,
cv,
skewness,
kurtosis,
autocorr_lag1,
trend_slope,
sample_count: values.len(),
})
}
pub fn to_vec(&self) -> Vec<f64> {
vec![
self.cv,
self.skewness,
self.kurtosis,
self.autocorr_lag1,
self.trend_slope,
]
}
}
#[derive(Debug, Clone)]
pub struct AnomalyResult {
pub is_anomaly: bool,
pub score: f64,
pub threshold: f64,
pub confidence: f64,
pub workload_class: WorkloadClass,
pub reason: String,
}
#[derive(Debug, Clone, Default)]
pub struct ClassificationMetrics {
pub true_positives: usize,
pub false_positives: usize,
pub true_negatives: usize,
pub false_negatives: usize,
}
impl ClassificationMetrics {
pub fn precision(&self) -> f64 {
let total = self.true_positives + self.false_positives;
if total == 0 {
0.0
} else {
self.true_positives as f64 / total as f64
}
}
pub fn recall(&self) -> f64 {
let total = self.true_positives + self.false_negatives;
if total == 0 {
0.0
} else {
self.true_positives as f64 / total as f64
}
}
pub fn f1(&self) -> f64 {
let p = self.precision();
let r = self.recall();
if p + r == 0.0 {
0.0
} else {
2.0 * p * r / (p + r)
}
}
pub fn false_positive_rate(&self) -> f64 {
let total = self.false_positives + self.true_negatives;
if total == 0 {
0.0
} else {
self.false_positives as f64 / total as f64
}
}
}