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