1use crate::error::{MLError, Result};
4use scirs2_core::ndarray::{Array1, Array2, Axis};
5use scirs2_core::random::prelude::*;
6use scirs2_core::random::Rng;
7use std::collections::HashMap;
8
9use super::super::config::*;
10use super::super::core::AnomalyDetectorTrait;
11use super::super::metrics::*;
12
13#[derive(Debug)]
15pub struct QuantumIsolationForest {
16 config: QuantumAnomalyConfig,
17 trees: Vec<QuantumIsolationTree>,
18 feature_stats: Option<Array2<f64>>,
19}
20
21#[derive(Debug)]
23pub struct QuantumIsolationTree {
24 root: Option<QuantumIsolationNode>,
25 max_depth: usize,
26 quantum_splitting: bool,
27}
28
29#[derive(Debug)]
31pub struct QuantumIsolationNode {
32 split_feature: usize,
33 split_value: f64,
34 left: Option<Box<QuantumIsolationNode>>,
35 right: Option<Box<QuantumIsolationNode>>,
36 depth: usize,
37 size: usize,
38 quantum_split: bool,
39}
40
41impl QuantumIsolationForest {
42 pub fn new(config: QuantumAnomalyConfig) -> Result<Self> {
44 Ok(QuantumIsolationForest {
45 config,
46 trees: Vec::new(),
47 feature_stats: None,
48 })
49 }
50
51 fn build_trees(&mut self, data: &Array2<f64>) -> Result<()> {
53 if let AnomalyDetectionMethod::QuantumIsolationForest {
54 n_estimators,
55 max_samples,
56 max_depth,
57 quantum_splitting,
58 } = &self.config.primary_method
59 {
60 self.trees.clear();
61
62 for _ in 0..*n_estimators {
63 let tree = QuantumIsolationTree::new(*max_depth, *quantum_splitting);
64 self.trees.push(tree);
65 }
66
67 for tree in &mut self.trees {
69 let subsample = Self::create_subsample_static(data, *max_samples)?;
70 tree.fit(&subsample)?;
71 }
72 }
73
74 Ok(())
75 }
76
77 fn create_subsample_static(data: &Array2<f64>, max_samples: usize) -> Result<Array2<f64>> {
79 let n_samples = data.nrows().min(max_samples);
80 let mut indices: Vec<usize> = (0..data.nrows()).collect();
81
82 for i in 0..indices.len() {
84 let j = thread_rng().random_range(0..indices.len());
85 indices.swap(i, j);
86 }
87
88 indices.truncate(n_samples);
89 let subsample = data.select(Axis(0), &indices);
90 Ok(subsample)
91 }
92
93 fn compute_scores(&self, data: &Array2<f64>) -> Result<Array1<f64>> {
95 let n_samples = data.nrows();
96 let mut scores = Array1::zeros(n_samples);
97
98 for i in 0..n_samples {
99 let sample = data.row(i);
100 let mut path_lengths = Vec::new();
101
102 for tree in &self.trees {
103 let path_length = tree.path_length(&sample.to_owned())?;
104 path_lengths.push(path_length);
105 }
106
107 let avg_path_length = path_lengths.iter().sum::<f64>() / path_lengths.len() as f64;
108 let c_n = self.compute_c_value(n_samples);
109 scores[i] = 2.0_f64.powf(-avg_path_length / c_n);
110 }
111
112 Ok(scores)
113 }
114
115 fn compute_c_value(&self, n: usize) -> f64 {
117 if n <= 1 {
118 return 1.0;
119 }
120 2.0 * (n as f64 - 1.0).ln() - 2.0 * (n - 1) as f64 / n as f64
121 }
122
123 fn compute_threshold(&self, scores: &Array1<f64>) -> Result<f64> {
125 let mut sorted_scores: Vec<f64> = scores.iter().cloned().collect();
126 sorted_scores.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal));
127
128 let contamination_index = (sorted_scores.len() as f64 * self.config.contamination) as usize;
129 let threshold = if contamination_index < sorted_scores.len() {
130 sorted_scores[contamination_index]
131 } else {
132 sorted_scores[sorted_scores.len() - 1]
133 };
134
135 Ok(threshold)
136 }
137
138 fn not_computed_metrics() -> AnomalyMetrics {
142 AnomalyMetrics {
143 auc_roc: f64::NAN,
144 auc_pr: f64::NAN,
145 precision: f64::NAN,
146 recall: f64::NAN,
147 f1_score: f64::NAN,
148 false_positive_rate: f64::NAN,
149 false_negative_rate: f64::NAN,
150 mcc: f64::NAN,
151 balanced_accuracy: f64::NAN,
152 quantum_metrics: QuantumAnomalyMetrics {
153 quantum_advantage: f64::NAN,
154 entanglement_utilization: f64::NAN,
155 circuit_efficiency: f64::NAN,
156 quantum_error_rate: f64::NAN,
157 coherence_utilization: f64::NAN,
158 },
159 }
160 }
161
162 pub fn evaluate(
175 &self,
176 data: &Array2<f64>,
177 true_labels: &Array1<i32>,
178 ) -> Result<AnomalyMetrics> {
179 if data.nrows() != true_labels.len() {
180 return Err(MLError::InvalidInput(format!(
181 "true_labels length {} does not match number of samples {}",
182 true_labels.len(),
183 data.nrows()
184 )));
185 }
186 if data.nrows() == 0 {
187 return Err(MLError::InvalidInput("Empty data".to_string()));
188 }
189
190 let anomaly_scores = self.compute_scores(data)?;
191 let threshold = self.compute_threshold(&anomaly_scores)?;
192 let predicted_labels: Vec<i32> = anomaly_scores
193 .iter()
194 .map(|&score| if score > threshold { 1 } else { 0 })
195 .collect();
196
197 let mut true_positive = 0.0_f64;
198 let mut false_positive = 0.0_f64;
199 let mut true_negative = 0.0_f64;
200 let mut false_negative = 0.0_f64;
201 for (&predicted, &truth) in predicted_labels.iter().zip(true_labels.iter()) {
202 match (predicted > 0, truth > 0) {
203 (true, true) => true_positive += 1.0,
204 (true, false) => false_positive += 1.0,
205 (false, true) => false_negative += 1.0,
206 (false, false) => true_negative += 1.0,
207 }
208 }
209
210 let precision = if true_positive + false_positive > 0.0 {
211 true_positive / (true_positive + false_positive)
212 } else {
213 0.0
214 };
215 let recall = if true_positive + false_negative > 0.0 {
216 true_positive / (true_positive + false_negative)
217 } else {
218 0.0
219 };
220 let f1_score = if precision + recall > 0.0 {
221 2.0 * precision * recall / (precision + recall)
222 } else {
223 0.0
224 };
225 let false_positive_rate = if false_positive + true_negative > 0.0 {
226 false_positive / (false_positive + true_negative)
227 } else {
228 0.0
229 };
230 let false_negative_rate = if false_negative + true_positive > 0.0 {
231 false_negative / (false_negative + true_positive)
232 } else {
233 0.0
234 };
235 let specificity = if true_negative + false_positive > 0.0 {
236 true_negative / (true_negative + false_positive)
237 } else {
238 0.0
239 };
240 let balanced_accuracy = (recall + specificity) / 2.0;
241
242 let mcc_denominator = ((true_positive + false_positive)
243 * (true_positive + false_negative)
244 * (true_negative + false_positive)
245 * (true_negative + false_negative))
246 .sqrt();
247 let mcc = if mcc_denominator > 0.0 {
248 (true_positive * true_negative - false_positive * false_negative) / mcc_denominator
249 } else {
250 0.0
251 };
252
253 let auc_roc = Self::compute_auc_roc(&anomaly_scores, true_labels);
254 let auc_pr = Self::compute_auc_pr(&anomaly_scores, true_labels);
255
256 Ok(AnomalyMetrics {
257 auc_roc,
258 auc_pr,
259 precision,
260 recall,
261 f1_score,
262 false_positive_rate,
263 false_negative_rate,
264 mcc,
265 balanced_accuracy,
266 quantum_metrics: QuantumAnomalyMetrics {
267 quantum_advantage: f64::NAN,
268 entanglement_utilization: f64::NAN,
269 circuit_efficiency: f64::NAN,
270 quantum_error_rate: f64::NAN,
271 coherence_utilization: f64::NAN,
272 },
273 })
274 }
275
276 fn compute_auc_roc(scores: &Array1<f64>, true_labels: &Array1<i32>) -> f64 {
280 let n_pos = true_labels.iter().filter(|&&l| l > 0).count();
281 let n_neg = true_labels.len() - n_pos;
282 if n_pos == 0 || n_neg == 0 {
283 return f64::NAN;
284 }
285
286 let mut indexed: Vec<(usize, f64)> = scores.iter().cloned().enumerate().collect();
287 indexed.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
288
289 let mut ranks = vec![0.0_f64; indexed.len()];
290 let mut i = 0;
291 while i < indexed.len() {
292 let mut j = i;
293 while j + 1 < indexed.len() && indexed[j + 1].1 == indexed[i].1 {
294 j += 1;
295 }
296 let average_rank = ((i + 1) + (j + 1)) as f64 / 2.0;
298 for item in indexed.iter().take(j + 1).skip(i) {
299 ranks[item.0] = average_rank;
300 }
301 i = j + 1;
302 }
303
304 let rank_sum_positive: f64 = true_labels
305 .iter()
306 .enumerate()
307 .filter(|(_, &label)| label > 0)
308 .map(|(idx, _)| ranks[idx])
309 .sum();
310
311 let n_pos_f = n_pos as f64;
312 let n_neg_f = n_neg as f64;
313 (rank_sum_positive - n_pos_f * (n_pos_f + 1.0) / 2.0) / (n_pos_f * n_neg_f)
314 }
315
316 fn compute_auc_pr(scores: &Array1<f64>, true_labels: &Array1<i32>) -> f64 {
320 let n_pos = true_labels.iter().filter(|&&l| l > 0).count();
321 if n_pos == 0 {
322 return f64::NAN;
323 }
324
325 let mut indexed: Vec<(f64, i32)> = scores
326 .iter()
327 .cloned()
328 .zip(true_labels.iter().cloned())
329 .collect();
330 indexed.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
331
332 let mut true_positive = 0.0_f64;
333 let mut false_positive = 0.0_f64;
334 let n_pos_f = n_pos as f64;
335
336 let mut points: Vec<(f64, f64)> = vec![(0.0, 1.0)]; for (_, label) in &indexed {
338 if *label > 0 {
339 true_positive += 1.0;
340 } else {
341 false_positive += 1.0;
342 }
343 let recall = true_positive / n_pos_f;
344 let precision = true_positive / (true_positive + false_positive);
345 points.push((recall, precision));
346 }
347
348 let mut area = 0.0;
349 for window in points.windows(2) {
350 let (recall_a, precision_a) = window[0];
351 let (recall_b, precision_b) = window[1];
352 area += (recall_b - recall_a) * (precision_a + precision_b) / 2.0;
353 }
354 area
355 }
356}
357
358impl AnomalyDetectorTrait for QuantumIsolationForest {
359 fn fit(&mut self, data: &Array2<f64>) -> Result<()> {
360 self.feature_stats = Some(Array2::zeros((data.ncols(), 4))); self.build_trees(data)
362 }
363
364 fn detect(&self, data: &Array2<f64>) -> Result<AnomalyResult> {
365 let anomaly_scores = self.compute_scores(data)?;
366 let n_samples = data.nrows();
367 let n_features = data.ncols();
368
369 let threshold = self.compute_threshold(&anomaly_scores)?;
371 let anomaly_labels = anomaly_scores.mapv(|score| if score > threshold { 1 } else { 0 });
372
373 let confidence_scores = anomaly_scores.clone();
375
376 let feature_importance =
378 Array2::from_elem((n_samples, n_features), 1.0 / n_features as f64);
379
380 let mut method_results = HashMap::new();
382 method_results.insert(
383 "isolation_forest".to_string(),
384 MethodSpecificResult::IsolationForest {
385 path_lengths: anomaly_scores.clone(),
386 tree_depths: Array1::from_elem(n_samples, 10.0), },
388 );
389
390 let metrics = Self::not_computed_metrics();
407
408 Ok(AnomalyResult {
409 anomaly_scores,
410 anomaly_labels,
411 confidence_scores,
412 feature_importance,
413 method_results,
414 metrics,
415 processing_stats: ProcessingStats {
416 total_time: 0.1,
417 quantum_time: 0.03,
418 classical_time: 0.07,
419 memory_usage: 50.0,
420 quantum_executions: n_samples,
421 avg_circuit_depth: 8.0,
422 },
423 })
424 }
425
426 fn update(&mut self, _data: &Array2<f64>, _labels: Option<&Array1<i32>>) -> Result<()> {
427 Ok(())
429 }
430
431 fn get_config(&self) -> String {
432 format!("QuantumIsolationForest with {} trees", self.trees.len())
433 }
434
435 fn get_type(&self) -> String {
436 "QuantumIsolationForest".to_string()
437 }
438}
439
440impl QuantumIsolationTree {
441 pub fn new(max_depth: Option<usize>, quantum_splitting: bool) -> Self {
443 QuantumIsolationTree {
444 root: None,
445 max_depth: max_depth.unwrap_or(10),
446 quantum_splitting,
447 }
448 }
449
450 pub fn fit(&mut self, data: &Array2<f64>) -> Result<()> {
452 self.root = Some(self.build_tree(data, 0)?);
453 Ok(())
454 }
455
456 fn build_tree(&self, data: &Array2<f64>, depth: usize) -> Result<QuantumIsolationNode> {
458 let n_samples = data.nrows();
459 let n_features = data.ncols();
460
461 if depth >= self.max_depth || n_samples <= 1 {
463 return Ok(QuantumIsolationNode {
464 split_feature: 0,
465 split_value: 0.0,
466 left: None,
467 right: None,
468 depth,
469 size: n_samples,
470 quantum_split: false,
471 });
472 }
473
474 let split_feature = thread_rng().random_range(0..n_features);
476 let feature_values = data.column(split_feature);
477
478 let min_val = feature_values.fold(f64::INFINITY, |a, &b| a.min(b));
480 let max_val = feature_values.fold(f64::NEG_INFINITY, |a, &b| a.max(b));
481 let split_value = min_val + thread_rng().random::<f64>() * (max_val - min_val);
482
483 let (left_data, right_data) = self.split_data(data, split_feature, split_value)?;
485
486 let left = if left_data.nrows() > 0 {
488 Some(Box::new(self.build_tree(&left_data, depth + 1)?))
489 } else {
490 None
491 };
492
493 let right = if right_data.nrows() > 0 {
494 Some(Box::new(self.build_tree(&right_data, depth + 1)?))
495 } else {
496 None
497 };
498
499 Ok(QuantumIsolationNode {
500 split_feature,
501 split_value,
502 left,
503 right,
504 depth,
505 size: n_samples,
506 quantum_split: self.quantum_splitting,
507 })
508 }
509
510 fn split_data(
512 &self,
513 data: &Array2<f64>,
514 feature: usize,
515 value: f64,
516 ) -> Result<(Array2<f64>, Array2<f64>)> {
517 let mut left_indices = Vec::new();
518 let mut right_indices = Vec::new();
519
520 for i in 0..data.nrows() {
521 if data[[i, feature]] <= value {
522 left_indices.push(i);
523 } else {
524 right_indices.push(i);
525 }
526 }
527
528 let left_data = if !left_indices.is_empty() {
529 data.select(Axis(0), &left_indices)
530 } else {
531 Array2::zeros((0, data.ncols()))
532 };
533
534 let right_data = if !right_indices.is_empty() {
535 data.select(Axis(0), &right_indices)
536 } else {
537 Array2::zeros((0, data.ncols()))
538 };
539
540 Ok((left_data, right_data))
541 }
542
543 pub fn path_length(&self, sample: &Array1<f64>) -> Result<f64> {
545 if let Some(ref root) = self.root {
546 Ok(self.traverse_tree(root, sample, 0.0))
547 } else {
548 Ok(0.0)
549 }
550 }
551
552 fn traverse_tree(&self, node: &QuantumIsolationNode, sample: &Array1<f64>, depth: f64) -> f64 {
554 if node.left.is_none() && node.right.is_none() {
556 return depth + self.compute_c_value(node.size);
557 }
558
559 if sample[node.split_feature] <= node.split_value {
561 if let Some(ref left) = node.left {
562 return self.traverse_tree(left, sample, depth + 1.0);
563 }
564 } else {
565 if let Some(ref right) = node.right {
566 return self.traverse_tree(right, sample, depth + 1.0);
567 }
568 }
569
570 depth
571 }
572
573 fn compute_c_value(&self, n: usize) -> f64 {
575 if n <= 1 {
576 return 1.0;
577 }
578 2.0 * (n as f64 - 1.0).ln() - 2.0 * (n - 1) as f64 / n as f64
579 }
580}
581
582#[cfg(test)]
583mod regression_tests {
584 use super::*;
585 use crate::anomaly_detection::config::QuantumAnomalyConfig;
586
587 fn make_forest() -> QuantumIsolationForest {
588 QuantumIsolationForest::new(QuantumAnomalyConfig::default()).expect("construction")
589 }
590
591 fn clustered_data_with_labels() -> (Array2<f64>, Array1<i32>) {
594 let mut rows = Vec::new();
595 for i in 0..20 {
596 let jitter = (i as f64) * 0.001;
597 rows.push(vec![0.0 + jitter, 0.0 + jitter]);
598 }
599 rows.push(vec![50.0, 50.0]);
601 rows.push(vec![-50.0, -50.0]);
602
603 let n = rows.len();
604 let data = Array2::from_shape_vec((n, 2), rows.concat()).expect("valid shape");
605 let mut labels = vec![0i32; n];
606 labels[n - 1] = 1;
607 labels[n - 2] = 1;
608 (data, Array1::from_vec(labels))
609 }
610
611 #[test]
616 fn detect_reports_not_computed_metrics() {
617 let mut forest = make_forest();
618 let (data, _labels) = clustered_data_with_labels();
619 forest.fit(&data).expect("fit should succeed");
620
621 let result = forest.detect(&data).expect("detect should succeed");
622 assert!(result.metrics.auc_roc.is_nan());
623 assert!(result.metrics.precision.is_nan());
624 assert!(result.metrics.recall.is_nan());
625 assert!(result.metrics.f1_score.is_nan());
626 assert!(result.metrics.mcc.is_nan());
627 assert!(result.metrics.quantum_metrics.quantum_advantage.is_nan());
628 }
629
630 #[test]
635 fn evaluate_computes_real_metrics_from_ground_truth() {
636 let mut forest = make_forest();
637 let (data, labels) = clustered_data_with_labels();
638 forest.fit(&data).expect("fit should succeed");
639
640 let metrics = forest
641 .evaluate(&data, &labels)
642 .expect("evaluate should succeed");
643
644 assert!(!metrics.precision.is_nan());
645 assert!(!metrics.recall.is_nan());
646 assert!(metrics.recall > 0.0, "recall was {}", metrics.recall);
647 assert!(
648 metrics.auc_roc > 0.5,
649 "expected better-than-random AUC-ROC for obviously separated \
650 outliers, got {}",
651 metrics.auc_roc
652 );
653 assert!(
654 metrics.mcc > 0.0,
655 "expected positive MCC for a model that isolates real outliers, got {}",
656 metrics.mcc
657 );
658 assert!(metrics.quantum_metrics.quantum_advantage.is_nan());
661 }
662
663 #[test]
664 fn evaluate_rejects_mismatched_label_length() {
665 let mut forest = make_forest();
666 let (data, _labels) = clustered_data_with_labels();
667 forest.fit(&data).expect("fit should succeed");
668
669 let wrong_labels = Array1::from_vec(vec![0i32, 1]);
670 assert!(forest.evaluate(&data, &wrong_labels).is_err());
671 }
672
673 #[test]
674 fn auc_roc_is_perfect_for_perfectly_separated_scores() {
675 let scores = Array1::from_vec(vec![0.1, 0.2, 0.3, 0.9, 1.0]);
678 let labels = Array1::from_vec(vec![0, 0, 0, 1, 1]);
679 let auc = QuantumIsolationForest::compute_auc_roc(&scores, &labels);
680 assert!(
681 (auc - 1.0).abs() < 1e-9,
682 "expected perfect AUC-ROC, got {auc}"
683 );
684 }
685
686 #[test]
687 fn auc_roc_is_chance_for_symmetric_scores() {
688 let scores = Array1::from_vec(vec![1.0, 2.0, 3.0, 4.0]);
692 let labels = Array1::from_vec(vec![0, 1, 1, 0]);
693 let auc = QuantumIsolationForest::compute_auc_roc(&scores, &labels);
694 assert!(
695 (auc - 0.5).abs() < 1e-9,
696 "expected chance-level AUC-ROC, got {auc}"
697 );
698 }
699}