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}