1use std::fmt;
2
3#[derive(Debug, Clone, PartialEq, Eq, Hash)]
5pub enum PatternKind {
6 Clustering,
8 PhaseTransition,
10 Conservation,
12 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#[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 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 pub fn kind(&self) -> &PatternKind {
60 &self.kind
61 }
62
63 pub fn description(&self) -> &str {
65 &self.description
66 }
67
68 pub fn involved_tasks(&self) -> &[String] {
70 &self.involved_tasks
71 }
72
73 pub fn confidence(&self) -> f64 {
75 self.confidence
76 }
77
78 pub fn metrics(&self) -> &[(String, f64)] {
80 &self.metrics
81 }
82
83 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 pub fn with_metrics(mut self, metrics: Vec<(String, f64)>) -> Self {
91 self.metrics = metrics;
92 self
93 }
94}
95
96#[derive(Debug, Clone)]
98pub struct ClusteringPattern;
99
100impl ClusteringPattern {
101 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 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#[derive(Debug, Clone)]
194pub struct PhaseTransitionPattern;
195
196impl PhaseTransitionPattern {
197 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#[derive(Debug, Clone)]
279pub struct ConservationPattern;
280
281impl ConservationPattern {
282 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 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#[derive(Debug, Clone)]
350pub struct CorrelationPattern;
351
352impl CorrelationPattern {
353 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 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}