quant_iron/components/
pauli_string.rs

1use crate::{
2    components::{operator::Operator, operator::Pauli, state::State},
3    errors::Error,
4};
5use num_complex::Complex;
6use rayon::prelude::*;
7use std::collections::HashMap;
8use std::ops::{Add, Mul};
9
10/// Represents a Pauli string, which is a product of Pauli operators (X, Y, Z) acting on qubits.
11/// Used to represent a term in a Hamiltonian or a quantum operator.
12#[derive(Debug, Clone, PartialEq)]
13pub struct PauliString {
14    /// A mapping from qubit indices to Pauli operators.
15    ops: HashMap<usize, Pauli>,
16    /// The coefficient of the Pauli string, which is a complex number.
17    coefficient: Complex<f64>,
18}
19
20impl PauliString {
21    /// Creates a new Pauli string with the given coefficient and an empty set of operators.
22    ///
23    /// # Arguments
24    /// * `coefficient` - The coefficient of the Pauli string, represented as a complex number.
25    ///
26    /// # Returns
27    /// A new `PauliString` instance with the specified coefficient and no operators.
28    pub fn new(coefficient: Complex<f64>) -> Self {
29        Self {
30            ops: HashMap::new(),
31            coefficient,
32        }
33    }
34
35    /// Creates a new Pauli string with the given coefficient and a set of operators.
36    ///
37    /// # Arguments
38    /// * `coefficient` - The coefficient of the Pauli string, represented as a complex number.
39    /// * `ops` - A mapping from qubit indices to Pauli operators.
40    ///
41    /// # Returns
42    /// A new `PauliString` instance with the specified coefficient and operators.
43    pub fn with_ops(coefficient: Complex<f64>, ops: HashMap<usize, Pauli>) -> Self {
44        Self { ops, coefficient }
45    }
46
47    /// Adds a Pauli operator to the Pauli string at the specified qubit index.
48    ///
49    /// # Arguments
50    /// * `qubit` - The index of the qubit to which the operator is applied.
51    /// * `op` - The Pauli operator to be added (X, Y, or Z).
52    pub fn add_op(&mut self, qubit: usize, op: Pauli) {
53        self.ops.insert(qubit, op);
54    }
55
56    /// Adds a Pauli operator to the Pauli string at the specified qubit index and returns the new `PauliString` instance.
57    ///
58    /// # Arguments
59    ///
60    /// * `qubit` - The index of the qubit to which the operator is applied.
61    /// * `op` - The Pauli operator to be added (X, Y, or Z).
62    ///
63    /// # Returns
64    ///
65    /// * `Self` - A new `PauliString` instance with the added operator.
66    pub fn with_op(mut self, qubit: usize, op: Pauli) -> Self {
67        self.add_op(qubit, op);
68        self
69    }
70
71    /// Returns the coefficient of the Pauli string.
72    ///
73    /// # Returns
74    ///
75    /// * `Complex<f64>` - The coefficient of the Pauli string, represented as a complex number.
76    pub fn coefficient(&self) -> Complex<f64> {
77        self.coefficient
78    }
79
80    /// Returns a reference to the operators in the Pauli string.
81    ///
82    /// # Returns
83    ///
84    /// * `&HashMap<usize, Pauli>` - A reference to the mapping of qubit indices to Pauli operators.
85    pub fn ops(&self) -> &HashMap<usize, Pauli> {
86        &self.ops
87    }
88
89    /// Applies the Pauli string to a given state.
90    ///
91    /// # Arguments
92    /// * `state` - The state to which the Pauli string is applied.
93    ///
94    /// # Returns
95    /// * `Result<State, Error>` - The resulting state after applying the Pauli string, or an error if the operation fails.
96    ///
97    /// # Errors
98    ///
99    /// * Returns an error if the operations in the Pauli string refer to qubits outside the range of the state.
100    pub fn apply(&self, state: &State) -> Result<State, Error> {
101        // If the Pauli string is empty, return the state multiplied by the coefficient
102        if self.ops.is_empty() {
103            return Ok(state.clone() * self.coefficient);
104        }
105
106        // Apply the Pauli string to the state
107        let mut new_state: State = state.clone();
108        for (qubit, op) in &self.ops {
109            new_state = op.apply(&mut new_state, &[*qubit], &[])?; // Assumes op.apply can take &mut State and modify it or returns a new one
110        }
111        Ok(new_state * self.coefficient)
112    }
113
114    /// Helper function to apply only the operator part of the Pauli string (P_ops) to a state.
115    /// This does not include the PauliString's own coefficient.
116    fn apply_operators(&self, state: &State) -> Result<State, Error> {
117        if self.ops.is_empty() {
118            // If there are no operators, P_ops is effectively Identity.
119            // P_ops |state> = |state>.
120            return Ok(state.clone());
121        }
122
123        let mut current_state = state.clone();
124        for (qubit_idx, pauli_op) in &self.ops {
125            current_state = pauli_op.apply(&mut current_state, &[*qubit_idx], &[])?;
126        }
127        Ok(current_state)
128    }
129
130    /// Applies the exponential of the Pauli string to a given state.
131    ///
132    /// # Arguments
133    ///
134    /// * `state` - The state to which the exponential of the Pauli string is applied.
135    ///
136    /// # Returns
137    ///
138    /// * `Result<State, Error>` - The resulting state after applying the exponential of the Pauli string, or an error if the operation fails.
139    ///
140    /// # Errors
141    ///
142    /// * Returns an error if the operations in the Pauli string refer to qubits outside the range of the state.
143    pub fn apply_exp(&self, state: &State) -> Result<State, Error> {
144        // If the Pauli string is empty, return the state multiplied by the exponential of the coefficient
145        let alpha: Complex<f64> = self.coefficient;
146
147        if self.ops.is_empty() {
148            // P_ops is Identity. exp(alpha * I) |state> = exp(alpha) * |state>
149            return Ok(state.clone() * alpha.exp());
150        }
151
152        // 1. Calculate P_ops |state> using the helper
153        let p_ops_psi_state = self.apply_operators(state)?;
154
155        // 2. Calculate scalar coefficients for exp(alpha * P_ops) = cosh(alpha)*I + sinh(alpha)*P_ops
156        let cosh_alpha: Complex<f64> = alpha.cosh();
157        let sinh_alpha: Complex<f64> = alpha.sinh();
158
159        // 3. Calculate cosh(alpha) * |state>
160        let term_identity_part: State = state.clone() * cosh_alpha;
161
162        // 4. Calculate sinh(alpha) * (P_ops |state>)
163        let term_operator_part: State = p_ops_psi_state * sinh_alpha;
164
165        // 5. Add the two terms: cosh(alpha) * |state> + sinh(alpha) * (P_ops |state>)
166        Ok(term_identity_part + term_operator_part)
167    }
168
169    /// Applies the exponential of the Pauli string to a given state with a specified factor.
170    ///
171    /// # Arguments
172    /// * `state` - The state to which the exponential of the Pauli string is applied.
173    /// * `factor` - A complex factor to be multiplied with the coefficient of the Pauli string.
174    ///
175    /// # Returns
176    /// * `Result<State, Error>` - The resulting state after applying the exponential of the Pauli string with the factor, or an error if the operation fails.
177    ///
178    /// # Errors
179    ///
180    /// * Returns an error if the operations in the Pauli string refer to qubits outside the range of the state.
181    pub fn apply_exp_factor(&self, state: &State, factor: Complex<f64>) -> Result<State, Error> {
182        // Calculate the effective coefficient for the exponentiation
183        let alpha: Complex<f64> = self.coefficient * factor;
184
185        if self.ops.is_empty() {
186            // P_ops is Identity. exp(alpha * I) |state> = exp(alpha) * |state>
187            return Ok(state.clone() * alpha.exp());
188        }
189
190        // 1. Calculate P_ops |state> using the helper
191        let p_ops_psi_state = self.apply_operators(state)?;
192        // p_ops_psi_state now holds P_ops |state> (where P_ops is the product of Pauli operators without self.coefficient)
193
194        // 2. Calculate scalar coefficients for exp(alpha * P_ops) = cosh(alpha)*I + sinh(alpha)*P_ops
195        let cosh_alpha: Complex<f64> = alpha.cosh();
196        let sinh_alpha: Complex<f64> = alpha.sinh();
197
198        // 3. Calculate cosh(alpha) * |state>
199        let term_identity_part: State = state.clone() * cosh_alpha;
200
201        // 4. Calculate sinh(alpha) * (P_ops |state>)
202        let term_operator_part: State = p_ops_psi_state * sinh_alpha;
203
204        // 5. Add the two terms: cosh(alpha) * |state> + sinh(alpha) * (P_ops |state>)
205        Ok(term_identity_part + term_operator_part)
206    }
207
208    /// Returns the Hermitian conjugate of the Pauli string.
209    ///
210    /// # Returns
211    ///
212    /// * `Self` - A new `PauliString` instance representing the Hermitian conjugate of the original Pauli string.
213    pub fn hermitian_conjugate(&self) -> Self {
214        PauliString {
215            ops: self.ops.clone(),
216            coefficient: self.coefficient.conj(),
217        }
218    }
219}
220
221impl Add for PauliString {
222    type Output = SumOp;
223
224    fn add(self, other: Self) -> Self::Output {
225        // Create a new SumOp with the two Pauli strings
226        let terms: Vec<PauliString> = vec![self, other];
227        // Return a new SumOp instance with the terms
228        SumOp::new(terms)
229    }
230}
231
232impl Mul<Complex<f64>> for PauliString {
233    type Output = Self;
234
235    fn mul(self, rhs: Complex<f64>) -> Self::Output {
236        // Create a new Pauli string with the product of the coefficient and the given complex number
237        PauliString {
238            ops: self.ops.clone(),
239            coefficient: self.coefficient * rhs,
240        }
241    }
242}
243
244impl Mul<f64> for PauliString {
245    type Output = Self;
246
247    fn mul(self, rhs: f64) -> Self::Output {
248        // Create a new Pauli string with the product of the coefficient and the given real number
249        PauliString {
250            ops: self.ops.clone(),
251            coefficient: self.coefficient * Complex::new(rhs, 0.0),
252        }
253    }
254}
255
256impl Mul<PauliString> for f64 {
257    type Output = PauliString;
258
259    fn mul(self, rhs: PauliString) -> Self::Output {
260        rhs * self
261    }
262}
263
264impl std::fmt::Display for PauliString {
265    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
266        let coeff_str: String = if self.coefficient.re == 0.0 && self.coefficient.im == 0.0 {
267            "0".to_string()
268        } else if self.coefficient.im == 0.0 {
269            format!("{}", self.coefficient.re)
270        } else if self.coefficient.re == 0.0 {
271            format!("{}i", self.coefficient.im)
272        } else {
273            format!("({} + {}i)", self.coefficient.re, self.coefficient.im)
274        };
275
276        let mut result: String = coeff_str + " * ";
277
278            let mut sorted_ops: Vec<(&usize, &Pauli)> = self.ops.iter().collect();
279            sorted_ops.sort_by(|&(qubit_a, op_a), &(qubit_b, op_b)| {
280            if qubit_a == qubit_b {
281                // If qubits are the same, sort by operator type (X -> Y -> Z)
282                let op_a_str: String = format!("{}", op_a);
283                let op_b_str: String = format!("{}", op_b);
284                match (op_a_str.as_str(), op_b_str.as_str()) {
285                    ("X", "Y") | ("X", "Z") | ("Y", "Z") => std::cmp::Ordering::Less,
286                    ("Y", "X") | ("Z", "X") | ("Z", "Y") => std::cmp::Ordering::Greater,
287                    _ => std::cmp::Ordering::Equal,
288                }
289            } else {
290                // Otherwise, sort by qubit index
291                qubit_a.cmp(qubit_b)
292            }
293        });
294        for (qubit, op) in sorted_ops {
295            result.push_str(&format!("{}[{}] ", op, qubit));
296            }
297
298        write!(f, "{}", result.trim())
299    }
300}
301
302/// A vector of Pauli strings that are summed together.
303/// Useful for representing Hamiltonians or observables.
304///
305/// # Fields
306///
307/// * `terms` - A vector of `PauliString` instances that are summed together.
308#[derive(Debug, Clone, PartialEq)]
309pub struct SumOp {
310    /// A vector of Pauli strings that are summed together.
311    pub terms: Vec<PauliString>,
312}
313
314impl SumOp {
315    /// Creates a new `SumPauliString` instance with the given terms and number of qubits.
316    ///
317    /// # Arguments
318    ///
319    /// * `terms` - A vector of `PauliString` instances that are summed together.
320    ///
321    /// # Returns
322    ///
323    /// * A new `SumPauliString` instance with the specified terms and number of qubits.
324    pub fn new(terms: Vec<PauliString>) -> Self {
325        Self { terms }
326    }
327
328    /// Returns the number of terms in the sum.
329    ///
330    /// # Returns
331    ///
332    /// * `usize` - The number of terms in the sum.
333    pub fn num_terms(&self) -> usize {
334        self.terms.len()
335    }
336
337    /// Adds a new term to the sum.
338    /// 
339    /// # Arguments
340    /// 
341    /// * `term` - The `PauliString` term to be added to the sum.
342    pub fn add_term(&mut self, term: PauliString) {
343        self.terms.push(term);
344    }
345
346    /// Adds a new term to the sum and returns a new `SumOp` instance.
347    /// 
348    /// # Arguments
349    /// 
350    /// * `term` - The `PauliString` term to be added to the sum.
351    /// 
352    /// # Returns
353    /// * `Self` - A new `SumOp` instance with the added term.
354    pub fn with_term(mut self, term: PauliString) -> Self {
355        self.add_term(term);
356        self
357    }
358
359    /// Applies the sum of Pauli strings to a given state.
360    ///
361    /// # Arguments
362    ///
363    /// * `state` - The state to which the sum of Pauli strings is applied.
364    ///
365    /// # Returns
366    ///
367    /// * `Result<State, Error>` - The resulting state after applying the sum of Pauli strings, or an error if the operation fails.
368    ///
369    /// # Errors
370    ///
371    /// * Returns an error if the operations in the Pauli strings refer to qubits outside the range of the state.
372    pub fn apply(&self, state: &State) -> Result<State, Error> {
373        if self.terms.is_empty() {
374            // An empty sumop is equivalent to the zero operator
375            return Ok(state.clone() * 0.0);
376        }
377
378        Ok(self
379            .terms
380            .par_iter()
381            .map(|term| term.apply(state))
382            .collect::<Result<Vec<State>, Error>>()?
383            .into_iter()
384            .sum())
385    }
386
387    /// Calculates the expectation value <psi|H|psi> = Sum_i <psi|P_i|psi>.
388    ///
389    /// The expectation value is generally real for Hermitian operators and normalized states.
390    /// However, this function returns a Complex<f64> as intermediate PauliStrings
391    /// might have complex coefficients or the operator/state might not be strictly physical.
392    ///
393    /// # Arguments
394    /// * `state` - The state |psi> for which to calculate the expectation value.
395    ///             For a physically meaningful expectation value, this state should be normalized.
396    ///
397    /// # Returns
398    /// * `Result<Complex<f64>, Error>` - The expectation value, or an error if the operation fails.
399    ///
400    /// # Errors
401    /// * Returns an error if any underlying `PauliString::apply` fails (e.g., invalid qubit index).
402    /// * Returns an error if `state.inner_product` fails (e.g., mismatched number of qubits,
403    ///   though `PauliString::apply` should also catch qubit count issues).
404    pub fn expectation_value(&self, state: &State) -> Result<Complex<f64>, Error> {
405        if self.terms.is_empty() {
406            // The expectation value of a zero operator is zero.
407            return Ok(Complex::new(0.0, 0.0));
408        }
409
410        let expectation_values_per_term: Vec<Complex<f64>> = self
411            .terms
412            .par_iter()
413            .map(|term| {
414                // For each term P_i in the sum H = Sum_i P_i:
415                // 1. Calculate |phi_i> = P_i |psi>
416                let phi_i_state = term.apply(state)?;
417
418                // 2. Calculate <psi | phi_i> = <psi | P_i | psi>
419                state.inner_product(&phi_i_state)
420            })
421            .collect::<Result<Vec<Complex<f64>>, Error>>()?; // Collect into Result<Vec<_>, E>, propagating errors
422
423        // Sum the individual expectation values <psi|P_i|psi>
424        // Complex<f64> from num_complex implements std::iter::Sum.
425        Ok(expectation_values_per_term.into_iter().sum())
426    }
427}
428
429impl std::fmt::Display for SumOp {
430    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
431        let mut result: String = String::new();
432        for (i, term) in self.terms.iter().enumerate() {
433            if i > 0 {
434                result.push_str(" + ");
435            }
436            result.push_str(&format!("{}", term));
437        }
438        write!(f, "{}", result)
439    }
440}
441
442impl Mul<Complex<f64>> for SumOp {
443    type Output = Self;
444
445    fn mul(self, rhs: Complex<f64>) -> Self::Output {
446        // Create a new SumOp with the coefficient multiplied by the given complex number
447        let terms: Vec<PauliString> = self
448            .terms
449            .into_iter()
450            .map(|term| term * rhs)
451            .collect();
452        // Return a new SumOp instance with the modified terms
453        SumOp::new(terms)
454    }
455}
456
457impl Mul<f64> for SumOp {
458    type Output = Self;
459
460    fn mul(self, rhs: f64) -> Self::Output {
461        // Create a new SumOp with the coefficient multiplied by the given real number
462        let terms: Vec<PauliString> = self
463            .terms
464            .into_iter()
465            .map(|term| term * rhs)
466            .collect();
467        // Return a new SumOp instance with the modified terms
468        SumOp::new(terms)
469    }
470}
471
472impl Add for SumOp {
473    type Output = Self;
474
475    fn add(self, other: Self) -> Self::Output {
476        // Create a new SumOp with the two sets of terms
477        let mut terms: Vec<PauliString> = self.terms;
478        terms.extend(other.terms);
479        // Return a new SumOp instance with the combined terms
480        SumOp::new(terms)
481    }
482}
483
484impl Add<PauliString> for SumOp {
485    type Output = Self;
486
487    fn add(self, other: PauliString) -> Self::Output {
488        // Create a new SumOp with the existing terms and the new Pauli string
489        let mut terms: Vec<PauliString> = self.terms;
490        terms.push(other);
491        // Return a new SumOp instance with the combined terms
492        SumOp::new(terms)
493    }
494}