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;