quantrs2_core/
operations.rs

1//! Non-unitary quantum operations and measurements
2//!
3//! This module provides support for non-unitary quantum operations including:
4//! - Projective measurements
5//! - POVM measurements
6//! - Quantum channels
7//! - Reset operations
8
9use crate::error::{QuantRS2Error, QuantRS2Result};
10use crate::qubit::QubitId;
11use ndarray::{Array1, Array2, ArrayView1, ArrayView2};
12use num_complex::Complex64;
13use rand::prelude::*;
14use rand::rng;
15use std::fmt::Debug;
16
17/// Trait for quantum operations (both unitary and non-unitary)
18pub trait QuantumOperation: Debug + Send + Sync {
19    /// Apply the operation to a state vector
20    fn apply_to_state(&self, state: &ArrayView1<Complex64>) -> QuantRS2Result<OperationResult>;
21
22    /// Apply the operation to a density matrix
23    fn apply_to_density_matrix(
24        &self,
25        rho: &ArrayView2<Complex64>,
26    ) -> QuantRS2Result<Array2<Complex64>>;
27
28    /// Get the qubits this operation acts on
29    fn qubits(&self) -> Vec<QubitId>;
30
31    /// Check if the operation is deterministic
32    fn is_deterministic(&self) -> bool;
33}
34
35/// Result of a quantum operation
36#[derive(Debug, Clone)]
37pub enum OperationResult {
38    /// Deterministic result (new state)
39    Deterministic(Array1<Complex64>),
40    /// Probabilistic result (outcome, probability, new state)
41    Probabilistic {
42        outcome: usize,
43        probability: f64,
44        state: Array1<Complex64>,
45    },
46    /// Multiple possible outcomes
47    MultiOutcome(Vec<MeasurementOutcome>),
48}
49
50/// A single measurement outcome
51#[derive(Debug, Clone)]
52pub struct MeasurementOutcome {
53    /// The measurement result
54    pub outcome: usize,
55    /// The probability of this outcome
56    pub probability: f64,
57    /// The post-measurement state
58    pub state: Array1<Complex64>,
59}
60
61/// Projective measurement in the computational basis
62#[derive(Debug, Clone)]
63pub struct ProjectiveMeasurement {
64    /// Qubits to measure
65    pub qubits: Vec<QubitId>,
66    /// Optional specific outcome to project onto
67    pub outcome: Option<usize>,
68}
69
70impl ProjectiveMeasurement {
71    /// Create a new projective measurement
72    pub fn new(qubits: Vec<QubitId>) -> Self {
73        Self {
74            qubits,
75            outcome: None,
76        }
77    }
78
79    /// Create a measurement that projects onto a specific outcome
80    pub fn with_outcome(qubits: Vec<QubitId>, outcome: usize) -> Self {
81        Self {
82            qubits,
83            outcome: Some(outcome),
84        }
85    }
86
87    /// Calculate measurement probabilities
88    pub fn get_probabilities(&self, state: &ArrayView1<Complex64>) -> QuantRS2Result<Vec<f64>> {
89        let n_qubits = (state.len() as f64).log2() as usize;
90        let n_outcomes = 1 << self.qubits.len();
91        let mut probabilities = vec![0.0; n_outcomes];
92
93        // Calculate probability for each outcome
94        for (idx, amp) in state.iter().enumerate() {
95            let outcome = self.extract_outcome_from_index(idx, n_qubits);
96            probabilities[outcome] += amp.norm_sqr();
97        }
98
99        Ok(probabilities)
100    }
101
102    /// Extract the measurement outcome from a basis state index
103    fn extract_outcome_from_index(&self, index: usize, total_qubits: usize) -> usize {
104        let mut outcome = 0;
105        for (i, &qubit) in self.qubits.iter().enumerate() {
106            let bit = (index >> (total_qubits - 1 - qubit.0 as usize)) & 1;
107            outcome |= bit << (self.qubits.len() - 1 - i);
108        }
109        outcome
110    }
111
112    /// Project state onto a specific outcome
113    fn project_onto_outcome(
114        &self,
115        state: &ArrayView1<Complex64>,
116        outcome: usize,
117    ) -> QuantRS2Result<(f64, Array1<Complex64>)> {
118        let n_qubits = (state.len() as f64).log2() as usize;
119        let mut projected = Array1::zeros(state.len());
120        let mut norm_squared = 0.0;
121
122        for (idx, &amp) in state.iter().enumerate() {
123            if self.extract_outcome_from_index(idx, n_qubits) == outcome {
124                projected[idx] = amp;
125                norm_squared += amp.norm_sqr();
126            }
127        }
128
129        if norm_squared < 1e-10 {
130            return Err(QuantRS2Error::InvalidInput(
131                "Measurement outcome has zero probability".to_string(),
132            ));
133        }
134
135        // Normalize the projected state
136        let norm = norm_squared.sqrt();
137        projected = projected / norm;
138
139        Ok((norm_squared, projected))
140    }
141}
142
143impl QuantumOperation for ProjectiveMeasurement {
144    fn apply_to_state(&self, state: &ArrayView1<Complex64>) -> QuantRS2Result<OperationResult> {
145        match self.outcome {
146            Some(outcome) => {
147                // Project onto specific outcome
148                let (prob, new_state) = self.project_onto_outcome(state, outcome)?;
149                Ok(OperationResult::Probabilistic {
150                    outcome,
151                    probability: prob,
152                    state: new_state,
153                })
154            }
155            None => {
156                // Calculate all outcomes
157                let probabilities = self.get_probabilities(state)?;
158                let mut outcomes = Vec::new();
159
160                for (outcome, &prob) in probabilities.iter().enumerate() {
161                    if prob > 1e-10 {
162                        let (_, new_state) = self.project_onto_outcome(state, outcome)?;
163                        outcomes.push(MeasurementOutcome {
164                            outcome,
165                            probability: prob,
166                            state: new_state,
167                        });
168                    }
169                }
170
171                Ok(OperationResult::MultiOutcome(outcomes))
172            }
173        }
174    }
175
176    fn apply_to_density_matrix(
177        &self,
178        rho: &ArrayView2<Complex64>,
179    ) -> QuantRS2Result<Array2<Complex64>> {
180        let n_qubits = (rho.nrows() as f64).log2() as usize;
181        let mut result = Array2::zeros(rho.raw_dim());
182
183        // Apply measurement operators: ρ' = Σ_i P_i ρ P_i
184        for outcome in 0..(1 << self.qubits.len()) {
185            let projector = self.create_projector(outcome, n_qubits)?;
186            let proj_rho = projector.dot(rho).dot(&projector);
187            result = result + proj_rho;
188        }
189
190        Ok(result)
191    }
192
193    fn qubits(&self) -> Vec<QubitId> {
194        self.qubits.clone()
195    }
196
197    fn is_deterministic(&self) -> bool {
198        false
199    }
200}
201
202impl ProjectiveMeasurement {
203    /// Create projector matrix for a specific outcome
204    fn create_projector(
205        &self,
206        outcome: usize,
207        total_qubits: usize,
208    ) -> QuantRS2Result<Array2<Complex64>> {
209        let dim = 1 << total_qubits;
210        let mut projector = Array2::zeros((dim, dim));
211
212        for idx in 0..dim {
213            if self.extract_outcome_from_index(idx, total_qubits) == outcome {
214                projector[[idx, idx]] = Complex64::new(1.0, 0.0);
215            }
216        }
217
218        Ok(projector)
219    }
220}
221
222/// POVM (Positive Operator-Valued Measure) measurement
223#[derive(Debug, Clone)]
224pub struct POVMMeasurement {
225    /// The POVM elements (must sum to identity)
226    pub elements: Vec<Array2<Complex64>>,
227    /// Qubits this POVM acts on
228    pub qubits: Vec<QubitId>,
229}
230
231impl POVMMeasurement {
232    /// Create a new POVM measurement
233    pub fn new(elements: Vec<Array2<Complex64>>, qubits: Vec<QubitId>) -> QuantRS2Result<Self> {
234        // Verify that elements sum to identity
235        let dim = elements[0].nrows();
236        let mut sum = Array2::<Complex64>::zeros((dim, dim));
237
238        for element in &elements {
239            if element.nrows() != dim || element.ncols() != dim {
240                return Err(QuantRS2Error::InvalidInput(
241                    "All POVM elements must have the same dimension".to_string(),
242                ));
243            }
244            sum = sum + element;
245        }
246
247        // Check if sum is identity (within tolerance)
248        for i in 0..dim {
249            for j in 0..dim {
250                let expected = if i == j {
251                    Complex64::new(1.0, 0.0)
252                } else {
253                    Complex64::new(0.0, 0.0)
254                };
255                let diff: Complex64 = sum[[i, j]] - expected;
256                if diff.norm_sqr().sqrt() > 1e-10 {
257                    return Err(QuantRS2Error::InvalidInput(
258                        "POVM elements do not sum to identity".to_string(),
259                    ));
260                }
261            }
262        }
263
264        Ok(Self { elements, qubits })
265    }
266
267    /// Get measurement probabilities
268    pub fn get_probabilities(&self, state: &ArrayView1<Complex64>) -> QuantRS2Result<Vec<f64>> {
269        let mut probabilities = Vec::new();
270
271        for element in &self.elements {
272            // Probability = <ψ|M_i|ψ>
273            let temp = element.dot(state);
274            let prob = state
275                .iter()
276                .zip(temp.iter())
277                .map(|(&psi, &m_psi)| psi.conj() * m_psi)
278                .sum::<Complex64>()
279                .re;
280            probabilities.push(prob);
281        }
282
283        Ok(probabilities)
284    }
285}
286
287impl QuantumOperation for POVMMeasurement {
288    fn apply_to_state(&self, state: &ArrayView1<Complex64>) -> QuantRS2Result<OperationResult> {
289        let probabilities = self.get_probabilities(state)?;
290        let mut outcomes = Vec::new();
291
292        for (i, (&prob, element)) in probabilities.iter().zip(&self.elements).enumerate() {
293            if prob > 1e-10 {
294                // Apply measurement operator: |ψ'> = M_i|ψ>/√p_i
295                let new_state = element.dot(state) / prob.sqrt();
296                outcomes.push(MeasurementOutcome {
297                    outcome: i,
298                    probability: prob,
299                    state: new_state,
300                });
301            }
302        }
303
304        Ok(OperationResult::MultiOutcome(outcomes))
305    }
306
307    fn apply_to_density_matrix(
308        &self,
309        rho: &ArrayView2<Complex64>,
310    ) -> QuantRS2Result<Array2<Complex64>> {
311        let mut result = Array2::zeros(rho.raw_dim());
312
313        // Apply POVM: ρ' = Σ_i M_i ρ M_i†
314        for element in &self.elements {
315            let m_dag = element.t().mapv(|x| x.conj());
316            let term = element.dot(rho).dot(&m_dag);
317            result = result + term;
318        }
319
320        Ok(result)
321    }
322
323    fn qubits(&self) -> Vec<QubitId> {
324        self.qubits.clone()
325    }
326
327    fn is_deterministic(&self) -> bool {
328        false
329    }
330}
331
332/// Reset operation that sets qubits to |0⟩
333#[derive(Debug, Clone)]
334pub struct Reset {
335    /// Qubits to reset
336    pub qubits: Vec<QubitId>,
337}
338
339impl Reset {
340    /// Create a new reset operation
341    pub fn new(qubits: Vec<QubitId>) -> Self {
342        Self { qubits }
343    }
344}
345
346impl QuantumOperation for Reset {
347    fn apply_to_state(&self, state: &ArrayView1<Complex64>) -> QuantRS2Result<OperationResult> {
348        let n_qubits = (state.len() as f64).log2() as usize;
349        let mut new_state = Array1::zeros(state.len());
350
351        // Project onto |0⟩ for specified qubits and normalize
352        let mut norm_squared = 0.0;
353        for (idx, &amp) in state.iter().enumerate() {
354            let mut should_keep = true;
355            for &qubit in &self.qubits {
356                let bit = (idx >> (n_qubits - 1 - qubit.0 as usize)) & 1;
357                if bit != 0 {
358                    should_keep = false;
359                    break;
360                }
361            }
362            if should_keep {
363                new_state[idx] = amp;
364                norm_squared += amp.norm_sqr();
365            }
366        }
367
368        if norm_squared < 1e-10 {
369            // If projection gives zero, just set to |0...0⟩
370            new_state[0] = Complex64::new(1.0, 0.0);
371        } else {
372            new_state = new_state / norm_squared.sqrt();
373        }
374
375        Ok(OperationResult::Deterministic(new_state))
376    }
377
378    fn apply_to_density_matrix(
379        &self,
380        rho: &ArrayView2<Complex64>,
381    ) -> QuantRS2Result<Array2<Complex64>> {
382        // For reset, we trace out the qubits and replace with |0⟩⟨0|
383        // This is a simplified implementation
384        let n_qubits = (rho.nrows() as f64).log2() as usize;
385        let mut result = Array2::zeros(rho.raw_dim());
386
387        // Project onto |0⟩ for reset qubits
388        for i in 0..rho.nrows() {
389            for j in 0..rho.ncols() {
390                let mut should_keep = true;
391                for &qubit in &self.qubits {
392                    let bit_i = (i >> (n_qubits - 1 - qubit.0 as usize)) & 1;
393                    let bit_j = (j >> (n_qubits - 1 - qubit.0 as usize)) & 1;
394                    if bit_i != 0 || bit_j != 0 {
395                        should_keep = false;
396                        break;
397                    }
398                }
399                if should_keep {
400                    result[[i, j]] = rho[[i, j]];
401                }
402            }
403        }
404
405        // Renormalize
406        let trace = (0..result.nrows()).map(|i| result[[i, i]].re).sum::<f64>();
407        if trace > 1e-10 {
408            result = result / trace;
409        }
410
411        Ok(result)
412    }
413
414    fn qubits(&self) -> Vec<QubitId> {
415        self.qubits.clone()
416    }
417
418    fn is_deterministic(&self) -> bool {
419        true
420    }
421}
422
423/// Sample from measurement outcomes according to their probabilities
424pub fn sample_outcome(probabilities: &[f64]) -> QuantRS2Result<usize> {
425    let mut rng = rng();
426    let r: f64 = rng.random();
427
428    let mut cumsum = 0.0;
429    for (i, &prob) in probabilities.iter().enumerate() {
430        cumsum += prob;
431        if r < cumsum {
432            return Ok(i);
433        }
434    }
435
436    // Should not reach here if probabilities sum to 1
437    Err(QuantRS2Error::ComputationError(
438        "Probabilities do not sum to 1".to_string(),
439    ))
440}
441
442/// Apply a quantum operation and sample an outcome
443pub fn apply_and_sample<O: QuantumOperation>(
444    operation: &O,
445    state: &ArrayView1<Complex64>,
446) -> QuantRS2Result<(usize, Array1<Complex64>)> {
447    match operation.apply_to_state(state)? {
448        OperationResult::Deterministic(new_state) => Ok((0, new_state)),
449        OperationResult::Probabilistic { outcome, state, .. } => Ok((outcome, state)),
450        OperationResult::MultiOutcome(outcomes) => {
451            let probabilities: Vec<f64> = outcomes.iter().map(|o| o.probability).collect();
452            let sampled_idx = sample_outcome(&probabilities)?;
453            let outcome = &outcomes[sampled_idx];
454            Ok((outcome.outcome, outcome.state.clone()))
455        }
456    }
457}
458
459#[cfg(test)]
460mod tests {
461    use super::*;
462
463    #[test]
464    fn test_projective_measurement() {
465        // |+⟩ state
466        let state = Array1::from_vec(vec![
467            Complex64::new(1.0 / 2.0_f64.sqrt(), 0.0),
468            Complex64::new(1.0 / 2.0_f64.sqrt(), 0.0),
469        ]);
470
471        let measurement = ProjectiveMeasurement::new(vec![QubitId(0)]);
472        let probs = measurement.get_probabilities(&state.view()).unwrap();
473
474        assert!((probs[0] - 0.5).abs() < 1e-10);
475        assert!((probs[1] - 0.5).abs() < 1e-10);
476    }
477
478    #[test]
479    fn test_reset_operation() {
480        // |1⟩ state
481        let state = Array1::from_vec(vec![Complex64::new(0.0, 0.0), Complex64::new(1.0, 0.0)]);
482
483        let reset = Reset::new(vec![QubitId(0)]);
484        let result = reset.apply_to_state(&state.view()).unwrap();
485
486        match result {
487            OperationResult::Deterministic(new_state) => {
488                assert!((new_state[0].norm() - 1.0).abs() < 1e-10);
489                assert!(new_state[1].norm() < 1e-10);
490            }
491            _ => panic!("Reset should be deterministic"),
492        }
493    }
494}