1use std::collections::HashMap;
12use std::sync::atomic::{AtomicU64, Ordering};
13use chrono::{DateTime, Utc, Duration};
14use serde::{Deserialize, Serialize};
15
16#[cfg(feature = "parallel")]
17use rayon::prelude::*;
18
19use crate::ruvector_native::{
20 Domain, SemanticVector, GraphNode, GraphEdge, EdgeType,
21 CoherenceSnapshot, DiscoveredPattern, PatternType, Evidence, CrossDomainLink,
22};
23
24#[derive(Debug, Default)]
26pub struct PerformanceMetrics {
27 pub vector_comparisons: AtomicU64,
29 pub cache_hits: AtomicU64,
31 pub mincut_time_ns: AtomicU64,
33 pub similarity_time_ns: AtomicU64,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct OptimizedConfig {
40 pub similarity_threshold: f64,
42 pub mincut_sensitivity: f64,
44 pub cross_domain: bool,
46 pub batch_size: usize,
48 pub use_simd: bool,
50 pub similarity_cache_size: usize,
52 pub significance_threshold: f64,
54 pub causality_lookback: usize,
56 pub causality_min_correlation: f64,
58}
59
60impl Default for OptimizedConfig {
61 fn default() -> Self {
62 Self {
63 similarity_threshold: 0.65,
64 mincut_sensitivity: 0.12,
65 cross_domain: true,
66 batch_size: 256,
67 use_simd: true,
68 similarity_cache_size: 10000,
69 significance_threshold: 0.05,
70 causality_lookback: 10,
71 causality_min_correlation: 0.6,
72 }
73 }
74}
75
76pub struct OptimizedDiscoveryEngine {
78 config: OptimizedConfig,
79 vectors: Vec<SemanticVector>,
80 nodes: HashMap<u32, GraphNode>,
81 edges: Vec<GraphEdge>,
82 coherence_history: Vec<(DateTime<Utc>, f64, CoherenceSnapshot)>,
83 next_node_id: u32,
84 domain_nodes: HashMap<Domain, Vec<u32>>,
85
86 similarity_cache: HashMap<(usize, usize), f32>,
88 adjacency_dirty: bool,
89 cached_adjacency: Option<Vec<Vec<f64>>>,
90 metrics: PerformanceMetrics,
91
92 domain_timeseries: HashMap<Domain, Vec<(DateTime<Utc>, f64)>>,
94}
95
96impl OptimizedDiscoveryEngine {
97 pub fn new(config: OptimizedConfig) -> Self {
99 Self {
100 config,
101 vectors: Vec::with_capacity(1000),
102 nodes: HashMap::with_capacity(1000),
103 edges: Vec::with_capacity(5000),
104 coherence_history: Vec::with_capacity(100),
105 next_node_id: 0,
106 domain_nodes: HashMap::new(),
107 similarity_cache: HashMap::with_capacity(10000),
108 adjacency_dirty: true,
109 cached_adjacency: None,
110 metrics: PerformanceMetrics::default(),
111 domain_timeseries: HashMap::new(),
112 }
113 }
114
115 #[cfg(feature = "parallel")]
117 pub fn add_vectors_batch(&mut self, vectors: Vec<SemanticVector>) -> Vec<u32> {
118 let start_id = self.next_node_id;
119 let num_new = vectors.len();
120
121 let new_ids: Vec<u32> = (start_id..start_id + num_new as u32).collect();
123
124 for (i, vector) in vectors.into_iter().enumerate() {
125 let node_id = start_id + i as u32;
126 let vector_idx = self.vectors.len();
127
128 let node = GraphNode {
129 id: node_id,
130 external_id: vector.id.clone(),
131 domain: vector.domain,
132 vector_idx: Some(vector_idx),
133 weight: 1.0,
134 attributes: HashMap::new(),
135 };
136
137 self.domain_nodes.entry(vector.domain).or_default().push(node_id);
138 self.nodes.insert(node_id, node);
139 self.vectors.push(vector);
140 }
141
142 self.next_node_id = start_id + num_new as u32;
143
144 self.compute_batch_similarities_parallel(&new_ids);
146
147 self.adjacency_dirty = true;
148 new_ids
149 }
150
151 #[cfg(feature = "parallel")]
153 fn compute_batch_similarities_parallel(&mut self, new_ids: &[u32]) {
154 let threshold = self.config.similarity_threshold as f32;
155 let use_simd = self.config.use_simd;
156
157 let all_vectors: Vec<(u32, &[f32], Domain)> = self.nodes.iter()
159 .filter_map(|(&id, node)| {
160 node.vector_idx.map(|idx| (id, self.vectors[idx].embedding.as_slice(), node.domain))
161 })
162 .collect();
163
164 let new_edges: Vec<GraphEdge> = new_ids.par_iter()
166 .flat_map(|&new_id| {
167 let new_node = match self.nodes.get(&new_id) {
168 Some(n) => n,
169 None => return vec![],
170 };
171
172 let new_vec_idx = match new_node.vector_idx {
173 Some(idx) => idx,
174 None => return vec![],
175 };
176
177 let new_vec = self.vectors[new_vec_idx].embedding.as_slice();
178 let new_domain = new_node.domain;
179
180 all_vectors.iter()
181 .filter(|(id, _, _)| *id != new_id)
182 .filter_map(|(other_id, other_vec, other_domain)| {
183 let similarity = if use_simd {
184 simd_cosine_similarity(new_vec, other_vec)
185 } else {
186 cosine_similarity(new_vec, other_vec)
187 };
188
189 if similarity >= threshold {
190 let edge_type = if new_domain != *other_domain {
191 EdgeType::CrossDomain
192 } else {
193 EdgeType::Similarity
194 };
195
196 Some(GraphEdge {
197 source: new_id,
198 target: *other_id,
199 weight: similarity as f64,
200 edge_type,
201 timestamp: Utc::now(),
202 })
203 } else {
204 None
205 }
206 })
207 .collect::<Vec<_>>()
208 })
209 .collect();
210
211 self.edges.extend(new_edges);
212 self.metrics.vector_comparisons.fetch_add(
213 (new_ids.len() * all_vectors.len()) as u64,
214 Ordering::Relaxed
215 );
216 }
217
218 pub fn add_vector(&mut self, vector: SemanticVector) -> u32 {
220 #[cfg(feature = "parallel")]
221 {
222 self.add_vectors_batch(vec![vector])[0]
223 }
224
225 #[cfg(not(feature = "parallel"))]
226 {
227 let node_id = self.next_node_id;
229 self.next_node_id += 1;
230
231 let vector_idx = self.vectors.len();
232 self.vectors.push(vector.clone());
233
234 let node = GraphNode {
235 id: node_id,
236 external_id: vector.id.clone(),
237 domain: vector.domain,
238 vector_idx: Some(vector_idx),
239 weight: 1.0,
240 attributes: HashMap::new(),
241 };
242
243 self.nodes.insert(node_id, node);
244 self.domain_nodes.entry(vector.domain).or_default().push(node_id);
245 self.connect_similar_vectors(node_id);
246 self.adjacency_dirty = true;
247
248 node_id
249 }
250 }
251
252 #[cfg(not(feature = "parallel"))]
253 fn connect_similar_vectors(&mut self, node_id: u32) {
254 let node = match self.nodes.get(&node_id) {
255 Some(n) => n.clone(),
256 None => return,
257 };
258
259 let vector_idx = match node.vector_idx {
260 Some(idx) => idx,
261 None => return,
262 };
263
264 let source_vec = &self.vectors[vector_idx].embedding;
265 let threshold = self.config.similarity_threshold as f32;
266
267 for (other_id, other_node) in &self.nodes {
268 if *other_id == node_id {
269 continue;
270 }
271
272 if let Some(other_idx) = other_node.vector_idx {
273 let other_vec = &self.vectors[other_idx].embedding;
274 let similarity = if self.config.use_simd {
275 simd_cosine_similarity(source_vec, other_vec)
276 } else {
277 cosine_similarity(source_vec, other_vec)
278 };
279
280 if similarity >= threshold {
281 let edge_type = if node.domain != other_node.domain {
282 EdgeType::CrossDomain
283 } else {
284 EdgeType::Similarity
285 };
286
287 self.edges.push(GraphEdge {
288 source: node_id,
289 target: *other_id,
290 weight: similarity as f64,
291 edge_type,
292 timestamp: Utc::now(),
293 });
294 }
295 }
296 }
297 }
298
299 pub fn compute_coherence(&mut self) -> CoherenceSnapshot {
301 if self.nodes.is_empty() || self.edges.is_empty() {
302 return CoherenceSnapshot {
303 mincut_value: 0.0,
304 node_count: self.nodes.len(),
305 edge_count: self.edges.len(),
306 partition_sizes: (0, 0),
307 boundary_nodes: vec![],
308 avg_edge_weight: 0.0,
309 };
310 }
311
312 let start = std::time::Instant::now();
313
314 let adj = if self.adjacency_dirty || self.cached_adjacency.is_none() {
316 let new_adj = self.build_adjacency_matrix();
317 self.cached_adjacency = Some(new_adj.clone());
318 self.adjacency_dirty = false;
319 new_adj
320 } else {
321 self.cached_adjacency.clone().unwrap()
322 };
323
324 let mincut_result = self.stoer_wagner_optimized(&adj);
325
326 self.metrics.mincut_time_ns.fetch_add(
327 start.elapsed().as_nanos() as u64,
328 Ordering::Relaxed
329 );
330
331 let avg_edge_weight = if self.edges.is_empty() {
332 0.0
333 } else {
334 self.edges.iter().map(|e| e.weight).sum::<f64>() / self.edges.len() as f64
335 };
336
337 CoherenceSnapshot {
338 mincut_value: mincut_result.0,
339 node_count: self.nodes.len(),
340 edge_count: self.edges.len(),
341 partition_sizes: mincut_result.1,
342 boundary_nodes: mincut_result.2,
343 avg_edge_weight,
344 }
345 }
346
347 fn build_adjacency_matrix(&self) -> Vec<Vec<f64>> {
349 let n = self.nodes.len();
350 let node_ids: Vec<u32> = self.nodes.keys().copied().collect();
351 let id_to_idx: HashMap<u32, usize> = node_ids.iter()
352 .enumerate()
353 .map(|(i, &id)| (id, i))
354 .collect();
355
356 let mut adj = vec![vec![0.0; n]; n];
357
358 for edge in &self.edges {
359 if let (Some(&i), Some(&j)) = (id_to_idx.get(&edge.source), id_to_idx.get(&edge.target)) {
360 adj[i][j] += edge.weight;
361 adj[j][i] += edge.weight;
362 }
363 }
364
365 adj
366 }
367
368 fn stoer_wagner_optimized(&self, adj: &[Vec<f64>]) -> (f64, (usize, usize), Vec<u32>) {
370 let n = adj.len();
371 if n < 2 {
372 return (0.0, (n, 0), vec![]);
373 }
374
375 let node_ids: Vec<u32> = self.nodes.keys().copied().collect();
376
377 let mut adj = adj.to_vec();
378 let mut best_cut = f64::INFINITY;
379 let mut best_partition = (0, 0);
380 let mut best_boundary = vec![];
381
382 let mut active: Vec<bool> = vec![true; n];
383 let mut merged: Vec<Vec<usize>> = (0..n).map(|i| vec![i]).collect();
384
385 let early_term_threshold = 0.001;
387
388 for phase in 0..(n - 1) {
389 let mut in_a = vec![false; n];
390 let mut key = vec![0.0; n];
391
392 let start = match (0..n).find(|&i| active[i]) {
393 Some(s) => s,
394 None => break,
395 };
396 in_a[start] = true;
397
398 for j in 0..n {
399 if active[j] && !in_a[j] {
400 key[j] = adj[start][j];
401 }
402 }
403
404 let mut s = start;
405 let mut t = start;
406
407 for _ in 1..=(n - 1 - phase) {
408 let (max_node, max_key) = (0..n)
409 .filter(|&j| active[j] && !in_a[j])
410 .map(|j| (j, key[j]))
411 .max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
412 .unwrap_or((0, 0.0));
413
414 s = t;
415 t = max_node;
416 in_a[t] = true;
417
418 for j in 0..n {
419 if active[j] && !in_a[j] {
420 key[j] += adj[t][j];
421 }
422 }
423 }
424
425 let cut_weight = key[t];
426
427 if cut_weight < best_cut {
428 best_cut = cut_weight;
429
430 let partition_a: Vec<usize> = merged[t].clone();
431 let partition_b_count = (0..n)
432 .filter(|&i| active[i] && i != t)
433 .map(|i| merged[i].len())
434 .sum();
435
436 best_partition = (partition_a.len(), partition_b_count);
437 best_boundary = partition_a.iter()
438 .filter_map(|&i| node_ids.get(i).copied())
439 .collect();
440
441 if best_cut < early_term_threshold {
443 break;
444 }
445 }
446
447 active[t] = false;
449 let to_merge: Vec<usize> = merged[t].clone();
450 merged[s].extend(to_merge);
451
452 for i in 0..n {
453 if active[i] && i != s {
454 adj[s][i] += adj[t][i];
455 adj[i][s] += adj[i][t];
456 }
457 }
458 }
459
460 (best_cut, best_partition, best_boundary)
461 }
462
463 pub fn detect_patterns_with_significance(&mut self) -> Vec<SignificantPattern> {
465 let mut patterns = Vec::new();
466 let current = self.compute_coherence();
467 let now = Utc::now();
468
469 for domain in [Domain::Climate, Domain::Finance, Domain::Research] {
471 if let Some(coh) = self.domain_coherence(domain) {
472 self.domain_timeseries.entry(domain).or_default().push((now, coh));
473 }
474 }
475
476 if let Some((_, prev_mincut, prev_snapshot)) = self.coherence_history.last() {
477 let mincut_delta = current.mincut_value - prev_mincut;
478
479 let significance = self.compute_significance(mincut_delta);
481
482 if mincut_delta.abs() > self.config.mincut_sensitivity {
483 let pattern_type = if mincut_delta < 0.0 {
484 PatternType::CoherenceBreak
485 } else {
486 PatternType::Consolidation
487 };
488
489 let relative_change = if *prev_mincut > 0.0 {
490 mincut_delta.abs() / prev_mincut
491 } else {
492 mincut_delta.abs()
493 };
494
495 patterns.push(SignificantPattern {
496 pattern: DiscoveredPattern {
497 id: format!("{}_{}", pattern_type_name(&pattern_type), now.timestamp()),
498 pattern_type,
499 confidence: (relative_change.min(1.0) * 0.5 + 0.5),
500 affected_nodes: current.boundary_nodes.clone(),
501 detected_at: now,
502 description: format!(
503 "Min-cut changed {:.3} → {:.3} ({:+.1}%)",
504 prev_mincut, current.mincut_value, relative_change * 100.0
505 ),
506 evidence: vec![
507 Evidence {
508 evidence_type: "mincut_delta".to_string(),
509 value: mincut_delta,
510 description: "Change in min-cut value".to_string(),
511 },
512 ],
513 cross_domain_links: vec![],
514 },
515 p_value: significance.p_value,
516 effect_size: significance.effect_size,
517 confidence_interval: significance.confidence_interval,
518 is_significant: significance.p_value < self.config.significance_threshold,
519 });
520 }
521 }
522
523 if self.config.cross_domain {
525 patterns.extend(self.detect_causality_patterns());
526 }
527
528 self.coherence_history.push((now, current.mincut_value, current));
529
530 patterns
531 }
532
533 fn compute_significance(&self, delta: f64) -> SignificanceResult {
535 if self.coherence_history.len() < 3 {
536 return SignificanceResult {
537 p_value: 1.0,
538 effect_size: 0.0,
539 confidence_interval: (0.0, 0.0),
540 };
541 }
542
543 let deltas: Vec<f64> = self.coherence_history.windows(2)
545 .map(|w| w[1].1 - w[0].1)
546 .collect();
547
548 if deltas.is_empty() {
549 return SignificanceResult {
550 p_value: 1.0,
551 effect_size: 0.0,
552 confidence_interval: (0.0, 0.0),
553 };
554 }
555
556 let mean: f64 = deltas.iter().sum::<f64>() / deltas.len() as f64;
557 let variance: f64 = deltas.iter()
558 .map(|d| (d - mean).powi(2))
559 .sum::<f64>() / deltas.len() as f64;
560 let std_dev = variance.sqrt();
561
562 if std_dev < 1e-10 {
563 return SignificanceResult {
564 p_value: if delta.abs() > 1e-10 { 0.01 } else { 1.0 },
565 effect_size: delta / (delta.abs() + 1e-10),
566 confidence_interval: (delta - 0.01, delta + 0.01),
567 };
568 }
569
570 let z_score = (delta - mean) / std_dev;
572
573 let p_value = 2.0 * (1.0 - normal_cdf(z_score.abs()));
575
576 let effect_size = delta / std_dev;
578
579 let margin = 1.96 * std_dev / (deltas.len() as f64).sqrt();
581 let confidence_interval = (delta - margin, delta + margin);
582
583 SignificanceResult {
584 p_value,
585 effect_size,
586 confidence_interval,
587 }
588 }
589
590 fn detect_causality_patterns(&self) -> Vec<SignificantPattern> {
592 let mut patterns = Vec::new();
593
594 let domains: Vec<Domain> = self.domain_timeseries.keys().copied().collect();
595
596 for i in 0..domains.len() {
597 for j in 0..domains.len() {
598 if i == j {
599 continue;
600 }
601
602 let domain_a = domains[i];
603 let domain_b = domains[j];
604
605 if let Some(causality) = self.granger_causality(domain_a, domain_b) {
606 if causality.f_statistic > 3.0 && causality.correlation.abs() > self.config.causality_min_correlation {
607 patterns.push(SignificantPattern {
608 pattern: DiscoveredPattern {
609 id: format!("causality_{:?}_{:?}_{}", domain_a, domain_b, Utc::now().timestamp()),
610 pattern_type: PatternType::Cascade,
611 confidence: causality.correlation.abs(),
612 affected_nodes: vec![],
613 detected_at: Utc::now(),
614 description: format!(
615 "{:?} → {:?} causality detected (F={:.2}, lag={}, r={:.3})",
616 domain_a, domain_b, causality.f_statistic, causality.optimal_lag, causality.correlation
617 ),
618 evidence: vec![
619 Evidence {
620 evidence_type: "f_statistic".to_string(),
621 value: causality.f_statistic,
622 description: "Granger F-statistic".to_string(),
623 },
624 Evidence {
625 evidence_type: "correlation".to_string(),
626 value: causality.correlation,
627 description: "Cross-correlation at optimal lag".to_string(),
628 },
629 ],
630 cross_domain_links: vec![CrossDomainLink {
631 source_domain: domain_a,
632 target_domain: domain_b,
633 source_nodes: vec![],
634 target_nodes: vec![],
635 link_strength: causality.correlation.abs(),
636 link_type: format!("temporal_causality_lag_{}", causality.optimal_lag),
637 }],
638 },
639 p_value: causality.p_value,
640 effect_size: causality.correlation,
641 confidence_interval: (causality.correlation - 0.1, causality.correlation + 0.1),
642 is_significant: causality.p_value < self.config.significance_threshold,
643 });
644 }
645 }
646 }
647 }
648
649 patterns
650 }
651
652 fn granger_causality(&self, cause: Domain, effect: Domain) -> Option<CausalityResult> {
654 let cause_series = self.domain_timeseries.get(&cause)?;
655 let effect_series = self.domain_timeseries.get(&effect)?;
656
657 let lookback = self.config.causality_lookback.min(cause_series.len() / 2);
658 if lookback < 2 || cause_series.len() < lookback * 2 || effect_series.len() < lookback * 2 {
659 return None;
660 }
661
662 let mut best_lag = 0;
664 let mut best_corr = 0.0_f64;
665
666 for lag in 1..=lookback {
667 let corr = cross_correlation(
668 &cause_series.iter().map(|x| x.1).collect::<Vec<_>>(),
669 &effect_series.iter().map(|x| x.1).collect::<Vec<_>>(),
670 lag as i32,
671 );
672
673 if corr.abs() > best_corr.abs() {
674 best_corr = corr;
675 best_lag = lag;
676 }
677 }
678
679 let n = effect_series.len() - best_lag;
681 let r_squared = best_corr.powi(2);
682 let f_statistic = if r_squared < 1.0 {
683 (r_squared * (n as f64 - 2.0)) / (1.0 - r_squared)
684 } else {
685 0.0
686 };
687
688 let p_value = f_to_p(f_statistic, 1, (n - 2).max(1));
690
691 Some(CausalityResult {
692 optimal_lag: best_lag,
693 correlation: best_corr,
694 f_statistic,
695 p_value,
696 })
697 }
698
699 pub fn domain_coherence(&self, domain: Domain) -> Option<f64> {
701 let domain_node_ids = self.domain_nodes.get(&domain)?;
702
703 if domain_node_ids.len() < 2 {
704 return None;
705 }
706
707 let mut internal_weight = 0.0;
708 let mut edge_count = 0;
709
710 for edge in &self.edges {
711 if domain_node_ids.contains(&edge.source) && domain_node_ids.contains(&edge.target) {
712 internal_weight += edge.weight;
713 edge_count += 1;
714 }
715 }
716
717 if edge_count == 0 {
718 return Some(0.0);
719 }
720
721 Some(internal_weight / edge_count as f64)
722 }
723
724 pub fn metrics(&self) -> &PerformanceMetrics {
726 &self.metrics
727 }
728
729 pub fn stats(&self) -> OptimizedStats {
731 let mut domain_counts = HashMap::new();
732 for domain in self.domain_nodes.keys() {
733 domain_counts.insert(*domain, self.domain_nodes[domain].len());
734 }
735
736 let cross_domain_edges = self.edges.iter()
737 .filter(|e| e.edge_type == EdgeType::CrossDomain)
738 .count();
739
740 OptimizedStats {
741 total_nodes: self.nodes.len(),
742 total_edges: self.edges.len(),
743 total_vectors: self.vectors.len(),
744 domain_counts,
745 cross_domain_edges,
746 history_length: self.coherence_history.len(),
747 cache_hit_rate: self.cache_hit_rate(),
748 total_comparisons: self.metrics.vector_comparisons.load(Ordering::Relaxed),
749 }
750 }
751
752 fn cache_hit_rate(&self) -> f64 {
753 let hits = self.metrics.cache_hits.load(Ordering::Relaxed);
754 let total = self.metrics.vector_comparisons.load(Ordering::Relaxed);
755 if total == 0 {
756 0.0
757 } else {
758 hits as f64 / total as f64
759 }
760 }
761}
762
763#[derive(Debug, Clone, Serialize, Deserialize)]
765pub struct SignificantPattern {
766 pub pattern: DiscoveredPattern,
768 pub p_value: f64,
770 pub effect_size: f64,
772 pub confidence_interval: (f64, f64),
774 pub is_significant: bool,
776}
777
778#[derive(Debug, Clone)]
780struct SignificanceResult {
781 p_value: f64,
782 effect_size: f64,
783 confidence_interval: (f64, f64),
784}
785
786#[derive(Debug, Clone)]
788struct CausalityResult {
789 optimal_lag: usize,
790 correlation: f64,
791 f_statistic: f64,
792 p_value: f64,
793}
794
795#[derive(Debug, Clone, Serialize, Deserialize)]
797pub struct OptimizedStats {
798 pub total_nodes: usize,
800 pub total_edges: usize,
802 pub total_vectors: usize,
804 pub domain_counts: HashMap<Domain, usize>,
806 pub cross_domain_edges: usize,
808 pub history_length: usize,
810 pub cache_hit_rate: f64,
812 pub total_comparisons: u64,
814}
815
816#[inline]
823pub fn simd_cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
824 if a.len() != b.len() || a.is_empty() {
825 return 0.0;
826 }
827
828 #[cfg(all(target_arch = "x86_64", target_feature = "avx2"))]
829 {
830 simd_cosine_avx2(a, b)
831 }
832
833 #[cfg(not(all(target_arch = "x86_64", target_feature = "avx2")))]
834 {
835 simd_cosine_chunked(a, b)
837 }
838}
839
840#[inline]
842fn simd_cosine_chunked(a: &[f32], b: &[f32]) -> f32 {
843 const CHUNK_SIZE: usize = 8;
844
845 let mut dot_sum = 0.0_f32;
846 let mut norm_a_sum = 0.0_f32;
847 let mut norm_b_sum = 0.0_f32;
848
849 let chunks = a.len() / CHUNK_SIZE;
851 for i in 0..chunks {
852 let start = i * CHUNK_SIZE;
853 let a_chunk = &a[start..start + CHUNK_SIZE];
854 let b_chunk = &b[start..start + CHUNK_SIZE];
855
856 for j in 0..CHUNK_SIZE {
857 let av = a_chunk[j];
858 let bv = b_chunk[j];
859 dot_sum += av * bv;
860 norm_a_sum += av * av;
861 norm_b_sum += bv * bv;
862 }
863 }
864
865 for i in (chunks * CHUNK_SIZE)..a.len() {
867 let av = a[i];
868 let bv = b[i];
869 dot_sum += av * bv;
870 norm_a_sum += av * av;
871 norm_b_sum += bv * bv;
872 }
873
874 let denom = (norm_a_sum * norm_b_sum).sqrt();
875 if denom < 1e-10 {
876 0.0
877 } else {
878 dot_sum / denom
879 }
880}
881
882#[cfg(all(target_arch = "x86_64", target_feature = "avx2"))]
883#[inline]
884fn simd_cosine_avx2(a: &[f32], b: &[f32]) -> f32 {
885 use std::arch::x86_64::*;
886
887 unsafe {
888 let mut dot = _mm256_setzero_ps();
889 let mut norm_a = _mm256_setzero_ps();
890 let mut norm_b = _mm256_setzero_ps();
891
892 let chunks = a.len() / 8;
893
894 for i in 0..chunks {
895 let offset = i * 8;
896 let va = _mm256_loadu_ps(a.as_ptr().add(offset));
897 let vb = _mm256_loadu_ps(b.as_ptr().add(offset));
898
899 dot = _mm256_fmadd_ps(va, vb, dot);
900 norm_a = _mm256_fmadd_ps(va, va, norm_a);
901 norm_b = _mm256_fmadd_ps(vb, vb, norm_b);
902 }
903
904 let dot_sum = hsum_avx(dot);
906 let norm_a_sum = hsum_avx(norm_a);
907 let norm_b_sum = hsum_avx(norm_b);
908
909 let mut dot_rem = 0.0_f32;
911 let mut norm_a_rem = 0.0_f32;
912 let mut norm_b_rem = 0.0_f32;
913
914 for i in (chunks * 8)..a.len() {
915 let av = a[i];
916 let bv = b[i];
917 dot_rem += av * bv;
918 norm_a_rem += av * av;
919 norm_b_rem += bv * bv;
920 }
921
922 let total_dot = dot_sum + dot_rem;
923 let total_norm_a = norm_a_sum + norm_a_rem;
924 let total_norm_b = norm_b_sum + norm_b_rem;
925
926 let denom = (total_norm_a * total_norm_b).sqrt();
927 if denom < 1e-10 {
928 0.0
929 } else {
930 total_dot / denom
931 }
932 }
933}
934
935#[cfg(all(target_arch = "x86_64", target_feature = "avx2"))]
936#[inline]
937unsafe fn hsum_avx(v: std::arch::x86_64::__m256) -> f32 {
938 use std::arch::x86_64::*;
939
940 let low = _mm256_castps256_ps128(v);
941 let high = _mm256_extractf128_ps(v, 1);
942 let sum128 = _mm_add_ps(low, high);
943 let shuf = _mm_movehdup_ps(sum128);
944 let sums = _mm_add_ps(sum128, shuf);
945 let shuf2 = _mm_movehl_ps(sums, sums);
946 let result = _mm_add_ss(sums, shuf2);
947 _mm_cvtss_f32(result)
948}
949
950#[inline]
952fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
953 if a.len() != b.len() || a.is_empty() {
954 return 0.0;
955 }
956
957 let dot: f32 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
958 let norm_a: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt();
959 let norm_b: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt();
960
961 if norm_a < 1e-10 || norm_b < 1e-10 {
962 return 0.0;
963 }
964
965 dot / (norm_a * norm_b)
966}
967
968fn normal_cdf(x: f64) -> f64 {
974 let a1 = 0.254829592;
975 let a2 = -0.284496736;
976 let a3 = 1.421413741;
977 let a4 = -1.453152027;
978 let a5 = 1.061405429;
979 let p = 0.3275911;
980
981 let sign = if x < 0.0 { -1.0 } else { 1.0 };
982 let x = x.abs() / std::f64::consts::SQRT_2;
983
984 let t = 1.0 / (1.0 + p * x);
985 let y = 1.0 - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * (-x * x).exp();
986
987 0.5 * (1.0 + sign * y)
988}
989
990fn cross_correlation(x: &[f64], y: &[f64], lag: i32) -> f64 {
992 let n = x.len().min(y.len());
993 if n < 2 {
994 return 0.0;
995 }
996
997 let (x_slice, y_slice) = if lag >= 0 {
998 let lag = lag as usize;
999 if lag >= n {
1000 return 0.0;
1001 }
1002 (&x[..n - lag], &y[lag..n])
1003 } else {
1004 let lag = (-lag) as usize;
1005 if lag >= n {
1006 return 0.0;
1007 }
1008 (&x[lag..n], &y[..n - lag])
1009 };
1010
1011 let len = x_slice.len();
1012 if len < 2 {
1013 return 0.0;
1014 }
1015
1016 let mean_x: f64 = x_slice.iter().sum::<f64>() / len as f64;
1017 let mean_y: f64 = y_slice.iter().sum::<f64>() / len as f64;
1018
1019 let mut cov = 0.0;
1020 let mut var_x = 0.0;
1021 let mut var_y = 0.0;
1022
1023 for i in 0..len {
1024 let dx = x_slice[i] - mean_x;
1025 let dy = y_slice[i] - mean_y;
1026 cov += dx * dy;
1027 var_x += dx * dx;
1028 var_y += dy * dy;
1029 }
1030
1031 let denom = (var_x * var_y).sqrt();
1032 if denom < 1e-10 {
1033 0.0
1034 } else {
1035 cov / denom
1036 }
1037}
1038
1039fn f_to_p(f: f64, _df1: usize, df2: usize) -> f64 {
1041 if df2 < 2 || f <= 0.0 {
1043 return 1.0;
1044 }
1045
1046 let x = f * (df2 as f64) / (1.0 + f * (df2 as f64));
1048 let p = 1.0 - x.powf(0.5);
1049 p.max(0.0).min(1.0)
1050}
1051
1052fn pattern_type_name(pt: &PatternType) -> &'static str {
1053 match pt {
1054 PatternType::CoherenceBreak => "coherence_break",
1055 PatternType::Consolidation => "consolidation",
1056 PatternType::EmergingCluster => "emerging_cluster",
1057 PatternType::DissolvingCluster => "dissolving_cluster",
1058 PatternType::BridgeFormation => "bridge",
1059 PatternType::AnomalousNode => "anomaly",
1060 PatternType::TemporalShift => "temporal_shift",
1061 PatternType::Cascade => "cascade",
1062 }
1063}
1064
1065#[cfg(test)]
1066mod tests {
1067 use super::*;
1068
1069 #[test]
1070 fn test_simd_cosine() {
1071 let a = vec![1.0, 0.0, 0.0, 0.0];
1072 let b = vec![1.0, 0.0, 0.0, 0.0];
1073 assert!((simd_cosine_similarity(&a, &b) - 1.0).abs() < 1e-6);
1074
1075 let c = vec![0.0, 1.0, 0.0, 0.0];
1076 assert!(simd_cosine_similarity(&a, &c).abs() < 1e-6);
1077 }
1078
1079 #[test]
1080 fn test_cross_correlation() {
1081 let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
1082 let y = vec![1.0, 2.0, 3.0, 4.0, 5.0];
1083 let corr = cross_correlation(&x, &y, 0);
1084 assert!((corr - 1.0).abs() < 1e-6);
1085 }
1086
1087 #[test]
1088 fn test_normal_cdf() {
1089 assert!((normal_cdf(0.0) - 0.5).abs() < 0.01);
1090 assert!(normal_cdf(3.0) > 0.99);
1091 assert!(normal_cdf(-3.0) < 0.01);
1092 }
1093}