Skip to main content

crackle_runtime/
patterns.rs

1use std::fmt;
2
3/// The kind of pattern detected during cooling.
4#[derive(Debug, Clone, PartialEq, Eq, Hash)]
5pub enum PatternKind {
6    /// Tasks that cluster together in metric space.
7    Clustering,
8    /// A shift in output distribution during cooling.
9    PhaseTransition,
10    /// A conservation law that holds across a group of tasks.
11    Conservation,
12    /// An unexpected correlation between seemingly unrelated tasks.
13    Correlation,
14}
15
16impl fmt::Display for PatternKind {
17    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
18        match self {
19            PatternKind::Clustering => write!(f, "clustering"),
20            PatternKind::PhaseTransition => write!(f, "phase transition"),
21            PatternKind::Conservation => write!(f, "conservation law"),
22            PatternKind::Correlation => write!(f, "correlation"),
23        }
24    }
25}
26
27/// A pattern detected during the cooling phase.
28///
29/// Like a craze line in pottery glaze, each pattern is a record of something
30/// that wasn't designed — it emerged from the interaction of many tasks
31/// as the system cooled.
32#[derive(Debug, Clone)]
33pub struct CracklePattern {
34    kind: PatternKind,
35    description: String,
36    involved_tasks: Vec<String>,
37    confidence: f64,
38    metrics: Vec<(String, f64)>,
39}
40
41impl CracklePattern {
42    /// Create a new detected pattern.
43    pub fn new(
44        kind: PatternKind,
45        description: impl Into<String>,
46        involved_tasks: Vec<String>,
47        confidence: f64,
48    ) -> Self {
49        CracklePattern {
50            kind,
51            description: description.into(),
52            involved_tasks,
53            confidence: confidence.clamp(0.0, 1.0),
54            metrics: vec![],
55        }
56    }
57
58    /// The kind of pattern detected.
59    pub fn kind(&self) -> &PatternKind {
60        &self.kind
61    }
62
63    /// Human-readable description of the pattern.
64    pub fn description(&self) -> &str {
65        &self.description
66    }
67
68    /// Labels of tasks involved in this pattern.
69    pub fn involved_tasks(&self) -> &[String] {
70        &self.involved_tasks
71    }
72
73    /// Confidence score (0.0 to 1.0).
74    pub fn confidence(&self) -> f64 {
75        self.confidence
76    }
77
78    /// Additional metrics associated with this pattern.
79    pub fn metrics(&self) -> &[(String, f64)] {
80        &self.metrics
81    }
82
83    /// Add a metric to this pattern.
84    pub fn with_metric(mut self, name: impl Into<String>, value: f64) -> Self {
85        self.metrics.push((name.into(), value));
86        self
87    }
88
89    /// Create a pattern with additional metrics.
90    pub fn with_metrics(mut self, metrics: Vec<(String, f64)>) -> Self {
91        self.metrics = metrics;
92        self
93    }
94}
95
96/// Detector for clustering patterns — tasks that complete near each other in metric space.
97#[derive(Debug, Clone)]
98pub struct ClusteringPattern;
99
100impl ClusteringPattern {
101    /// Detect clusters among task metrics.
102    ///
103    /// Returns groups of task labels that cluster together based on metric proximity.
104    pub fn detect(
105        task_labels: &[String],
106        task_metrics: &[Vec<(String, f64)>],
107        threshold: f64,
108    ) -> Vec<CracklePattern> {
109        if task_labels.len() < 2 {
110            return vec![];
111        }
112
113        let mut patterns = Vec::new();
114        let n = task_labels.len();
115        let mut visited = vec![false; n];
116
117        for i in 0..n {
118            if visited[i] {
119                continue;
120            }
121            let mut cluster = vec![i];
122            visited[i] = true;
123
124            for j in (i + 1)..n {
125                if visited[j] {
126                    continue;
127                }
128                if Self::metric_distance(&task_metrics[i], &task_metrics[j]) < threshold {
129                    cluster.push(j);
130                    visited[j] = true;
131                }
132            }
133
134            if cluster.len() > 1 {
135                let labels: Vec<String> = cluster.iter().map(|&idx| task_labels[idx].clone()).collect();
136                let avg_dist = Self::avg_cluster_distance(&cluster, task_metrics);
137                patterns.push(
138                    CracklePattern::new(
139                        PatternKind::Clustering,
140                        format!(
141                            "{} tasks clustered together in metric space (avg distance: {:.3})",
142                            labels.len(),
143                            avg_dist
144                        ),
145                        labels,
146                        1.0 - (avg_dist / threshold).min(1.0),
147                    )
148                    .with_metric("avg_distance", avg_dist)
149                    .with_metric("cluster_size", cluster.len() as f64),
150                );
151            }
152        }
153
154        patterns
155    }
156
157    /// Compute Euclidean-like distance between two metric sets.
158    pub fn metric_distance(a: &[(String, f64)], b: &[(String, f64)]) -> f64 {
159        let mut sum_sq = 0.0;
160        let mut matched = 0;
161
162        for (name_a, val_a) in a {
163            if let Some((_, val_b)) = b.iter().find(|(name_b, _)| name_b == name_a) {
164                sum_sq += (val_a - val_b).powi(2);
165                matched += 1;
166            }
167        }
168
169        if matched == 0 {
170            f64::MAX
171        } else {
172            sum_sq.sqrt()
173        }
174    }
175
176    fn avg_cluster_distance(indices: &[usize], metrics: &[Vec<(String, f64)>]) -> f64 {
177        if indices.len() < 2 {
178            return 0.0;
179        }
180        let mut total = 0.0;
181        let mut count = 0;
182        for i in 0..indices.len() {
183            for j in (i + 1)..indices.len() {
184                total += Self::metric_distance(&metrics[indices[i]], &metrics[indices[j]]);
185                count += 1;
186            }
187        }
188        total / count as f64
189    }
190}
191
192/// Detector for phase transitions — shifts in output distributions.
193#[derive(Debug, Clone)]
194pub struct PhaseTransitionPattern;
195
196impl PhaseTransitionPattern {
197    /// Detect phase transitions by looking for significant shifts in metric values.
198    ///
199    /// Works by comparing the first half of tasks to the second half.
200    pub fn detect(
201        task_labels: &[String],
202        task_metrics: &[Vec<(String, f64)>],
203        sensitivity: f64,
204    ) -> Vec<CracklePattern> {
205        let n = task_labels.len();
206        if n < 2 {
207            return vec![];
208        }
209
210        let mut patterns = Vec::new();
211        let all_metric_names = Self::collect_metric_names(task_metrics);
212
213        for metric_name in &all_metric_names {
214            let values: Vec<(usize, f64)> = task_metrics
215                .iter()
216                .enumerate()
217                .filter_map(|(i, m)| {
218                    m.iter()
219                        .find(|(n, _)| n == metric_name)
220                        .map(|(_, v)| (i, *v))
221                })
222                .collect();
223
224            if values.len() < 2 {
225                continue;
226            }
227
228            let mid = values.len() / 2;
229            let first_half_avg = values[..mid].iter().map(|(_, v)| v).sum::<f64>() / mid as f64;
230            let second_half_avg =
231                values[mid..].iter().map(|(_, v)| v).sum::<f64>() / (values.len() - mid) as f64;
232
233            let global_avg = values.iter().map(|(_, v)| v).sum::<f64>() / values.len() as f64;
234            if global_avg.abs() < f64::EPSILON {
235                continue;
236            }
237
238            let shift = (second_half_avg - first_half_avg).abs() / global_avg.abs();
239            if shift > sensitivity {
240                let involved: Vec<String> = values
241                    .iter()
242                    .map(|(idx, _)| task_labels[*idx].clone())
243                    .collect();
244                patterns.push(
245                    CracklePattern::new(
246                        PatternKind::PhaseTransition,
247                        format!(
248                            "metric '{}' shifted by {:.1}% between first and second half of tasks",
249                            metric_name,
250                            shift * 100.0
251                        ),
252                        involved,
253                        (shift / sensitivity).min(1.0),
254                    )
255                    .with_metric("metric_name_hash", metric_name.len() as f64)
256                    .with_metric("shift_magnitude", shift)
257                    .with_metric("first_half_avg", first_half_avg)
258                    .with_metric("second_half_avg", second_half_avg),
259                );
260            }
261        }
262
263        patterns
264    }
265
266    fn collect_metric_names(metrics: &[Vec<(String, f64)>]) -> Vec<String> {
267        let mut names = std::collections::HashSet::new();
268        for m in metrics {
269            for (name, _) in m {
270                names.insert(name.clone());
271            }
272        }
273        names.into_iter().collect()
274    }
275}
276
277/// Detector for conservation laws — metrics whose sum stays near-constant across tasks.
278#[derive(Debug, Clone)]
279pub struct ConservationPattern;
280
281impl ConservationPattern {
282    /// Detect conservation laws: metrics that sum to approximately the same value across task groups.
283    pub fn detect(
284        task_labels: &[String],
285        task_metrics: &[Vec<(String, f64)>],
286        tolerance: f64,
287    ) -> Vec<CracklePattern> {
288        let n = task_labels.len();
289        if n < 2 {
290            return vec![];
291        }
292
293        let mut patterns = Vec::new();
294        let all_metric_names = PhaseTransitionPattern::collect_metric_names(task_metrics);
295
296        for metric_name in &all_metric_names {
297            let values: Vec<(usize, f64)> = task_metrics
298                .iter()
299                .enumerate()
300                .filter_map(|(i, m)| {
301                    m.iter()
302                        .find(|(n, _)| n == metric_name)
303                        .map(|(_, v)| (i, *v))
304                })
305                .collect();
306
307            if values.len() < 2 {
308                continue;
309            }
310
311            let total: f64 = values.iter().map(|(_, v)| v).sum();
312            let avg = total / values.len() as f64;
313
314            // Check if values hover around a constant (low variance = conservation)
315            let variance =
316                values.iter().map(|(_, v)| (v - avg).powi(2)).sum::<f64>() / values.len() as f64;
317            let std_dev = variance.sqrt();
318
319            if avg.abs() > f64::EPSILON && std_dev / avg.abs() < tolerance {
320                let involved: Vec<String> = values
321                    .iter()
322                    .map(|(idx, _)| task_labels[*idx].clone())
323                    .collect();
324                patterns.push(
325                    CracklePattern::new(
326                        PatternKind::Conservation,
327                        format!(
328                            "metric '{}' is conserved across {} tasks (sum: {:.3}, std_dev: {:.3})",
329                            metric_name,
330                            involved.len(),
331                            total,
332                            std_dev
333                        ),
334                        involved,
335                        1.0 - (std_dev / avg.abs()).min(1.0),
336                    )
337                    .with_metric("total", total)
338                    .with_metric("std_dev", std_dev)
339                    .with_metric("coefficient_of_variation", std_dev / avg.abs()),
340                );
341            }
342        }
343
344        patterns
345    }
346}
347
348/// Detector for unexpected correlations between tasks.
349#[derive(Debug, Clone)]
350pub struct CorrelationPattern;
351
352impl CorrelationPattern {
353    /// Detect correlations between different metrics across tasks.
354    pub fn detect(
355        task_labels: &[String],
356        task_metrics: &[Vec<(String, f64)>],
357        threshold: f64,
358    ) -> Vec<CracklePattern> {
359        let n = task_labels.len();
360        if n < 3 {
361            return vec![];
362        }
363
364        let metric_names = PhaseTransitionPattern::collect_metric_names(task_metrics);
365        if metric_names.len() < 2 {
366            return vec![];
367        }
368
369        let mut patterns = Vec::new();
370
371        for i in 0..metric_names.len() {
372            for j in (i + 1)..metric_names.len() {
373                let name_a = &metric_names[i];
374                let name_b = &metric_names[j];
375
376                let pairs: Vec<(f64, f64)> = task_metrics
377                    .iter()
378                    .filter_map(|m| {
379                        let a = m.iter().find(|(n, _)| n == name_a).map(|(_, v)| *v);
380                        let b = m.iter().find(|(n, _)| n == name_b).map(|(_, v)| *v);
381                        match (a, b) {
382                            (Some(a), Some(b)) => Some((a, b)),
383                            _ => None,
384                        }
385                    })
386                    .collect();
387
388                if pairs.len() < 3 {
389                    continue;
390                }
391
392                let corr = Self::pearson_correlation(&pairs);
393                if corr.abs() >= threshold {
394                    let involved: Vec<String> = task_labels
395                        .iter()
396                        .take(pairs.len())
397                        .cloned()
398                        .collect();
399                    patterns.push(
400                        CracklePattern::new(
401                            PatternKind::Correlation,
402                            format!(
403                                "strong {} correlation between '{}' and '{}' (r = {:.3})",
404                                if corr > 0.0 { "positive" } else { "negative" },
405                                name_a,
406                                name_b,
407                                corr
408                            ),
409                            involved,
410                            corr.abs(),
411                        )
412                        .with_metric("correlation", corr)
413                        .with_metric("metric_a_len", name_a.len() as f64)
414                        .with_metric("metric_b_len", name_b.len() as f64),
415                    );
416                }
417            }
418        }
419
420        patterns
421    }
422
423    /// Compute Pearson correlation coefficient.
424    pub fn pearson_correlation(pairs: &[(f64, f64)]) -> f64 {
425        let n = pairs.len() as f64;
426        if n < 2.0 {
427            return 0.0;
428        }
429
430        let sum_x: f64 = pairs.iter().map(|(x, _)| x).sum();
431        let sum_y: f64 = pairs.iter().map(|(_, y)| y).sum();
432        let sum_xy: f64 = pairs.iter().map(|(x, y)| x * y).sum();
433        let sum_x2: f64 = pairs.iter().map(|(x, _)| x * x).sum();
434        let sum_y2: f64 = pairs.iter().map(|(_, y)| y * y).sum();
435
436        let numerator = n * sum_xy - sum_x * sum_y;
437        let denominator = ((n * sum_x2 - sum_x * sum_x) * (n * sum_y2 - sum_y * sum_y)).sqrt();
438
439        if denominator.abs() < f64::EPSILON {
440            0.0
441        } else {
442            numerator / denominator
443        }
444    }
445}