1use crate::task::{CrackleTask, TaskMetadata, TaskOutput, Timestamp};
2use crate::patterns::{
3 ClusteringPattern, ConservationPattern, CorrelationPattern, CracklePattern, PhaseTransitionPattern,
4};
5use crate::profile::ThermalProfile;
6use crate::error::CrackleError;
7use crate::information::{entropy, jsd, kl_divergence, mutual_information, permutation_entropy};
8#[cfg(feature = "serde")]
9use serde::{Serialize, Deserialize};
10
11#[derive(Debug, Clone)]
13#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
14pub struct TaskEntry {
15 pub label: String,
17 pub metrics: Vec<(String, f64)>,
19 pub cooled_metrics: Vec<(String, f64)>,
21 pub metadata: TaskMetadata,
23}
24
25impl TaskEntry {
26 pub fn all_metrics(&self) -> Vec<(String, f64)> {
28 let mut result = self.cooled_metrics.clone();
29 for (name, val) in &self.metrics {
30 if !result.iter().any(|(n, _)| n == name) {
31 result.push((name.clone(), *val));
32 }
33 }
34 result
35 }
36}
37
38pub struct Kiln {
72 profile: ThermalProfile,
73 entries: Vec<TaskEntry>,
74 cooled: bool,
75}
76
77impl Kiln {
78 pub fn new(profile: ThermalProfile) -> Self {
80 Kiln {
81 profile,
82 entries: Vec::new(),
83 cooled: false,
84 }
85 }
86
87 pub fn default_profile() -> Self {
89 Kiln::new(ThermalProfile::default())
90 }
91
92 pub fn fire_task<T: CrackleTask>(&self, task: T) -> crate::Result<TaskOutput<T::Output>> {
100 if self.cooled {
101 return Err(CrackleError::KilnCooled);
102 }
103
104 Ok(task.fire())
105 }
106
107 pub fn fire_and_record<T: CrackleTask>(&mut self, task: T) -> crate::Result<TaskOutput<T::Output>> {
116 if self.cooled {
117 return Err(CrackleError::KilnCooled);
118 }
119
120 let label = task.label();
121 let fired_at = Timestamp::now();
122 let start = std::time::Instant::now();
123
124 let output = task.fire();
125
126 let fire_duration = start.elapsed();
127 let metadata = TaskMetadata {
128 fired_at,
129 cooled_at: None,
130 fire_duration,
131 label: label.clone(),
132 };
133
134 let entry = TaskEntry {
135 label,
136 metrics: output.metrics.clone(),
137 cooled_metrics: vec![],
138 metadata,
139 };
140
141 self.entries.push(entry);
142 Ok(output)
143 }
144
145 pub fn fire_all<T: CrackleTask>(&mut self, tasks: Vec<T>) -> crate::Result<Vec<TaskOutput<T::Output>>> {
151 tasks
152 .into_iter()
153 .map(|task| self.fire_and_record(task))
154 .collect()
155 }
156
157 pub fn add_entry(&mut self, label: impl Into<String>, metrics: Vec<(String, f64)>) {
159 let label = label.into();
160 let metadata = TaskMetadata::new(&label);
161 self.entries.push(TaskEntry {
162 label,
163 metrics,
164 cooled_metrics: vec![],
165 metadata,
166 });
167 }
168
169 pub fn task_count(&self) -> usize {
171 self.entries.len()
172 }
173
174 pub fn entries(&self) -> &[TaskEntry] {
176 &self.entries
177 }
178
179 pub fn cool(&mut self) -> Vec<CracklePattern> {
187 self.cooled = true;
188 let mut patterns = Vec::new();
189
190 if self.entries.len() < self.profile.rate.min_tasks_for_detection() {
191 return patterns;
192 }
193
194 let labels: Vec<String> = self.entries.iter().map(|e| e.label.clone()).collect();
195 let metrics: Vec<Vec<(String, f64)>> = self.entries.iter().map(|e| e.all_metrics()).collect();
196
197 let cooled_ts = Timestamp::now();
199 for entry in &mut self.entries {
200 entry.metadata.cooled_at = Some(cooled_ts);
201 }
202
203 if self.profile.detect_clustering {
204 let p = ClusteringPattern::detect(
205 &labels,
206 &metrics,
207 self.profile.rate.cluster_threshold(),
208 );
209 patterns.extend(p);
210 }
211
212 if self.profile.detect_phase_transitions {
213 let p = PhaseTransitionPattern::detect(
214 &labels,
215 &metrics,
216 self.profile.rate.phase_transition_sensitivity(),
217 );
218 patterns.extend(p);
219 }
220
221 if self.profile.detect_conservation {
222 let p = ConservationPattern::detect(
223 &labels,
224 &metrics,
225 self.profile.rate.conservation_tolerance(),
226 );
227 patterns.extend(p);
228 }
229
230 if self.profile.detect_correlations {
231 let p = CorrelationPattern::detect(
232 &labels,
233 &metrics,
234 self.profile.rate.correlation_threshold(),
235 );
236 patterns.extend(p);
237 }
238
239 patterns.sort_by(|a, b| b.confidence().partial_cmp(&a.confidence()).unwrap_or(std::cmp::Ordering::Equal));
241
242 patterns
243 }
244
245 pub fn is_cooled(&self) -> bool {
247 self.cooled
248 }
249
250 pub fn profile(&self) -> &ThermalProfile {
252 &self.profile
253 }
254
255 pub fn reset(&mut self) {
257 self.entries.clear();
258 self.cooled = false;
259 }
260
261 pub fn mi_matrix(&self, bins: usize) -> Vec<Vec<f64>> {
275 let metric_names = self.collect_metric_names();
276 let n = metric_names.len();
277 if n == 0 {
278 return vec![];
279 }
280
281 let metric_values: Vec<Vec<f64>> = metric_names
283 .iter()
284 .map(|name| {
285 self.entries
286 .iter()
287 .filter_map(|e| {
288 e.all_metrics()
289 .iter()
290 .find(|(n, _)| n == name)
291 .map(|(_, v)| *v)
292 })
293 .collect()
294 })
295 .collect();
296
297 let mut matrix = vec![vec![0.0f64; n]; n];
298
299 for i in 0..n {
300 matrix[i][i] = entropy(&metric_values[i], bins);
301 for j in (i + 1)..n {
302 let mi = mutual_information(&metric_values[i], &metric_values[j], bins);
303 matrix[i][j] = mi;
304 matrix[j][i] = mi;
305 }
306 }
307
308 matrix
309 }
310
311 pub fn distribution_shift(&self, bins: usize) -> Vec<(String, f64)> {
316 let metric_names = self.collect_metric_names();
317 let n = self.entries.len();
318 if n < 2 {
319 return vec![];
320 }
321
322 let mid = n / 2;
323 let mut results = Vec::new();
324
325 for name in &metric_names {
326 let first_half: Vec<f64> = self.entries[..mid]
327 .iter()
328 .filter_map(|e| {
329 e.all_metrics()
330 .iter()
331 .find(|(n, _)| n == name)
332 .map(|(_, v)| *v)
333 })
334 .collect();
335
336 let second_half: Vec<f64> = self.entries[mid..]
337 .iter()
338 .filter_map(|e| {
339 e.all_metrics()
340 .iter()
341 .find(|(n, _)| n == name)
342 .map(|(_, v)| *v)
343 })
344 .collect();
345
346 if !first_half.is_empty() && !second_half.is_empty() {
347 let kl = kl_divergence(&second_half, &first_half, bins);
348 results.push((name.clone(), kl));
349 }
350 }
351
352 results.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
353 results
354 }
355
356 pub fn jsd_shift(&self, bins: usize) -> Vec<(String, f64)> {
360 let metric_names = self.collect_metric_names();
361 let n = self.entries.len();
362 if n < 2 {
363 return vec![];
364 }
365
366 let mid = n / 2;
367 let mut results = Vec::new();
368
369 for name in &metric_names {
370 let first_half: Vec<f64> = self.entries[..mid]
371 .iter()
372 .filter_map(|e| {
373 e.all_metrics()
374 .iter()
375 .find(|(n, _)| n == name)
376 .map(|(_, v)| *v)
377 })
378 .collect();
379
380 let second_half: Vec<f64> = self.entries[mid..]
381 .iter()
382 .filter_map(|e| {
383 e.all_metrics()
384 .iter()
385 .find(|(n, _)| n == name)
386 .map(|(_, v)| *v)
387 })
388 .collect();
389
390 if !first_half.is_empty() && !second_half.is_empty() {
391 let js = jsd(&first_half, &second_half, bins);
392 results.push((name.clone(), js));
393 }
394 }
395
396 results.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
397 results
398 }
399
400 pub fn permutation_entropies(&self, order: usize) -> Vec<(String, f64)> {
404 let metric_names = self.collect_metric_names();
405
406 metric_names
407 .iter()
408 .map(|name| {
409 let values: Vec<f64> = self
410 .entries
411 .iter()
412 .filter_map(|e| {
413 e.all_metrics()
414 .iter()
415 .find(|(n, _)| n == name)
416 .map(|(_, v)| *v)
417 })
418 .collect();
419 (name.clone(), permutation_entropy(&values, order))
420 })
421 .collect()
422 }
423
424 fn collect_metric_names(&self) -> Vec<String> {
426 let mut names = std::collections::HashSet::new();
427 for entry in &self.entries {
428 for (name, _) in entry.all_metrics() {
429 names.insert(name.clone());
430 }
431 }
432 names.into_iter().collect()
433 }
434}