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(¶m_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;