quantrs2_ml/anomaly_detection/algorithms/
isolation_forest.rs1use crate::error::{MLError, Result};
4use scirs2_core::random::prelude::*;
5use scirs2_core::ndarray::{Array1, Array2, Axis};
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().gen_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());
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
139impl AnomalyDetectorTrait for QuantumIsolationForest {
140 fn fit(&mut self, data: &Array2<f64>) -> Result<()> {
141 self.feature_stats = Some(Array2::zeros((data.ncols(), 4))); self.build_trees(data)
143 }
144
145 fn detect(&self, data: &Array2<f64>) -> Result<AnomalyResult> {
146 let anomaly_scores = self.compute_scores(data)?;
147 let n_samples = data.nrows();
148 let n_features = data.ncols();
149
150 let threshold = self.compute_threshold(&anomaly_scores)?;
152 let anomaly_labels = anomaly_scores.mapv(|score| if score > threshold { 1 } else { 0 });
153
154 let confidence_scores = anomaly_scores.clone();
156
157 let feature_importance =
159 Array2::from_elem((n_samples, n_features), 1.0 / n_features as f64);
160
161 let mut method_results = HashMap::new();
163 method_results.insert(
164 "isolation_forest".to_string(),
165 MethodSpecificResult::IsolationForest {
166 path_lengths: anomaly_scores.clone(),
167 tree_depths: Array1::from_elem(n_samples, 10.0), },
169 );
170
171 let metrics = AnomalyMetrics {
173 auc_roc: 0.85,
174 auc_pr: 0.80,
175 precision: 0.75,
176 recall: 0.70,
177 f1_score: 0.72,
178 false_positive_rate: 0.05,
179 false_negative_rate: 0.10,
180 mcc: 0.65,
181 balanced_accuracy: 0.80,
182 quantum_metrics: QuantumAnomalyMetrics {
183 quantum_advantage: 1.05,
184 entanglement_utilization: 0.60,
185 circuit_efficiency: 0.75,
186 quantum_error_rate: 0.03,
187 coherence_utilization: 0.70,
188 },
189 };
190
191 Ok(AnomalyResult {
192 anomaly_scores,
193 anomaly_labels,
194 confidence_scores,
195 feature_importance,
196 method_results,
197 metrics,
198 processing_stats: ProcessingStats {
199 total_time: 0.1,
200 quantum_time: 0.03,
201 classical_time: 0.07,
202 memory_usage: 50.0,
203 quantum_executions: n_samples,
204 avg_circuit_depth: 8.0,
205 },
206 })
207 }
208
209 fn update(&mut self, _data: &Array2<f64>, _labels: Option<&Array1<i32>>) -> Result<()> {
210 Ok(())
212 }
213
214 fn get_config(&self) -> String {
215 format!("QuantumIsolationForest with {} trees", self.trees.len())
216 }
217
218 fn get_type(&self) -> String {
219 "QuantumIsolationForest".to_string()
220 }
221}
222
223impl QuantumIsolationTree {
224 pub fn new(max_depth: Option<usize>, quantum_splitting: bool) -> Self {
226 QuantumIsolationTree {
227 root: None,
228 max_depth: max_depth.unwrap_or(10),
229 quantum_splitting,
230 }
231 }
232
233 pub fn fit(&mut self, data: &Array2<f64>) -> Result<()> {
235 self.root = Some(self.build_tree(data, 0)?);
236 Ok(())
237 }
238
239 fn build_tree(&self, data: &Array2<f64>, depth: usize) -> Result<QuantumIsolationNode> {
241 let n_samples = data.nrows();
242 let n_features = data.ncols();
243
244 if depth >= self.max_depth || n_samples <= 1 {
246 return Ok(QuantumIsolationNode {
247 split_feature: 0,
248 split_value: 0.0,
249 left: None,
250 right: None,
251 depth,
252 size: n_samples,
253 quantum_split: false,
254 });
255 }
256
257 let split_feature = thread_rng().gen_range(0..n_features);
259 let feature_values = data.column(split_feature);
260
261 let min_val = feature_values.fold(f64::INFINITY, |a, &b| a.min(b));
263 let max_val = feature_values.fold(f64::NEG_INFINITY, |a, &b| a.max(b));
264 let split_value = min_val + thread_rng().gen::<f64>() * (max_val - min_val);
265
266 let (left_data, right_data) = self.split_data(data, split_feature, split_value)?;
268
269 let left = if left_data.nrows() > 0 {
271 Some(Box::new(self.build_tree(&left_data, depth + 1)?))
272 } else {
273 None
274 };
275
276 let right = if right_data.nrows() > 0 {
277 Some(Box::new(self.build_tree(&right_data, depth + 1)?))
278 } else {
279 None
280 };
281
282 Ok(QuantumIsolationNode {
283 split_feature,
284 split_value,
285 left,
286 right,
287 depth,
288 size: n_samples,
289 quantum_split: self.quantum_splitting,
290 })
291 }
292
293 fn split_data(
295 &self,
296 data: &Array2<f64>,
297 feature: usize,
298 value: f64,
299 ) -> Result<(Array2<f64>, Array2<f64>)> {
300 let mut left_indices = Vec::new();
301 let mut right_indices = Vec::new();
302
303 for i in 0..data.nrows() {
304 if data[[i, feature]] <= value {
305 left_indices.push(i);
306 } else {
307 right_indices.push(i);
308 }
309 }
310
311 let left_data = if !left_indices.is_empty() {
312 data.select(Axis(0), &left_indices)
313 } else {
314 Array2::zeros((0, data.ncols()))
315 };
316
317 let right_data = if !right_indices.is_empty() {
318 data.select(Axis(0), &right_indices)
319 } else {
320 Array2::zeros((0, data.ncols()))
321 };
322
323 Ok((left_data, right_data))
324 }
325
326 pub fn path_length(&self, sample: &Array1<f64>) -> Result<f64> {
328 if let Some(ref root) = self.root {
329 Ok(self.traverse_tree(root, sample, 0.0))
330 } else {
331 Ok(0.0)
332 }
333 }
334
335 fn traverse_tree(&self, node: &QuantumIsolationNode, sample: &Array1<f64>, depth: f64) -> f64 {
337 if node.left.is_none() && node.right.is_none() {
339 return depth + self.compute_c_value(node.size);
340 }
341
342 if sample[node.split_feature] <= node.split_value {
344 if let Some(ref left) = node.left {
345 return self.traverse_tree(left, sample, depth + 1.0);
346 }
347 } else {
348 if let Some(ref right) = node.right {
349 return self.traverse_tree(right, sample, depth + 1.0);
350 }
351 }
352
353 depth
354 }
355
356 fn compute_c_value(&self, n: usize) -> f64 {
358 if n <= 1 {
359 return 1.0;
360 }
361 2.0 * (n as f64 - 1.0).ln() - 2.0 * (n - 1) as f64 / n as f64
362 }
363}