Skip to main content

cqlib_core/qis/
evolution.rs

1// This code is part of Cqlib.
2//
3// (C) Copyright China Telecom Quantum Group 2026
4//
5// This code is licensed under the Apache License, Version 2.0. You may
6// obtain a copy of this license in the LICENSE.txt file in the root directory
7// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
8//
9// Any modifications or derivative works of this code must retain this
10// copyright notice, and modified files need to carry a notice indicating
11// that they have been altered from the originals.
12
13//! Pauli evolution gates and operations.
14//!
15//! This module provides functionality to implement Pauli rotation gates of the form
16//! $e^{-i\theta/2 \cdot P}$ where $P$ is a multi-qubit Pauli string operator.
17//!
18//! # Algorithm
19//!
20//! The Pauli rotation is implemented using basis transformation and CNOT ladder:
21//!
22//! 1. **Basis Transformation**: For each non-identity Pauli operator $P_i \in \{X, Y, Z\}$,
23//!    apply a basis transformation to convert it to the $Z$ basis:
24//!    - $X \to Z$: Apply $H$ gate
25//!    - $Y \to Z$: Apply $H \cdot S^\dagger$ (or equivalently $R_X(\pi/2)$)
26//!    - $Z \to Z$: No transformation needed
27//!
28//! 2. **CNOT Chain (Cascade)**: Use CNOT gates to accumulate parity along a chain
29//!    to the last non-identity qubit:
30//!    - For non-identity qubits at indices $[i_0, i_1, i_2, \ldots, i_{n-1}]$,
31//!      apply $CNOT(i_0 \to i_1), CNOT(i_1 \to i_2), \ldots, CNOT(i_{n-2} \to i_{n-1})$
32//!    - This computes the product $P_{i_0} \cdot P_{i_1} \cdot \ldots \cdot P_{i_{n-1}}$
33//!      in the computational basis
34//!
35//! 3. **Core Rotation**: Apply $RZ(\theta)$ on the last non-identity qubit.
36//!    The multi-qubit Pauli rotation reduces to a single-qubit RZ after the
37//!    parity accumulation.
38//!
39//! 4. **Reverse CNOT Ladder**: Apply the CNOT gates in reverse order to uncompute
40//!    the parity.
41//!
42//! 5. **Inverse Basis Transformation**: Apply the inverse basis transformations in
43//!    reverse order to restore the original basis.
44//!
45//! # Mathematical Background
46//!
47//! For a Pauli string $P = \bigotimes_{i} P_i$, the rotation is:
48//! $$e^{-i\theta/2 \cdot P} = \cos(\theta/2) I - i\sin(\theta/2) P$$
49//!
50//! Since all Pauli operators can be simultaneously diagonalized in the $Z$ basis,
51//! they share eigenstates. The CNOT ladder effectively performs a basis change
52//! from the individual qubit basis to the joint parity basis, allowing the
53//! multi-qubit rotation to be performed as a single-qubit RZ gate.
54
55use crate::circuit::bit::Qubit;
56use crate::circuit::circuit_impl::Circuit;
57use crate::circuit::circuit_param::ParameterValue;
58use crate::circuit::error::CircuitError;
59use crate::circuit::parameter::Parameter;
60use crate::qis::error::QisError;
61use crate::qis::hamiltonian::Hamiltonian;
62use crate::qis::pauli::{Pauli, PauliString};
63use num_complex::Complex64;
64use rand::SeedableRng;
65use rand::rngs::StdRng;
66use rand::seq::SliceRandom;
67
68/// Trotter-Suzuki decomposition modes for Hamiltonian time evolution.
69///
70/// These modes determine how the time evolution operator $U(t) = e^{-iHt}$ is
71/// approximated as a product of Pauli rotations.
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
73pub enum TrotterMode {
74    /// First-order Lie-Trotter decomposition.
75    ///
76    /// $$U(t) \approx \left[ \prod_k e^{-i c_k t/n \cdot P_k} \right]^n$$
77    ///
78    /// Error scales as $O(t^2/n)$.
79    FirstOrder,
80
81    /// Second-order Strang splitting (symmetric decomposition).
82    ///
83    /// $$U(t) \approx \left[ e^{-i c_1 t/2n \cdot P_1} \cdots e^{-i c_m t/2n \cdot P_m}
84    /// \cdot e^{-i c_m t/2n \cdot P_m} \cdots e^{-i c_1 t/2n \cdot P_1} \right]^n$$
85    ///
86    /// Error scales as $O(t^3/n^2)$.
87    SecondOrder,
88
89    /// Randomized first-order Trotter with specified random seed.
90    ///
91    /// In each Trotter step, the order of Pauli terms is randomly shuffled.
92    /// This can help reduce systematic errors and improve convergence in some cases.
93    ///
94    /// # Arguments
95    /// * `seed` - The random seed for reproducibility
96    Randomized(u64),
97}
98
99/// Extension trait for Circuit to add Pauli evolution functionality.
100///
101/// This trait provides methods to append Pauli rotation gates to a circuit.
102pub trait PauliEvolution {
103    /// Appends a Pauli evolution gate $e^{-i\theta/2 \cdot P}$ to the circuit.
104    ///
105    /// This method implements the Pauli rotation using the basis transformation
106    /// and CNOT ladder algorithm.
107    ///
108    /// # Arguments
109    ///
110    /// * `pauli` - The Pauli string operator $P$ to exponentiate. Must be Hermitian
111    ///   (i.e., its phase must be $\pm 1$) for the evolution to be unitary.
112    /// * `angle` - The rotation angle $\theta$ (can be symbolic or fixed)
113    /// * `qubits` - The qubits to apply the operation on (must match `pauli.num_qubits`)
114    ///
115    /// # Returns
116    ///
117    /// * `Ok(())` - If the operation was successfully appended
118    /// * `Err(CircuitError)` - If qubit count mismatch, PauliString has non-Hermitian
119    ///   phase ($\pm i$), or other error occurs
120    ///
121    /// # Algorithm Details
122    ///
123    /// The rotation $e^{-i\theta/2 \cdot P}$ is implemented as follows:
124    ///
125    /// 1. **Phase Validation**: The PauliString's internal phase must be $\pm 1$
126    ///    (Hermitian). If the phase is $\pm i$, an error is returned because
127    ///    such operators cannot generate unitary evolution.
128    ///
129    /// 2. **Phase Absorption**: The PauliString's phase ($\pm 1$) is absorbed into
130    ///    the rotation angle: $\theta_{eff} = \theta \cdot \text{phase}$
131    ///
132    /// 3. **Basis Transformation**: For each qubit $i$ with non-identity Pauli $P_i$:
133    ///    - $P_i = X$: Apply $H$ gate (transforms $X$ to $Z$ basis)
134    ///    - $P_i = Y$: Apply $S^\dagger$ then $H$ (circuit order: $S^\dagger \to H$,
135    ///      i.e. $S^\dagger$ is applied first, then $H$). This transforms $Y \to Z$:
136    ///      $S^\dagger |y+\rangle = |x+\rangle$, $H |x+\rangle = |z+\rangle$.
137    ///    - $P_i = Z$: No transformation needed
138    ///
139    /// 4. **CNOT Chain**: For non-identity qubits $[i_0, i_1, \ldots, i_{n-1}]$,
140    ///    apply $CNOT(i_0 \to i_1), CNOT(i_1 \to i_2), \ldots$ to accumulate parity.
141    ///
142    /// 5. **Core Rotation**: Apply $RZ(\theta_{eff})$ on the last non-identity qubit.
143    ///
144    /// 6. **Reverse CNOT Ladder**: Apply CNOT gates in reverse order.
145    ///
146    /// 7. **Inverse Transformation**: Apply inverse basis transformations.
147    ///
148    /// # Global Phase
149    ///
150    /// If the Pauli string consists entirely of identity operators (e.g., "III"),
151    /// the operation reduces to a global phase $e^{-i\theta/2}$, which is added
152    /// to the circuit's global phase.
153    ///
154    /// # Examples
155    ///
156    /// ```rust
157    /// use cqlib_core::circuit::Circuit;
158    /// use cqlib_core::qis::pauli::PauliString;
159    /// use cqlib_core::qis::evolution::PauliEvolution;
160    ///
161    /// let mut circuit = Circuit::new(3);
162    /// let qubits = circuit.qubits();
163    ///
164    /// // Create a Pauli string X ⊗ Z ⊗ I
165    /// let pauli: PauliString = "XZI".parse().unwrap();
166    ///
167    /// // Apply e^(-i * π/4 * XZI)
168    /// circuit.pauli_evolution(&pauli, std::f64::consts::PI / 2.0, &qubits).unwrap();
169    /// ```
170    fn pauli_evolution(
171        &mut self,
172        pauli: &PauliString,
173        angle: impl Into<ParameterValue>,
174        qubits: &[Qubit],
175    ) -> Result<(), CircuitError>;
176}
177
178impl PauliEvolution for Circuit {
179    fn pauli_evolution(
180        &mut self,
181        pauli: &PauliString,
182        angle: impl Into<ParameterValue>,
183        qubits: &[Qubit],
184    ) -> Result<(), CircuitError> {
185        // Validate qubit count
186        if qubits.len() != pauli.num_qubits {
187            return Err(CircuitError::QubitCountMismatch {
188                expected: pauli.num_qubits,
189                actual: qubits.len(),
190            });
191        }
192
193        // Check that the PauliString phase is Hermitian (±1)
194        // Only Hermitian operators can generate unitary evolution
195        let phase_complex = pauli.phase.to_complex();
196
197        // For Hermitian operators, phase must be real (±1)
198        // If phase is ±i, the operator is not Hermitian
199        if phase_complex.im.abs() > 1e-10 {
200            return Err(CircuitError::InvalidOperation(
201                "Pauli string phase must be Hermitian (±1)".to_string(),
202            ));
203        }
204
205        // Absorb PauliString's phase into the rotation angle
206        // phase_complex.re is either 1.0 or -1.0
207        let angle_param: ParameterValue = angle.into();
208        let adjusted_angle = if phase_complex.re < 0.0 {
209            angle_param.scaled(-1.0)
210        } else {
211            angle_param
212        };
213
214        // Collect non-identity Pauli operators with their positions
215        let mut non_identity_positions: Vec<(usize, Pauli)> = Vec::new();
216        for i in 0..pauli.num_qubits {
217            let x_bit = pauli.x[i];
218            let z_bit = pauli.z[i];
219
220            let pauli_op = match (x_bit, z_bit) {
221                (false, false) => Pauli::I,
222                (true, false) => Pauli::X,
223                (true, true) => Pauli::Y,
224                (false, true) => Pauli::Z,
225            };
226
227            if pauli_op != Pauli::I {
228                non_identity_positions.push((i, pauli_op));
229            }
230        }
231
232        // Handle the case where all Pauli operators are identity
233        if non_identity_positions.is_empty() {
234            // The operation is e^(-iθ/2 * I) = e^(-iθ/2) * I
235            // This is just a global phase
236            let global_phase_param = adjusted_angle.scaled(-0.5);
237
238            // Add to circuit's global phase
239            let current_phase = self.global_phase();
240            let global_phase_param = Parameter::from(global_phase_param);
241            let new_phase = current_phase + global_phase_param;
242            self.set_global_phase(new_phase);
243
244            return Ok(());
245        }
246
247        // Apply basis transformations to convert non-Z Paulis to Z basis
248        // Store the transformations for later inversion
249        let mut transformations: Vec<(Qubit, Pauli)> =
250            Vec::with_capacity(non_identity_positions.len());
251
252        for (idx, pauli_op) in &non_identity_positions {
253            let qubit = qubits[*idx];
254            transformations.push((qubit, *pauli_op));
255
256            match pauli_op {
257                Pauli::X => {
258                    // X -> Z: Apply H
259                    self.h(qubit)?;
260                }
261                Pauli::Y => {
262                    // Y -> Z basis change: circuit order S† → H
263                    // S† maps |y+⟩→|x+⟩, then H maps |x+⟩→|z+⟩.
264                    // Inverse (uncompute): H → S (see inverse block below).
265                    self.sdg(qubit)?;
266                    self.h(qubit)?;
267                }
268                Pauli::Z => {
269                    // Already in Z basis, no transformation needed
270                }
271                Pauli::I => unreachable!(),
272            }
273        }
274
275        // Chain CNOT ladder: accumulate parity along the chain to the last qubit
276        // For non-identity qubits at positions [i0, i1, i2, ...], apply:
277        // CNOT(i0 -> i1), CNOT(i1 -> i2), ...
278        let num_non_identity = non_identity_positions.len();
279        let last_qubit = qubits[non_identity_positions[num_non_identity - 1].0];
280
281        // Forward chain: propagate parity from first to last
282        for i in 0..num_non_identity - 1 {
283            let curr_qubit = qubits[non_identity_positions[i].0];
284            let next_qubit = qubits[non_identity_positions[i + 1].0];
285            self.cx(curr_qubit, next_qubit)?;
286        }
287
288        // Apply RZ(θ) on the last qubit
289        // After the CNOT chain, this effectively rotates the joint parity eigenstate
290        self.rz(last_qubit, adjusted_angle)?;
291
292        // Reverse chain: uncompute parity from last-1 down to 0
293        for i in (0..num_non_identity - 1).rev() {
294            let curr_qubit = qubits[non_identity_positions[i].0];
295            let next_qubit = qubits[non_identity_positions[i + 1].0];
296            self.cx(curr_qubit, next_qubit)?;
297        }
298
299        // Apply inverse basis transformations in reverse order
300        for (qubit, pauli_op) in transformations.iter().rev() {
301            match pauli_op {
302                Pauli::X => {
303                    // H† = H
304                    self.h(*qubit)?;
305                }
306                Pauli::Y => {
307                    // Inverse of (S† → H): apply H† = H, then S†† = S.
308                    // (H · S†)⁻¹ = S · H
309                    self.h(*qubit)?;
310                    self.s(*qubit)?;
311                }
312                Pauli::Z => {
313                    // No transformation needed
314                }
315                Pauli::I => unreachable!(),
316            }
317        }
318
319        Ok(())
320    }
321}
322
323// These `pub(crate)` functions implement the product-formula math for both the
324// numeric API (`to_trotter_circuit` / `to_evolution_circuit`) and the parametric
325// ansatz (`PauliEvolutionAnsatz` in `circuit/ansatz/hamiltonian_evolution.rs`).
326//
327// Angle convention: `pauli_evolution` implements e^{-iθ/2 · P}; to realize
328// e^{-i c t P} we pass θ = 2 c t.
329//
330// Both `ParameterValue::Fixed(f64)` and `ParameterValue::Param(Parameter)` are
331// handled uniformly via `ParameterValue::scaled`.
332
333/// Applies first-order Lie-Trotter (or randomized product-formula) decomposition.
334///
335/// Realizes:
336/// $$U(t) \approx \left[\prod_k e^{-i c_k t/n \cdot P_k}\right]^n$$
337///
338/// # Arguments
339///
340/// * `terms`  – Pauli terms of the (simplified) Hamiltonian.
341/// * `t`      – Total evolution time, numeric (`Fixed`) or symbolic (`Param`).
342/// * `steps`  – Number of Trotter repetitions $n$.
343/// * `qubits` – Circuit qubits (must cover all Pauli positions).
344/// * `rng`    – If `Some`, term order is shuffled each step (randomized product formula).
345///   If `None`, terms are applied in fixed order (standard first-order).
346///
347/// # Performance
348///
349/// Angle `ParameterValue`s are pre-computed once per term (outside the step loop)
350/// to avoid O(m·n) Arc-node allocations for the symbolic case.
351pub(crate) fn trotter_first_order_core(
352    circuit: &mut Circuit,
353    terms: &[(PauliString, Complex64)],
354    t: ParameterValue,
355    steps: usize,
356    qubits: &[Qubit],
357    mut rng: Option<&mut StdRng>,
358) -> Result<(), CircuitError> {
359    // Pre-compute one ParameterValue per term: θ_k = 2 c_k t / n.
360    let angles: Vec<ParameterValue> = terms
361        .iter()
362        .map(|(_, coeff)| {
363            // Even finite coefficients can overflow when computing the factor.
364            let factor = 2.0 * coeff.re / steps as f64;
365            if !factor.is_finite() {
366                return Err(CircuitError::InvalidOperation(format!(
367                    "Hamiltonian evolution scale factor must be finite, got {factor}"
368                )));
369            }
370            Ok(t.scaled(factor))
371        })
372        .collect::<Result<_, _>>()?;
373
374    let mut indices: Vec<usize> = (0..terms.len()).collect();
375
376    for _ in 0..steps {
377        if let Some(ref mut r) = rng {
378            indices.shuffle(*r);
379        }
380        for &idx in &indices {
381            let (pauli, _) = &terms[idx];
382            circuit
383                .pauli_evolution(pauli, angles[idx].clone(), qubits)
384                .map_err(|e| {
385                    CircuitError::InvalidOperation(format!("Failed to add Pauli evolution: {}", e))
386                })?;
387        }
388    }
389    Ok(())
390}
391
392/// Applies second-order Suzuki (Strang) splitting decomposition.
393///
394/// Realizes:
395/// $$U(t) \approx \left[\prod_k e^{-i c_k t/(2n) P_k}
396///   \cdot \prod_k^{\leftarrow} e^{-i c_k t/(2n) P_k}\right]^n$$
397///
398/// # Arguments
399///
400/// * `terms`  – Pauli terms of the (simplified) Hamiltonian.
401/// * `t`      – Total evolution time, numeric (`Fixed`) or symbolic (`Param`).
402/// * `steps`  – Number of Suzuki repetitions $n$.
403/// * `qubits` – Circuit qubits.
404///
405/// # Performance
406///
407/// Half-step angles are pre-computed per term (θ_k = c_k t / n) outside the step
408/// loop; forward and backward passes share the same angle array.
409pub(crate) fn trotter_second_order_core(
410    circuit: &mut Circuit,
411    terms: &[(PauliString, Complex64)],
412    t: ParameterValue,
413    steps: usize,
414    qubits: &[Qubit],
415) -> Result<(), CircuitError> {
416    // Half-step angle: θ_k = 2 c_k (t/2n) = c_k t / n.
417    // Forward and backward passes share the same value by symmetry.
418    let angles: Vec<ParameterValue> = terms
419        .iter()
420        .map(|(_, coeff)| {
421            let factor = coeff.re / steps as f64;
422            if !factor.is_finite() {
423                return Err(CircuitError::InvalidOperation(format!(
424                    "Hamiltonian evolution scale factor must be finite, got {factor}"
425                )));
426            }
427            Ok(t.scaled(factor))
428        })
429        .collect::<Result<_, _>>()?;
430
431    for _ in 0..steps {
432        // Forward half-step
433        for (idx, (pauli, _)) in terms.iter().enumerate() {
434            circuit
435                .pauli_evolution(pauli, angles[idx].clone(), qubits)
436                .map_err(|e| {
437                    CircuitError::InvalidOperation(format!("Failed to add Pauli evolution: {}", e))
438                })?;
439        }
440        // Backward half-step in reverse term order
441        for (idx, (pauli, _)) in terms.iter().enumerate().rev() {
442            circuit
443                .pauli_evolution(pauli, angles[idx].clone(), qubits)
444                .map_err(|e| {
445                    CircuitError::InvalidOperation(format!("Failed to add Pauli evolution: {}", e))
446                })?;
447        }
448    }
449    Ok(())
450}
451
452// Numeric evolution historically reports coefficient overflow as unsupported.
453fn normalized_evolution(h: &Hamiltonian) -> Result<Hamiltonian, QisError> {
454    h.normalized_hermitian().map_err(|error| match error {
455        nonfinite @ QisError::NonFiniteCoefficient { .. } => {
456            QisError::UnsupportedOperation(nonfinite.to_string())
457        }
458        other => other,
459    })
460}
461
462impl Hamiltonian {
463    /// Converts the Hamiltonian to a Trotterized time evolution circuit.
464    ///
465    /// Implements Trotter-Suzuki decomposition to approximate the time evolution
466    /// operator $U(t) = e^{-iHt}$ as a sequence of Pauli rotations.
467    ///
468    /// This method **always** applies the product formula regardless of whether
469    /// the Hamiltonian terms commute. For an auto-selecting method that uses the
470    /// mathematically exact single-pass decomposition when all terms commute, see
471    /// [`to_evolution_circuit`](Hamiltonian::to_evolution_circuit).
472    /// Nonempty sums that cancel to zero produce an identity circuit, retaining
473    /// the declared qubit count. An empty input sum is rejected.
474    ///
475    /// # Arguments
476    ///
477    /// * `time` - The total evolution time $t$
478    /// * `steps` - The number of Trotter steps $n$ (must be > 0)
479    /// * `mode` - The Trotter decomposition mode (FirstOrder, SecondOrder, Randomized)
480    ///
481    /// # Returns
482    ///
483    /// * `Ok(Circuit)` - The approximated time evolution circuit
484    /// * `Err(QisError)` - If steps is 0, Hamiltonian is empty, or other error occurs
485    ///
486    /// # Mathematical Formulation
487    ///
488    /// For $H = \sum_k c_k P_k$, the decomposition approximates:
489    ///
490    /// **First Order:**
491    /// $$U(t) \approx \prod_{s=1}^{n} \prod_k e^{-i c_k \Delta t \cdot P_k}, \quad \Delta t = t/n$$
492    ///
493    /// **Second Order:**
494    /// $$U(t) \approx \prod_{s=1}^{n} \left[ \prod_k e^{-i c_k \Delta t/2 \cdot P_k}
495    /// \cdot \prod_k e^{-i c_k \Delta t/2 \cdot P_k} \right]$$
496    /// (with reverse order in second product)
497    ///
498    /// **Randomized:**
499    /// Like first order, but with randomized term order in each step.
500    ///
501    /// # Examples
502    ///
503    /// ```rust
504    /// use cqlib_core::qis::{Hamiltonian, PauliString};
505    /// use cqlib_core::qis::evolution::{PauliEvolution, TrotterMode};
506    ///
507    /// // Create H = 0.5 * ZZ + 0.3 * XX
508    /// let mut h = Hamiltonian::new(2);
509    /// h.add_term("ZZ".parse::<PauliString>().unwrap(), 0.5.into()).unwrap();
510    /// h.add_term("XX".parse::<PauliString>().unwrap(), 0.3.into()).unwrap();
511    ///
512    /// // Generate Trotter circuit for t=1.0 with 10 steps
513    /// let circuit = h.to_trotter_circuit(1.0, 10, TrotterMode::FirstOrder).unwrap();
514    ///
515    /// // Circuit contains Pauli evolution gates
516    /// assert!(circuit.num_qubits() >= 2);
517    /// ```
518    pub fn to_trotter_circuit(
519        &self,
520        time: f64,
521        steps: usize,
522        mode: TrotterMode,
523    ) -> Result<Circuit, QisError> {
524        if steps == 0 {
525            return Err(QisError::InvalidParameterValue(
526                "Trotter steps must be greater than 0".to_string(),
527            ));
528        }
529
530        if self.terms.is_empty() {
531            return Err(QisError::InvalidParameterValue(
532                "Cannot create Trotter circuit from empty Hamiltonian".to_string(),
533            ));
534        }
535
536        let simplified_h = normalized_evolution(self)?;
537        let mut circuit = Circuit::new(simplified_h.num_qubits);
538        let qubits: Vec<Qubit> = circuit.qubits();
539        let t = ParameterValue::Fixed(time);
540
541        // Dispatch to shared Trotter core helpers.
542        //
543        // NOTE: The parametric counterpart of this logic lives in
544        // `PauliEvolutionAnsatz::build_circuit()` (circuit/ansatz/hamiltonian_evolution.rs),
545        // which calls the same `trotter_first_order_core` / `trotter_second_order_core`
546        // with a symbolic `ParameterValue::Param(t)` instead of `Fixed(f64)`.
547        let err_map = |e: CircuitError| {
548            QisError::UnsupportedOperation(format!("Failed to add Pauli evolution: {}", e))
549        };
550
551        match mode {
552            TrotterMode::FirstOrder => {
553                trotter_first_order_core(
554                    &mut circuit,
555                    &simplified_h.terms,
556                    t,
557                    steps,
558                    &qubits,
559                    None,
560                )
561                .map_err(err_map)?;
562            }
563            TrotterMode::Randomized(seed) => {
564                let mut rng = StdRng::seed_from_u64(seed);
565                trotter_first_order_core(
566                    &mut circuit,
567                    &simplified_h.terms,
568                    t,
569                    steps,
570                    &qubits,
571                    Some(&mut rng),
572                )
573                .map_err(err_map)?;
574            }
575            TrotterMode::SecondOrder => {
576                trotter_second_order_core(&mut circuit, &simplified_h.terms, t, steps, &qubits)
577                    .map_err(err_map)?;
578            }
579        }
580
581        Ok(circuit)
582    }
583
584    /// Converts the Hamiltonian to a time evolution circuit, automatically selecting
585    /// the exact decomposition when possible.
586    ///
587    /// Unlike [`to_trotter_circuit`](Hamiltonian::to_trotter_circuit), this method
588    /// checks whether all Hamiltonian terms mutually commute:
589    ///
590    /// - **Commuting terms**: uses an exact single-pass decomposition
591    ///   $U(t) = \prod_k e^{-i c_k t P_k}$ (no approximation error, `steps` is ignored).
592    /// - **Non-commuting terms**: falls back to the Trotter approximation specified by
593    ///   `mode` and `steps`.
594    ///
595    /// This mirrors the behavior of [`PauliEvolutionAnsatz`] with
596    /// [`EvolutionStrategy::Auto`], closing the capability gap between the numeric
597    /// and parametric APIs.
598    /// Nonempty sums that cancel to zero produce an identity circuit, retaining
599    /// the declared qubit count. An empty input sum is rejected.
600    ///
601    /// # Arguments
602    ///
603    /// * `time`  - Total evolution time $t$
604    /// * `steps` - Trotter steps (used only when terms are non-commuting; must be ≥ 1)
605    /// * `mode`  - Trotter mode (used only when terms are non-commuting)
606    ///
607    /// # Returns
608    ///
609    /// * `Ok(Circuit)` - The evolution circuit (exact or approximate)
610    /// * `Err(QisError)` - If the Hamiltonian is empty, non-Hermitian, or `steps == 0`
611    ///
612    /// # Examples
613    ///
614    /// ```rust
615    /// use cqlib_core::qis::PauliString;
616    /// use cqlib_core::qis::Hamiltonian;
617    /// use cqlib_core::qis::evolution::TrotterMode;
618    ///
619    /// // Commuting Hamiltonian: ZZ and ZI share eigenstates — exact evolution
620    /// let mut h = Hamiltonian::new(2);
621    /// h.add_term("ZZ".parse::<PauliString>().unwrap(), 0.5.into()).unwrap();
622    /// h.add_term("ZI".parse::<PauliString>().unwrap(), 0.3.into()).unwrap();
623    /// // steps/mode are irrelevant here — exact path is taken automatically
624    /// let circuit = h.to_evolution_circuit(1.0, 1, TrotterMode::FirstOrder).unwrap();
625    ///
626    /// // Non-commuting: uses Trotter
627    /// let mut h2 = Hamiltonian::new(1);
628    /// h2.add_term("X".parse::<PauliString>().unwrap(), 1.0.into()).unwrap();
629    /// h2.add_term("Z".parse::<PauliString>().unwrap(), 1.0.into()).unwrap();
630    /// let circuit2 = h2.to_evolution_circuit(0.5, 10, TrotterMode::SecondOrder).unwrap();
631    /// ```
632    pub fn to_evolution_circuit(
633        &self,
634        time: f64,
635        steps: usize,
636        mode: TrotterMode,
637    ) -> Result<Circuit, QisError> {
638        if steps == 0 {
639            return Err(QisError::InvalidParameterValue(
640                "Trotter steps must be greater than 0".to_string(),
641            ));
642        }
643
644        if self.terms.is_empty() {
645            return Err(QisError::InvalidParameterValue(
646                "Cannot create evolution circuit from empty Hamiltonian".to_string(),
647            ));
648        }
649
650        let simplified_h = normalized_evolution(self)?;
651        let mut circuit = Circuit::new(simplified_h.num_qubits);
652        let qubits: Vec<Qubit> = circuit.qubits();
653
654        let err_map = |e: CircuitError| {
655            QisError::UnsupportedOperation(format!("Failed to add Pauli evolution: {}", e))
656        };
657
658        if simplified_h.all_terms_commute() {
659            // Exact decomposition: ∏_k e^{-i c_k t P_k}
660            // Angle convention: pauli_evolution implements e^{-iθ/2 P}, so θ = 2 c t.
661            for (pauli, coeff) in &simplified_h.terms {
662                circuit
663                    .pauli_evolution(pauli, 2.0 * coeff.re * time, &qubits)
664                    .map_err(err_map)?;
665            }
666        } else {
667            // Non-commuting: Trotter approximation
668            let t = ParameterValue::Fixed(time);
669            match mode {
670                TrotterMode::FirstOrder => {
671                    trotter_first_order_core(
672                        &mut circuit,
673                        &simplified_h.terms,
674                        t,
675                        steps,
676                        &qubits,
677                        None,
678                    )
679                    .map_err(err_map)?;
680                }
681                TrotterMode::Randomized(seed) => {
682                    let mut rng = StdRng::seed_from_u64(seed);
683                    trotter_first_order_core(
684                        &mut circuit,
685                        &simplified_h.terms,
686                        t,
687                        steps,
688                        &qubits,
689                        Some(&mut rng),
690                    )
691                    .map_err(err_map)?;
692                }
693                TrotterMode::SecondOrder => {
694                    trotter_second_order_core(&mut circuit, &simplified_h.terms, t, steps, &qubits)
695                        .map_err(err_map)?;
696                }
697            }
698        }
699
700        Ok(circuit)
701    }
702}
703
704#[cfg(test)]
705#[path = "evolution_test.rs"]
706mod evolution_test;