use crate::task::{CrackleTask, TaskMetadata, TaskOutput, Timestamp};
use crate::patterns::{
ClusteringPattern, ConservationPattern, CorrelationPattern, CracklePattern, PhaseTransitionPattern,
};
use crate::profile::ThermalProfile;
use crate::error::CrackleError;
use crate::information::{entropy, jsd, kl_divergence, mutual_information, permutation_entropy};
#[cfg(feature = "serde")]
use serde::{Serialize, Deserialize};
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct TaskEntry {
pub label: String,
pub metrics: Vec<(String, f64)>,
pub cooled_metrics: Vec<(String, f64)>,
pub metadata: TaskMetadata,
}
impl TaskEntry {
pub fn all_metrics(&self) -> Vec<(String, f64)> {
let mut result = self.cooled_metrics.clone();
for (name, val) in &self.metrics {
if !result.iter().any(|(n, _)| n == name) {
result.push((name.clone(), *val));
}
}
result
}
}
pub struct Kiln {
profile: ThermalProfile,
entries: Vec<TaskEntry>,
cooled: bool,
}
impl Kiln {
pub fn new(profile: ThermalProfile) -> Self {
Kiln {
profile,
entries: Vec::new(),
cooled: false,
}
}
pub fn default_profile() -> Self {
Kiln::new(ThermalProfile::default())
}
pub fn fire_task<T: CrackleTask>(&self, task: T) -> crate::Result<TaskOutput<T::Output>> {
if self.cooled {
return Err(CrackleError::KilnCooled);
}
Ok(task.fire())
}
pub fn fire_and_record<T: CrackleTask>(&mut self, task: T) -> crate::Result<TaskOutput<T::Output>> {
if self.cooled {
return Err(CrackleError::KilnCooled);
}
let label = task.label();
let fired_at = Timestamp::now();
let start = std::time::Instant::now();
let output = task.fire();
let fire_duration = start.elapsed();
let metadata = TaskMetadata {
fired_at,
cooled_at: None,
fire_duration,
label: label.clone(),
};
let entry = TaskEntry {
label,
metrics: output.metrics.clone(),
cooled_metrics: vec![],
metadata,
};
self.entries.push(entry);
Ok(output)
}
pub fn fire_all<T: CrackleTask>(&mut self, tasks: Vec<T>) -> crate::Result<Vec<TaskOutput<T::Output>>> {
tasks
.into_iter()
.map(|task| self.fire_and_record(task))
.collect()
}
pub fn add_entry(&mut self, label: impl Into<String>, metrics: Vec<(String, f64)>) {
let label = label.into();
let metadata = TaskMetadata::new(&label);
self.entries.push(TaskEntry {
label,
metrics,
cooled_metrics: vec![],
metadata,
});
}
pub fn task_count(&self) -> usize {
self.entries.len()
}
pub fn entries(&self) -> &[TaskEntry] {
&self.entries
}
pub fn cool(&mut self) -> Vec<CracklePattern> {
self.cooled = true;
let mut patterns = Vec::new();
if self.entries.len() < self.profile.rate.min_tasks_for_detection() {
return patterns;
}
let labels: Vec<String> = self.entries.iter().map(|e| e.label.clone()).collect();
let metrics: Vec<Vec<(String, f64)>> = self.entries.iter().map(|e| e.all_metrics()).collect();
let cooled_ts = Timestamp::now();
for entry in &mut self.entries {
entry.metadata.cooled_at = Some(cooled_ts);
}
if self.profile.detect_clustering {
let p = ClusteringPattern::detect(
&labels,
&metrics,
self.profile.rate.cluster_threshold(),
);
patterns.extend(p);
}
if self.profile.detect_phase_transitions {
let p = PhaseTransitionPattern::detect(
&labels,
&metrics,
self.profile.rate.phase_transition_sensitivity(),
);
patterns.extend(p);
}
if self.profile.detect_conservation {
let p = ConservationPattern::detect(
&labels,
&metrics,
self.profile.rate.conservation_tolerance(),
);
patterns.extend(p);
}
if self.profile.detect_correlations {
let p = CorrelationPattern::detect(
&labels,
&metrics,
self.profile.rate.correlation_threshold(),
);
patterns.extend(p);
}
patterns.sort_by(|a, b| b.confidence().partial_cmp(&a.confidence()).unwrap_or(std::cmp::Ordering::Equal));
patterns
}
pub fn is_cooled(&self) -> bool {
self.cooled
}
pub fn profile(&self) -> &ThermalProfile {
&self.profile
}
pub fn reset(&mut self) {
self.entries.clear();
self.cooled = false;
}
pub fn mi_matrix(&self, bins: usize) -> Vec<Vec<f64>> {
let metric_names = self.collect_metric_names();
let n = metric_names.len();
if n == 0 {
return vec![];
}
let metric_values: Vec<Vec<f64>> = metric_names
.iter()
.map(|name| {
self.entries
.iter()
.filter_map(|e| {
e.all_metrics()
.iter()
.find(|(n, _)| n == name)
.map(|(_, v)| *v)
})
.collect()
})
.collect();
let mut matrix = vec![vec![0.0f64; n]; n];
for i in 0..n {
matrix[i][i] = entropy(&metric_values[i], bins);
for j in (i + 1)..n {
let mi = mutual_information(&metric_values[i], &metric_values[j], bins);
matrix[i][j] = mi;
matrix[j][i] = mi;
}
}
matrix
}
pub fn distribution_shift(&self, bins: usize) -> Vec<(String, f64)> {
let metric_names = self.collect_metric_names();
let n = self.entries.len();
if n < 2 {
return vec![];
}
let mid = n / 2;
let mut results = Vec::new();
for name in &metric_names {
let first_half: Vec<f64> = self.entries[..mid]
.iter()
.filter_map(|e| {
e.all_metrics()
.iter()
.find(|(n, _)| n == name)
.map(|(_, v)| *v)
})
.collect();
let second_half: Vec<f64> = self.entries[mid..]
.iter()
.filter_map(|e| {
e.all_metrics()
.iter()
.find(|(n, _)| n == name)
.map(|(_, v)| *v)
})
.collect();
if !first_half.is_empty() && !second_half.is_empty() {
let kl = kl_divergence(&second_half, &first_half, bins);
results.push((name.clone(), kl));
}
}
results.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
results
}
pub fn jsd_shift(&self, bins: usize) -> Vec<(String, f64)> {
let metric_names = self.collect_metric_names();
let n = self.entries.len();
if n < 2 {
return vec![];
}
let mid = n / 2;
let mut results = Vec::new();
for name in &metric_names {
let first_half: Vec<f64> = self.entries[..mid]
.iter()
.filter_map(|e| {
e.all_metrics()
.iter()
.find(|(n, _)| n == name)
.map(|(_, v)| *v)
})
.collect();
let second_half: Vec<f64> = self.entries[mid..]
.iter()
.filter_map(|e| {
e.all_metrics()
.iter()
.find(|(n, _)| n == name)
.map(|(_, v)| *v)
})
.collect();
if !first_half.is_empty() && !second_half.is_empty() {
let js = jsd(&first_half, &second_half, bins);
results.push((name.clone(), js));
}
}
results.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
results
}
pub fn permutation_entropies(&self, order: usize) -> Vec<(String, f64)> {
let metric_names = self.collect_metric_names();
metric_names
.iter()
.map(|name| {
let values: Vec<f64> = self
.entries
.iter()
.filter_map(|e| {
e.all_metrics()
.iter()
.find(|(n, _)| n == name)
.map(|(_, v)| *v)
})
.collect();
(name.clone(), permutation_entropy(&values, order))
})
.collect()
}
fn collect_metric_names(&self) -> Vec<String> {
let mut names = std::collections::HashSet::new();
for entry in &self.entries {
for (name, _) in entry.all_metrics() {
names.insert(name.clone());
}
}
names.into_iter().collect()
}
}