ruqu_qear/
features.rs

1//! Feature extraction from quantum reservoir dynamics.
2//!
3//! This module provides methods to extract meaningful features from
4//! the reservoir state evolution, including simulated quantum state
5//! tomography and expectation value computation.
6
7use crate::error::{QearError, QearResult};
8use crate::reservoir::QuantumReservoir;
9use ndarray::{Array1, Array2, Axis};
10
11#[cfg(feature = "serde")]
12use serde::{Deserialize, Serialize};
13
14/// Feature extractor configuration.
15#[derive(Debug, Clone)]
16#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
17pub struct FeatureConfig {
18    /// Include raw reservoir states.
19    pub include_raw_states: bool,
20    /// Include squared states (probabilities).
21    pub include_probabilities: bool,
22    /// Include pairwise correlations.
23    pub include_correlations: bool,
24    /// Include temporal differences.
25    pub include_temporal_diff: bool,
26    /// Include spectral features via FFT.
27    pub include_spectral: bool,
28    /// Include statistical moments.
29    pub include_moments: bool,
30    /// Order of statistical moments to compute.
31    pub moment_order: usize,
32    /// Window size for local statistics.
33    pub window_size: usize,
34    /// Polynomial degree for nonlinear features.
35    pub polynomial_degree: usize,
36}
37
38impl Default for FeatureConfig {
39    fn default() -> Self {
40        Self {
41            include_raw_states: true,
42            include_probabilities: true,
43            include_correlations: false,
44            include_temporal_diff: true,
45            include_spectral: false,
46            include_moments: true,
47            moment_order: 4,
48            window_size: 10,
49            polynomial_degree: 2,
50        }
51    }
52}
53
54impl FeatureConfig {
55    /// Create a minimal feature configuration.
56    pub fn minimal() -> Self {
57        Self {
58            include_raw_states: true,
59            include_probabilities: false,
60            include_correlations: false,
61            include_temporal_diff: false,
62            include_spectral: false,
63            include_moments: false,
64            moment_order: 2,
65            window_size: 5,
66            polynomial_degree: 1,
67        }
68    }
69
70    /// Create a comprehensive feature configuration.
71    pub fn comprehensive() -> Self {
72        Self {
73            include_raw_states: true,
74            include_probabilities: true,
75            include_correlations: true,
76            include_temporal_diff: true,
77            include_spectral: true,
78            include_moments: true,
79            moment_order: 4,
80            window_size: 20,
81            polynomial_degree: 3,
82        }
83    }
84}
85
86/// Feature extractor for quantum reservoir states.
87#[derive(Debug, Clone)]
88#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
89pub struct FeatureExtractor {
90    /// Configuration.
91    config: FeatureConfig,
92}
93
94impl FeatureExtractor {
95    /// Create a new feature extractor.
96    pub fn new(config: FeatureConfig) -> Self {
97        Self { config }
98    }
99
100    /// Create a feature extractor with default configuration.
101    pub fn default_extractor() -> Self {
102        Self::new(FeatureConfig::default())
103    }
104
105    /// Extract features from reservoir states.
106    pub fn extract(&self, states: &Array2<f64>) -> QearResult<Array2<f64>> {
107        let n_samples = states.nrows();
108        let mut feature_sets: Vec<Array2<f64>> = Vec::new();
109
110        // Raw states
111        if self.config.include_raw_states {
112            feature_sets.push(states.clone());
113        }
114
115        // Probability amplitudes (squared states)
116        if self.config.include_probabilities {
117            let probs = states.mapv(|x| x * x);
118            feature_sets.push(probs);
119        }
120
121        // Pairwise correlations
122        if self.config.include_correlations {
123            let correlations = self.compute_pairwise_correlations(states)?;
124            feature_sets.push(correlations);
125        }
126
127        // Temporal differences
128        if self.config.include_temporal_diff && n_samples > 1 {
129            let diffs = self.compute_temporal_differences(states)?;
130            feature_sets.push(diffs);
131        }
132
133        // Statistical moments
134        if self.config.include_moments {
135            let moments = self.compute_moments(states)?;
136            feature_sets.push(moments);
137        }
138
139        // Spectral features
140        if self.config.include_spectral && n_samples >= self.config.window_size {
141            let spectral = self.compute_spectral_features(states)?;
142            feature_sets.push(spectral);
143        }
144
145        // Concatenate all features
146        if feature_sets.is_empty() {
147            return Err(QearError::feature_extraction(
148                "No features selected in configuration",
149            ));
150        }
151
152        let total_features: usize = feature_sets.iter().map(|f| f.ncols()).sum();
153        let mut result = Array2::zeros((n_samples, total_features));
154        let mut col_offset = 0;
155
156        for features in feature_sets {
157            let n_cols = features.ncols();
158            for i in 0..n_samples {
159                for j in 0..n_cols {
160                    result[[i, col_offset + j]] = features[[i, j]];
161                }
162            }
163            col_offset += n_cols;
164        }
165
166        Ok(result)
167    }
168
169    /// Compute pairwise correlations between reservoir neurons.
170    fn compute_pairwise_correlations(&self, states: &Array2<f64>) -> QearResult<Array2<f64>> {
171        let n_samples = states.nrows();
172        let n_neurons = states.ncols();
173
174        // Limit correlations to avoid explosion (take every kth neuron)
175        let max_correlations = 100;
176        let step = ((n_neurons * (n_neurons - 1) / 2) as f64 / max_correlations as f64)
177            .sqrt()
178            .ceil() as usize;
179        let step = step.max(1);
180
181        let selected_neurons: Vec<usize> = (0..n_neurons).step_by(step).collect();
182        let n_pairs = selected_neurons.len() * (selected_neurons.len() - 1) / 2;
183
184        let mut correlations = Array2::zeros((n_samples, n_pairs.max(1)));
185
186        let mut pair_idx = 0;
187        for (i, &ni) in selected_neurons.iter().enumerate() {
188            for &nj in selected_neurons.iter().skip(i + 1) {
189                for t in 0..n_samples {
190                    correlations[[t, pair_idx]] = states[[t, ni]] * states[[t, nj]];
191                }
192                pair_idx += 1;
193                if pair_idx >= n_pairs {
194                    break;
195                }
196            }
197            if pair_idx >= n_pairs {
198                break;
199            }
200        }
201
202        Ok(correlations)
203    }
204
205    /// Compute temporal differences (first derivative approximation).
206    fn compute_temporal_differences(&self, states: &Array2<f64>) -> QearResult<Array2<f64>> {
207        let n_samples = states.nrows();
208        let n_neurons = states.ncols();
209
210        let mut diffs = Array2::zeros((n_samples, n_neurons));
211
212        // First sample has zero difference
213        for j in 0..n_neurons {
214            diffs[[0, j]] = 0.0;
215        }
216
217        // Compute differences for remaining samples
218        for i in 1..n_samples {
219            for j in 0..n_neurons {
220                diffs[[i, j]] = states[[i, j]] - states[[i - 1, j]];
221            }
222        }
223
224        Ok(diffs)
225    }
226
227    /// Compute statistical moments of reservoir states.
228    fn compute_moments(&self, states: &Array2<f64>) -> QearResult<Array2<f64>> {
229        let n_samples = states.nrows();
230        let order = self.config.moment_order;
231
232        // Compute moments per sample (over neurons)
233        let mut moments = Array2::zeros((n_samples, order));
234
235        for i in 0..n_samples {
236            let row = states.row(i);
237            let mean = row.mean().unwrap_or(0.0);
238
239            // First moment: mean
240            moments[[i, 0]] = mean;
241
242            if order >= 2 {
243                // Second moment: variance
244                let variance: f64 = row.iter().map(|x| (x - mean).powi(2)).sum::<f64>()
245                    / row.len() as f64;
246                moments[[i, 1]] = variance;
247
248                if order >= 3 && variance > 1e-10 {
249                    // Third moment: skewness
250                    let std_dev = variance.sqrt();
251                    let skewness: f64 = row.iter().map(|x| ((x - mean) / std_dev).powi(3)).sum::<f64>()
252                        / row.len() as f64;
253                    moments[[i, 2]] = skewness;
254                }
255
256                if order >= 4 && variance > 1e-10 {
257                    // Fourth moment: kurtosis
258                    let std_dev = variance.sqrt();
259                    let kurtosis: f64 = row.iter().map(|x| ((x - mean) / std_dev).powi(4)).sum::<f64>()
260                        / row.len() as f64
261                        - 3.0; // Excess kurtosis
262                    moments[[i, 3]] = kurtosis;
263                }
264            }
265        }
266
267        Ok(moments)
268    }
269
270    /// Compute spectral features using simple DFT.
271    fn compute_spectral_features(&self, states: &Array2<f64>) -> QearResult<Array2<f64>> {
272        let n_samples = states.nrows();
273        let n_neurons = states.ncols();
274        let window = self.config.window_size;
275
276        // Compute power spectrum for each neuron, then aggregate
277        let n_freq = window / 2 + 1;
278        let mut spectral = Array2::zeros((n_samples, n_freq));
279
280        for i in 0..n_samples {
281            if i + 1 >= window {
282                // Compute DFT magnitudes for window ending at sample i
283                let start = i + 1 - window;
284
285                for k in 0..n_freq {
286                    let mut real_sum = 0.0;
287                    let mut imag_sum = 0.0;
288
289                    for t in 0..window {
290                        // Average over neurons
291                        let mut sample_avg = 0.0;
292                        for j in 0..n_neurons {
293                            sample_avg += states[[start + t, j]];
294                        }
295                        sample_avg /= n_neurons as f64;
296
297                        let angle = -2.0 * std::f64::consts::PI * k as f64 * t as f64 / window as f64;
298                        real_sum += sample_avg * angle.cos();
299                        imag_sum += sample_avg * angle.sin();
300                    }
301
302                    spectral[[i, k]] = (real_sum * real_sum + imag_sum * imag_sum).sqrt() / window as f64;
303                }
304            }
305        }
306
307        Ok(spectral)
308    }
309
310    /// Extract features from a single state vector.
311    pub fn extract_single(&self, state: &Array1<f64>) -> QearResult<Array1<f64>> {
312        let states = state.clone().insert_axis(Axis(0));
313        let features = self.extract(&states)?;
314        Ok(features.row(0).to_owned())
315    }
316
317    /// Get the configuration.
318    pub fn config(&self) -> &FeatureConfig {
319        &self.config
320    }
321}
322
323/// Simulated quantum state tomography.
324///
325/// Extracts information about the quantum state by measuring in different bases.
326#[derive(Debug, Clone)]
327#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
328pub struct QuantumTomography {
329    /// Number of measurement bases.
330    num_bases: usize,
331}
332
333impl QuantumTomography {
334    /// Create a new quantum tomography instance.
335    pub fn new(num_bases: usize) -> Self {
336        Self { num_bases }
337    }
338
339    /// Perform simulated tomography on reservoir.
340    pub fn measure(&self, reservoir: &QuantumReservoir) -> QearResult<Array1<f64>> {
341        let (real, imag) = reservoir.complex_state();
342        let n = real.len();
343
344        // For each basis, compute expectation values
345        let mut measurements = Vec::with_capacity(self.num_bases * 3);
346
347        // Pauli-Z measurements (computational basis)
348        let z_expectations = self.pauli_z_expectation(real, imag);
349        measurements.extend(z_expectations.iter().take(self.num_bases.min(n)));
350
351        // Pauli-X measurements (Hadamard basis)
352        let x_expectations = self.pauli_x_expectation(real, imag);
353        measurements.extend(x_expectations.iter().take(self.num_bases.min(n)));
354
355        // Pauli-Y measurements
356        let y_expectations = self.pauli_y_expectation(real, imag);
357        measurements.extend(y_expectations.iter().take(self.num_bases.min(n)));
358
359        Ok(Array1::from_vec(measurements))
360    }
361
362    /// Compute Pauli-Z expectation values.
363    fn pauli_z_expectation(&self, real: &Array1<f64>, imag: &Array1<f64>) -> Vec<f64> {
364        let n = real.len();
365        let n_qubits = (n as f64).log2().ceil() as usize;
366
367        let mut expectations = Vec::with_capacity(n_qubits);
368
369        for q in 0..n_qubits {
370            let mut expectation = 0.0;
371            for i in 0..n {
372                let prob = real[i] * real[i] + imag[i] * imag[i];
373                // +1 if qubit q is 0, -1 if qubit q is 1
374                let sign = if (i >> q) & 1 == 0 { 1.0 } else { -1.0 };
375                expectation += sign * prob;
376            }
377            expectations.push(expectation);
378        }
379
380        expectations
381    }
382
383    /// Compute Pauli-X expectation values (approximate).
384    fn pauli_x_expectation(&self, real: &Array1<f64>, imag: &Array1<f64>) -> Vec<f64> {
385        let n = real.len();
386        let n_qubits = (n as f64).log2().ceil() as usize;
387
388        let mut expectations = Vec::with_capacity(n_qubits);
389
390        for q in 0..n_qubits {
391            let mut expectation = 0.0;
392            for i in 0..n {
393                // X flips qubit q
394                let j = i ^ (1 << q);
395                if j < n {
396                    // <i|X|j> contribution
397                    expectation += real[i] * real[j] + imag[i] * imag[j];
398                }
399            }
400            expectations.push(expectation);
401        }
402
403        expectations
404    }
405
406    /// Compute Pauli-Y expectation values (approximate).
407    fn pauli_y_expectation(&self, real: &Array1<f64>, imag: &Array1<f64>) -> Vec<f64> {
408        let n = real.len();
409        let n_qubits = (n as f64).log2().ceil() as usize;
410
411        let mut expectations = Vec::with_capacity(n_qubits);
412
413        for q in 0..n_qubits {
414            let mut expectation = 0.0;
415            for i in 0..n {
416                let j = i ^ (1 << q);
417                if j < n {
418                    // Y = i * |0><1| - i * |1><0|
419                    let sign = if (i >> q) & 1 == 0 { 1.0 } else { -1.0 };
420                    expectation += sign * (real[i] * imag[j] - imag[i] * real[j]);
421                }
422            }
423            expectations.push(expectation);
424        }
425
426        expectations
427    }
428
429    /// Get the number of measurement bases.
430    pub fn num_bases(&self) -> usize {
431        self.num_bases
432    }
433}
434
435/// Compute expectation values of observables.
436#[derive(Debug, Clone)]
437#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
438pub struct ExpectationComputer {
439    /// Observables as matrices (diagonal elements for efficiency).
440    observables: Vec<Array1<f64>>,
441}
442
443impl ExpectationComputer {
444    /// Create a new expectation computer with default observables.
445    pub fn new(n_qubits: usize) -> Self {
446        let size = 1 << n_qubits;
447        let mut observables = Vec::new();
448
449        // Number operator for each computational basis state
450        for i in 0..size {
451            let mut observable = Array1::zeros(size);
452            observable[i] = 1.0;
453            observables.push(observable);
454        }
455
456        Self { observables }
457    }
458
459    /// Create with custom observables.
460    pub fn with_observables(observables: Vec<Array1<f64>>) -> Self {
461        Self { observables }
462    }
463
464    /// Add an observable.
465    pub fn add_observable(&mut self, observable: Array1<f64>) {
466        self.observables.push(observable);
467    }
468
469    /// Compute expectation values for all observables.
470    pub fn compute(&self, reservoir: &QuantumReservoir) -> QearResult<Array1<f64>> {
471        let probs = reservoir.probability_amplitudes();
472        let mut expectations = Vec::with_capacity(self.observables.len());
473
474        for observable in &self.observables {
475            if observable.len() != probs.len() {
476                return Err(QearError::dimension_mismatch(observable.len(), probs.len()));
477            }
478            let expectation: f64 = observable.iter().zip(probs.iter()).map(|(o, p)| o * p).sum();
479            expectations.push(expectation);
480        }
481
482        Ok(Array1::from_vec(expectations))
483    }
484
485    /// Compute expectation value for a single observable.
486    pub fn compute_single(
487        &self,
488        reservoir: &QuantumReservoir,
489        observable_idx: usize,
490    ) -> QearResult<f64> {
491        if observable_idx >= self.observables.len() {
492            return Err(QearError::invalid_parameter(
493                "observable_idx",
494                format!("Index {} out of bounds", observable_idx),
495            ));
496        }
497
498        let probs = reservoir.probability_amplitudes();
499        let observable = &self.observables[observable_idx];
500        let expectation: f64 = observable.iter().zip(probs.iter()).map(|(o, p)| o * p).sum();
501
502        Ok(expectation)
503    }
504
505    /// Get the number of observables.
506    pub fn num_observables(&self) -> usize {
507        self.observables.len()
508    }
509}
510
511#[cfg(test)]
512mod tests {
513    use super::*;
514    use crate::reservoir::ReservoirConfig;
515
516    #[test]
517    fn test_feature_config_default() {
518        let config = FeatureConfig::default();
519        assert!(config.include_raw_states);
520        assert!(config.include_probabilities);
521    }
522
523    #[test]
524    fn test_feature_extractor_creation() {
525        let extractor = FeatureExtractor::default_extractor();
526        assert!(extractor.config().include_raw_states);
527    }
528
529    #[test]
530    fn test_feature_extraction() {
531        let config = FeatureConfig::minimal();
532        let extractor = FeatureExtractor::new(config);
533
534        let states = Array2::from_shape_fn((10, 16), |(i, j)| {
535            ((i + j) as f64 / 26.0).sin()
536        });
537
538        let features = extractor.extract(&states).unwrap();
539        assert_eq!(features.nrows(), 10);
540        assert!(features.ncols() >= 16); // At least raw states
541    }
542
543    #[test]
544    fn test_feature_extraction_comprehensive() {
545        let config = FeatureConfig::comprehensive();
546        let extractor = FeatureExtractor::new(config);
547
548        let states = Array2::from_shape_fn((50, 16), |(i, j)| {
549            ((i + j) as f64 / 66.0).sin()
550        });
551
552        let features = extractor.extract(&states).unwrap();
553        assert_eq!(features.nrows(), 50);
554        assert!(features.ncols() > 16); // More than just raw states
555    }
556
557    #[test]
558    fn test_temporal_differences() {
559        let extractor = FeatureExtractor::default_extractor();
560
561        let states = Array2::from_shape_vec(
562            (3, 2),
563            vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
564        )
565        .unwrap();
566
567        let diffs = extractor.compute_temporal_differences(&states).unwrap();
568
569        assert_eq!(diffs[[0, 0]], 0.0);
570        assert_eq!(diffs[[0, 1]], 0.0);
571        assert!((diffs[[1, 0]] - 2.0).abs() < 1e-10);
572        assert!((diffs[[1, 1]] - 2.0).abs() < 1e-10);
573    }
574
575    #[test]
576    fn test_moments_computation() {
577        let extractor = FeatureExtractor::default_extractor();
578
579        let states = Array2::from_shape_fn((5, 10), |(i, _j)| i as f64);
580        let moments = extractor.compute_moments(&states).unwrap();
581
582        assert_eq!(moments.nrows(), 5);
583        assert_eq!(moments.ncols(), 4); // Default moment_order is 4
584    }
585
586    #[test]
587    fn test_quantum_tomography() {
588        let config = ReservoirConfig::new(4).with_seed(42);
589        let reservoir = QuantumReservoir::new(config).unwrap();
590
591        let tomography = QuantumTomography::new(4);
592        let measurements = tomography.measure(&reservoir).unwrap();
593
594        assert!(measurements.len() > 0);
595    }
596
597    #[test]
598    fn test_expectation_computer() {
599        let config = ReservoirConfig::new(4).with_seed(42);
600        let reservoir = QuantumReservoir::new(config).unwrap();
601
602        let computer = ExpectationComputer::new(4);
603        let expectations = computer.compute(&reservoir).unwrap();
604
605        // Should have 16 expectation values (2^4 basis states)
606        assert_eq!(expectations.len(), 16);
607
608        // Expectations should sum to 1 (probabilities)
609        let sum: f64 = expectations.sum();
610        assert!((sum - 1.0).abs() < 1e-6);
611    }
612
613    #[test]
614    fn test_single_state_extraction() {
615        let extractor = FeatureExtractor::default_extractor();
616        let state = Array1::from_vec(vec![0.1, 0.2, 0.3, 0.4]);
617
618        let features = extractor.extract_single(&state).unwrap();
619        assert!(features.len() > 0);
620    }
621
622    #[test]
623    fn test_pairwise_correlations() {
624        let extractor = FeatureExtractor::default_extractor();
625        let states = Array2::from_shape_fn((10, 8), |(i, j)| {
626            ((i + j) as f64 / 18.0).sin()
627        });
628
629        let correlations = extractor.compute_pairwise_correlations(&states).unwrap();
630        assert_eq!(correlations.nrows(), 10);
631    }
632}