Skip to main content

cqlib_core/circuit/ansatz/
hamiltonian_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//! # Hamiltonian-to-Circuit Ansatz
14//!
15//! This module provides [`PauliEvolutionAnsatz`], a variational ansatz that compiles
16//! a [`Hamiltonian`] into a parameterized quantum circuit implementing (or approximating)
17//! the time evolution operator $U(t) = e^{-iHt}$, where the evolution time $t$ is a
18//! symbolic [`Parameter`].
19//!
20//! ## Mathematical Background
21//!
22//! For $H = \sum_k c_k P_k$, the time evolution operator is:
23//!
24//! $$U(t) = e^{-iHt} = e^{-i \sum_k c_k t P_k}$$
25//!
26//! ### Exact Evolution (Commuting Terms)
27//!
28//! If all terms $P_j$ mutually commute ($[P_j, P_k] = 0$ for all $j, k$), then the
29//! exponential factors exactly:
30//!
31//! $$e^{-iHt} = \prod_k e^{-i c_k t P_k}$$
32//!
33//! Each factor is a single Pauli rotation, directly implementable without error.
34//!
35//! ### Approximate Evolution (Non-Commuting Terms)
36//!
37//! When terms do not commute, we use product-formula approximations:
38//!
39//! **First-order Lie-Trotter** (error $O(t^2/n)$):
40//! $$U(t) \approx \left[\prod_k e^{-i c_k (t/n) P_k}\right]^n$$
41//!
42//! **Second-order Suzuki** (error $O(t^3/n^2)$):
43//! $$U(t) \approx \left[\prod_k e^{-i c_k (t/2n) P_k} \cdot
44//!   \prod_k^{\leftarrow} e^{-i c_k (t/2n) P_k}\right]^n$$
45//!
46//! ## Angle Convention
47//!
48//! The underlying [`PauliEvolution`] trait implements $e^{-i\theta/2 \cdot P}$.
49//! To realize $e^{-i c t P}$, the angle must be $\theta = 2 c t$.
50//!
51//! This convention is identical to the one used in
52//! [`QAOAAnsatz`](crate::circuit::ansatz::QAOAAnsatz):
53//! ```text
54//! let angle = t_param * (2.0 * coeff.re);
55//! ```
56//!
57//! ## Downstream Use Cases
58//!
59//! - **QAOA cost layers**: variational $e^{-i\gamma H_C}$
60//! - **VQE operator ansatz**: parameterized Hamiltonian evolution
61//! - **Variational time evolution**: $t$ optimized to match target dynamics
62//! - **Chemistry / spin-model simulation**: Trotter decomposition of physical Hamiltonians
63//!
64//! ## Example
65//!
66//! ```rust
67//! use cqlib_core::circuit::ansatz::{Ansatz, PauliEvolutionAnsatz};
68//! use cqlib_core::circuit::ansatz::hamiltonian_evolution::EvolutionStrategy;
69//! use cqlib_core::qis::hamiltonian::Hamiltonian;
70//!
71//! // H = 0.5 ZZ + 0.3 XI  (commuting diagonal terms)
72//! let mut h = Hamiltonian::new(2);
73//! h.add_term("ZZ".parse().unwrap(), 0.5.into()).unwrap();
74//! h.add_term("ZI".parse().unwrap(), 0.3.into()).unwrap();
75//!
76//! // Auto-detects exact evolution for commuting Hamiltonian
77//! let ansatz = PauliEvolutionAnsatz::new(h).unwrap();
78//! assert_eq!(ansatz.num_parameters(), 1);  // only time t
79//!
80//! let circuit = ansatz.build_circuit("evo").unwrap();
81//! println!("circuit {:?}", circuit);
82//! // circuit has parameter "evo_t"
83//! ```
84
85use super::traits::Ansatz;
86use crate::circuit::Parameter;
87use crate::circuit::circuit_impl::Circuit;
88use crate::circuit::circuit_param::ParameterValue;
89use crate::circuit::error::CircuitError;
90use crate::qis::error::QisError;
91use crate::qis::evolution::{
92    PauliEvolution, TrotterMode, trotter_first_order_core, trotter_second_order_core,
93};
94use crate::qis::hamiltonian::Hamiltonian;
95use rand::SeedableRng;
96use rand::rngs::StdRng;
97
98/// Strategy for compiling a [`Hamiltonian`] into a parameterized circuit.
99///
100/// The strategy determines whether the resulting circuit is an exact or approximate
101/// implementation of $e^{-iHt}$.
102#[derive(Debug, Clone, PartialEq, Eq)]
103pub enum EvolutionStrategy {
104    /// **Automatically detect** the best strategy:
105    ///
106    /// - If all Hamiltonian terms mutually commute, uses [`EvolutionStrategy::Exact`].
107    /// - Otherwise, applies first-order Trotter with the given number of steps.
108    ///
109    /// This is the recommended default for most use cases.
110    Auto {
111        /// Number of Trotter steps to use when the Hamiltonian is non-commuting.
112        /// Ignored when all terms commute (exact case).
113        steps: usize,
114    },
115
116    /// **Exact decomposition** — no Trotter approximation.
117    ///
118    /// Compiles each Pauli term independently as $e^{-i c_k t P_k}$ in a single pass.
119    /// This is valid **only** when all Hamiltonian terms mutually commute.
120    ///
121    /// Returns [`CircuitError::InvalidOperation`] from [`Ansatz::validate`] if any
122    /// two terms fail to commute.
123    Exact,
124
125    /// **Trotter-Suzuki approximation** with explicit mode and step count.
126    ///
127    /// Use this when you need fine-grained control over the approximation quality
128    /// and the Trotter mode (first-order, second-order, or randomized).
129    Trotter {
130        /// The Trotter decomposition algorithm to apply.
131        mode: TrotterMode,
132        /// Number of Trotter repetitions $n$. Must be $\geq 1$.
133        steps: usize,
134    },
135}
136
137/// Metadata about the compiled evolution returned by [`PauliEvolutionAnsatz::evolution_info`].
138///
139/// This struct documents whether the circuit is exact or approximate, which is
140/// essential for downstream callers that need to reason about approximation quality.
141#[derive(Debug, Clone)]
142pub struct EvolutionInfo {
143    /// Whether the decomposition is mathematically **exact**.
144    ///
145    /// `true` iff all Hamiltonian terms mutually commute.
146    ///
147    /// Note: [`EvolutionStrategy::Exact`] on a **non-commuting** Hamiltonian will report
148    /// `is_exact = false` here and will also fail [`Ansatz::validate`].
149    pub is_exact: bool,
150
151    /// The number of decomposition repetitions actually emitted into the circuit.
152    ///
153    /// - [`EvolutionStrategy::Exact`]: always `1`.
154    /// - [`EvolutionStrategy::Auto`] with a commuting Hamiltonian: `1` because the
155    ///   exact single-pass shortcut is taken.
156    /// - Explicit [`EvolutionStrategy::Trotter`]: the configured `steps` value, even
157    ///   when the Hamiltonian happens to commute and the result is mathematically exact.
158    pub steps: usize,
159
160    /// The decomposition mode used to build the circuit.
161    ///
162    /// `None` means the single-pass exact path was used. `Some(mode)` means an explicit
163    /// Trotter-style decomposition was emitted, which may still be mathematically exact
164    /// when all Hamiltonian terms commute.
165    pub trotter_mode: Option<TrotterMode>,
166
167    /// Whether all Hamiltonian terms mutually commute.
168    ///
169    /// When `true`, exact evolution is mathematically valid.
170    pub all_terms_commute: bool,
171
172    /// Number of Pauli terms in the (simplified) Hamiltonian.
173    pub num_terms: usize,
174}
175
176/// Ansatz for parameterized Hamiltonian time evolution.
177///
178/// Compiles $H = \sum_k c_k P_k$ into a parameterized quantum circuit that
179/// implements $U(t) = e^{-iHt}$ (or its Trotter approximation), where the
180/// evolution time $t$ is a single symbolic [`Parameter`].
181///
182/// # Requirements
183///
184/// - The Hamiltonian must be **Hermitian**: after phase normalization and merging, all
185///   coefficients must have negligible imaginary part ($|\text{Im}(c_k)| < 10^{-10}$).
186/// - The Hamiltonian must be **non-empty**.
187/// - For [`EvolutionStrategy::Exact`], all terms must mutually commute.
188/// - For `Trotter` / `Auto`, `steps` must be $\geq 1$.
189///
190/// # Angle Convention
191///
192/// The underlying [`PauliEvolution::pauli_evolution`] implements $e^{-i\theta/2 \cdot P}$.
193/// To realize $e^{-i c t P}$, we pass $\theta = 2 c t$ as the angle. This is the same
194/// convention used in [`QAOAAnsatz`](super::qaoa::QAOAAnsatz).
195///
196/// # Output Circuit
197///
198/// The built circuit always has exactly **one symbolic parameter**: the evolution time $t$.
199/// Its name follows this priority:
200///
201/// 1. If [`with_time_param_name`](PauliEvolutionAnsatz::with_time_param_name) was called
202///    with a non-empty name: use that name exactly.
203/// 2. Otherwise: `"{prefix}_t"` where `prefix` is the argument to
204///    [`build_circuit`](Ansatz::build_circuit).
205///
206/// # Example
207///
208/// ```rust
209/// use cqlib_core::circuit::ansatz::{Ansatz, PauliEvolutionAnsatz};
210/// use cqlib_core::circuit::ansatz::hamiltonian_evolution::EvolutionStrategy;
211/// use cqlib_core::qis::hamiltonian::Hamiltonian;
212/// use cqlib_core::qis::evolution::TrotterMode;
213///
214/// // Non-commuting Hamiltonian: H = X + Z
215/// let mut h = Hamiltonian::new(1);
216/// h.add_term("X".parse().unwrap(), 1.0.into()).unwrap();
217/// h.add_term("Z".parse().unwrap(), 1.0.into()).unwrap();
218///
219/// let ansatz = PauliEvolutionAnsatz::new(h)
220///     .unwrap()
221///     .with_strategy(EvolutionStrategy::Trotter {
222///         mode: TrotterMode::SecondOrder,
223///         steps: 10,
224///     })
225///     .with_time_param_name("tau");
226///
227/// let circuit = ansatz.build_circuit("evo").unwrap();
228/// // Parameter is named "tau" (explicit override)
229/// ```
230#[derive(Debug, Clone)]
231pub struct PauliEvolutionAnsatz {
232    /// The simplified Hamiltonian (normalized, de-duplicated).
233    ///
234    /// **Invariant**: always in simplified form (phases absorbed, duplicates merged,
235    /// exact-zero terms removed). Any future mutating API must re-run normalization and
236    /// re-validate Hermiticity before updating this field.
237    hamiltonian: Hamiltonian,
238    /// Cached result of `hamiltonian.all_terms_commute()`, computed once in `new()`.
239    ///
240    /// Stored to avoid repeated O(m² · n/64) computation across `validate()`,
241    /// `evolution_info()`, and `build_circuit()`.
242    all_terms_commute: bool,
243    /// Optional explicit name for the time parameter.
244    ///
245    /// `None` → derive name as `"{prefix}_t"` in `build_circuit`.
246    /// `Some(name)` → use `name` exactly, ignoring `prefix`.
247    time_param_name: Option<String>,
248    /// The compilation strategy.
249    strategy: EvolutionStrategy,
250}
251
252impl PauliEvolutionAnsatz {
253    /// Creates a new ansatz from a Hamiltonian.
254    ///
255    /// Validates and normalizes the Hamiltonian (merges duplicate terms, absorbs
256    /// phases into coefficients and removes exact zeros). Small nonzero coefficients
257    /// are retained, matching the numeric Hamiltonian evolution APIs. Imaginary
258    /// roundoff residuals of magnitude at most `1e-10` are projected to zero.
259    ///
260    /// The default strategy is [`EvolutionStrategy::Auto`] with `steps = 1`.
261    ///
262    /// # Errors
263    ///
264    /// Returns [`QisError::InvalidParameterValue`] if the Hamiltonian is empty after
265    /// real projection, or [`QisError::NotHermitian`] if any merged coefficient
266    /// has an imaginary part of magnitude greater than `1e-10`.
267    pub fn new(hamiltonian: Hamiltonian) -> Result<Self, QisError> {
268        let h = hamiltonian.normalized_hermitian()?;
269
270        if h.terms.is_empty() {
271            return Err(QisError::InvalidParameterValue(
272                "Hamiltonian is empty after simplification; cannot build evolution circuit"
273                    .to_string(),
274            ));
275        }
276
277        // Cache commutativity once; reused by validate(), evolution_info(), build_circuit().
278        let all_terms_commute = h.all_terms_commute();
279
280        Ok(Self {
281            hamiltonian: h,
282            all_terms_commute,
283            time_param_name: None,
284            strategy: EvolutionStrategy::Auto { steps: 1 },
285        })
286    }
287
288    /// Overrides the name of the time parameter in the built circuit.
289    ///
290    /// - `Some(name)` or any non-empty string: [`build_circuit`](Ansatz::build_circuit)
291    ///   will use that name exactly, ignoring the `prefix` argument for the time parameter.
292    ///   Useful when composing multiple ansatze that must share a common time parameter.
293    /// - `None` (the default): the time parameter is named `"{prefix}_t"`.
294    ///
295    /// The name must be a single symbolic identifier and must not shadow the built-in
296    /// mathematical constants `e` or `π`.
297    ///
298    /// Accepts `impl Into<String>` for ergonomic use: `with_time_param_name("tau")`.
299    pub fn with_time_param_name(mut self, name: impl Into<String>) -> Self {
300        let s = name.into();
301        self.time_param_name = if s.is_empty() { None } else { Some(s) };
302        self
303    }
304
305    /// Sets the compilation strategy.
306    ///
307    /// See [`EvolutionStrategy`] for available options and their trade-offs.
308    pub fn with_strategy(mut self, strategy: EvolutionStrategy) -> Self {
309        self.strategy = strategy;
310        self
311    }
312
313    /// Returns metadata about the compiled evolution based on the current configuration.
314    ///
315    /// This is a **cheap** introspection method — it reads the cached commutativity
316    /// result computed at construction time and does not build the circuit.
317    ///
318    /// ### `EvolutionStrategy::Exact` and non-commuting Hamiltonians
319    ///
320    /// If the strategy is `Exact` but the Hamiltonian is non-commuting,
321    /// `is_exact` will be `false` and `all_terms_commute` will be `false`.
322    /// Calling [`Ansatz::build_circuit`] in this configuration will fail at `validate()`.
323    pub fn evolution_info(&self) -> EvolutionInfo {
324        let all_commute = self.all_terms_commute;
325
326        let (is_exact, steps, trotter_mode) = match &self.strategy {
327            EvolutionStrategy::Exact => (all_commute, 1, None),
328            EvolutionStrategy::Auto { steps } => {
329                if all_commute {
330                    (true, 1, None)
331                } else {
332                    (false, *steps, Some(TrotterMode::FirstOrder))
333                }
334            }
335            EvolutionStrategy::Trotter { mode, steps } => (all_commute, *steps, Some(*mode)),
336        };
337
338        EvolutionInfo {
339            is_exact,
340            steps,
341            trotter_mode,
342            all_terms_commute: all_commute,
343            num_terms: self.hamiltonian.terms.len(),
344        }
345    }
346}
347
348impl Ansatz for PauliEvolutionAnsatz {
349    /// Validates the ansatz configuration without building the circuit.
350    ///
351    /// Checks:
352    /// - Hamiltonian is non-empty.
353    /// - All coefficients are real (Hermitian requirement).
354    /// - [`EvolutionStrategy::Exact`] is only used when all terms commute.
355    /// - Trotter step count is $\geq 1$.
356    fn validate(&self) -> Result<(), CircuitError> {
357        if self.hamiltonian.terms.is_empty() {
358            return Err(CircuitError::InvalidOperation(
359                "Hamiltonian is empty; cannot build evolution circuit".to_string(),
360            ));
361        }
362
363        // Hermitian check (redundant with new(), but guards against any future
364        // internal mutation that bypasses the constructor invariant).
365        for (pauli, coeff) in &self.hamiltonian.terms {
366            if coeff.im.abs() > 1e-10 {
367                return Err(CircuitError::InvalidOperation(format!(
368                    "Hamiltonian is not Hermitian: coefficient of {} has imaginary part {}",
369                    pauli, coeff.im
370                )));
371            }
372        }
373
374        // Strategy-specific validation — uses cached commutativity result.
375        match &self.strategy {
376            EvolutionStrategy::Exact => {
377                if !self.all_terms_commute {
378                    return Err(CircuitError::InvalidOperation(
379                        "EvolutionStrategy::Exact requires all Hamiltonian terms to \
380                         mutually commute. Use EvolutionStrategy::Auto or \
381                         EvolutionStrategy::Trotter for non-commuting Hamiltonians."
382                            .to_string(),
383                    ));
384                }
385            }
386            EvolutionStrategy::Auto { steps } => {
387                if *steps == 0 {
388                    return Err(CircuitError::InvalidOperation(
389                        "EvolutionStrategy::Auto requires steps >= 1".to_string(),
390                    ));
391                }
392            }
393            EvolutionStrategy::Trotter { steps, .. } => {
394                if *steps == 0 {
395                    return Err(CircuitError::InvalidOperation(
396                        "EvolutionStrategy::Trotter requires steps >= 1".to_string(),
397                    ));
398                }
399            }
400        }
401
402        Ok(())
403    }
404
405    /// Builds the parameterized time evolution circuit.
406    ///
407    /// The resulting circuit has exactly one symbolic parameter: the evolution time $t$.
408    ///
409    /// ## Parameter Naming
410    ///
411    /// - If [`with_time_param_name`](PauliEvolutionAnsatz::with_time_param_name) was called
412    ///   with a non-empty name, that name is used exactly.
413    /// - Otherwise the parameter is named `"{prefix}_t"`.
414    ///
415    /// ## Angle Convention
416    ///
417    /// Each Pauli term $c_k P_k$ contributes a rotation angle $\theta_k = 2 c_k t$
418    /// (or $\theta_k = 2 c_k t / n$ per Trotter step). This realizes $e^{-i c_k t P_k}$
419    /// via the underlying `pauli_evolution` call which implements $e^{-i \theta/2 P}$.
420    ///
421    /// ## Approximation Labeling
422    ///
423    /// The docstring of the returned circuit is not annotated internally, but callers can
424    /// use [`evolution_info`](PauliEvolutionAnsatz::evolution_info) to query whether the
425    /// circuit is exact or approximate.
426    fn build_circuit(&self, prefix: &str) -> Result<Circuit, CircuitError> {
427        self.validate()?;
428
429        // Resolve time parameter name: explicit override or prefix-derived.
430        let param_name = match &self.time_param_name {
431            Some(name) => name.clone(),
432            None => format!("{}_t", prefix),
433        };
434
435        validate_time_parameter_name(&param_name)?;
436        let t = Parameter::try_from(param_name.as_str()).map_err(|_| {
437            CircuitError::InvalidOperation(format!(
438                "Invalid time parameter name '{param_name}': expected a single symbolic identifier"
439            ))
440        })?;
441
442        let mut circuit = Circuit::new(self.hamiltonian.num_qubits);
443        let qubits = circuit.qubits();
444
445        // Use cached commutativity — not re-computed here.
446        let all_commute = self.all_terms_commute;
447
448        match &self.strategy {
449            EvolutionStrategy::Exact => {
450                // Exact single-pass; validate() already ensures all_commute == true
451                apply_exact_evolution(&mut circuit, &self.hamiltonian, &t, &qubits)?;
452            }
453
454            EvolutionStrategy::Auto { steps } => {
455                if all_commute {
456                    // Exact path: no approximation error
457                    apply_exact_evolution(&mut circuit, &self.hamiltonian, &t, &qubits)?;
458                } else {
459                    // Fall back to first-order Trotter via shared core helper
460                    trotter_first_order_core(
461                        &mut circuit,
462                        &self.hamiltonian.terms,
463                        ParameterValue::Param(t),
464                        *steps,
465                        &qubits,
466                        None,
467                    )?;
468                }
469            }
470
471            EvolutionStrategy::Trotter { mode, steps } => {
472                let n = *steps;
473                match mode {
474                    TrotterMode::FirstOrder => {
475                        trotter_first_order_core(
476                            &mut circuit,
477                            &self.hamiltonian.terms,
478                            ParameterValue::Param(t),
479                            n,
480                            &qubits,
481                            None,
482                        )?;
483                    }
484                    TrotterMode::SecondOrder => {
485                        trotter_second_order_core(
486                            &mut circuit,
487                            &self.hamiltonian.terms,
488                            ParameterValue::Param(t),
489                            n,
490                            &qubits,
491                        )?;
492                    }
493                    TrotterMode::Randomized(seed) => {
494                        let mut rng = StdRng::seed_from_u64(*seed);
495                        trotter_first_order_core(
496                            &mut circuit,
497                            &self.hamiltonian.terms,
498                            ParameterValue::Param(t),
499                            n,
500                            &qubits,
501                            Some(&mut rng),
502                        )?;
503                    }
504                }
505            }
506        }
507
508        Ok(circuit)
509    }
510
511    /// Returns `1`: the single evolution time parameter $t$.
512    fn num_parameters(&self) -> usize {
513        1
514    }
515
516    fn num_qubits(&self) -> usize {
517        self.hamiltonian.num_qubits
518    }
519}
520
521fn validate_time_parameter_name(name: &str) -> Result<(), CircuitError> {
522    if matches!(name, "e" | "π") {
523        return Err(CircuitError::InvalidOperation(format!(
524            "Invalid time parameter name '{name}': reserved mathematical constants cannot be used"
525        )));
526    }
527
528    let parsed = Parameter::try_from(name).map_err(|_| {
529        CircuitError::InvalidOperation(format!(
530            "Invalid time parameter name '{name}': expected a single symbolic identifier"
531        ))
532    })?;
533    let symbols = parsed.get_symbols();
534    if symbols.len() != 1 || !symbols.contains(name) {
535        return Err(CircuitError::InvalidOperation(format!(
536            "Invalid time parameter name '{name}': expected a single symbolic identifier"
537        )));
538    }
539
540    Ok(())
541}
542
543// The Trotter step helpers (first-order, second-order, randomized) are now the
544// shared `pub(crate)` functions `trotter_first_order_core` and
545// `trotter_second_order_core` defined in `qis/evolution.rs`. Both the numeric
546// API (`to_trotter_circuit` / `to_evolution_circuit`) and the parametric ansatz
547// call those same functions, eliminating duplicate implementations.
548//
549// Only the *exact* evolution helper remains here because it is specific to the
550// parametric (symbolic) path and has no numeric counterpart at this layer.
551
552/// Applies a single-pass exact evolution: ∏_k e^{-i c_k t P_k}.
553///
554/// Valid only when all Hamiltonian terms mutually commute.
555///
556/// **Identity terms** (`P_k = I⊗…⊗I`) contribute only a global phase `e^{-i c_k t}`.
557/// The underlying `pauli_evolution` handles all-identity strings correctly; no
558/// special skip logic is needed here. The global phase is not observable and is
559/// consistent across all evolution strategies.
560fn apply_exact_evolution(
561    circuit: &mut Circuit,
562    hamiltonian: &Hamiltonian,
563    t: &Parameter,
564    qubits: &[crate::circuit::bit::Qubit],
565) -> Result<(), CircuitError> {
566    for (pauli, coeff) in &hamiltonian.terms {
567        // Target: e^{-i c t P}
568        // pauli_evolution implements e^{-i θ/2 P}, so θ = 2 * c * t
569        let angle = t.clone() * (2.0 * coeff.re);
570        circuit.pauli_evolution(pauli, angle, qubits)?;
571    }
572    Ok(())
573}
574
575#[cfg(test)]
576#[path = "hamiltonian_evolution_test.rs"]
577mod hamiltonian_evolution_test;