use std::collections::HashMap;
use approx::assert_abs_diff_eq;
use ndarray::Array2;
use num_complex::Complex64;
use crate::circuit::ansatz::Ansatz;
use crate::circuit::circuit_to_matrix::circuit_to_matrix;
use crate::qis::evolution::TrotterMode;
use crate::qis::hamiltonian::Hamiltonian;
use super::{EvolutionStrategy, PauliEvolutionAnsatz};
fn hamiltonian_matrix(h: &Hamiltonian) -> Array2<Complex64> {
h.to_matrix()
.expect("Hamiltonian evolution tests use dimensionally valid terms")
}
fn matrix_exp_iht(h_mat: &Array2<Complex64>, t: f64) -> Array2<Complex64> {
let n = h_mat.nrows();
let neg_i = Complex64::new(0.0, -1.0);
let a: Array2<Complex64> = h_mat.mapv(|v| v * neg_i * t);
let mut result = Array2::<Complex64>::eye(n); let mut term = Array2::<Complex64>::eye(n);
for k in 1..=60usize {
term = term.dot(&a) / Complex64::from(k as f64);
result += &term;
if term.iter().all(|v| v.norm() < 1e-16) {
break;
}
}
result
}
fn frob_dist(a: &Array2<Complex64>, b: &Array2<Complex64>) -> f64 {
a.iter()
.zip(b.iter())
.map(|(x, y)| (x - y).norm_sqr())
.sum::<f64>()
.sqrt()
}
fn circuit_matrix_at_t(
ansatz: &PauliEvolutionAnsatz,
prefix: &str,
t_name: &str,
t_val: f64,
) -> Array2<Complex64> {
let circuit = ansatz.build_circuit(prefix).unwrap();
let mut bindings = HashMap::new();
bindings.insert(t_name, t_val);
let bound = circuit.assign_parameters(&Some(bindings)).unwrap();
circuit_to_matrix(&bound, None).unwrap()
}
#[test]
fn small_coefficients_survive_symbolic_evolution_and_trotter_scaling() {
for coefficient in [1e-16, 1e-12, -1e-16] {
for pauli in ["Z", "I", "X", "Y"] {
let h = Hamiltonian::from_list(vec![(
pauli.parse().unwrap(),
Complex64::new(coefficient, 0.0),
)])
.unwrap();
let time = 1.0 / coefficient;
let expected = matrix_exp_iht(&hamiltonian_matrix(&h), time);
for strategy in [
EvolutionStrategy::Exact,
EvolutionStrategy::Auto { steps: 512 },
EvolutionStrategy::Trotter {
mode: TrotterMode::FirstOrder,
steps: 512,
},
EvolutionStrategy::Trotter {
mode: TrotterMode::SecondOrder,
steps: 512,
},
EvolutionStrategy::Trotter {
mode: TrotterMode::Randomized(7),
steps: 512,
},
] {
let ansatz = PauliEvolutionAnsatz::new(h.clone())
.unwrap()
.with_strategy(strategy.clone());
let circuit = ansatz.build_circuit("small").unwrap();
assert_eq!(circuit.symbols().len(), ansatz.num_parameters());
assert!(circuit.symbols().contains("small_t"));
let partial = circuit.assign_parameters(&Some(HashMap::new())).unwrap();
assert!(partial.symbols().contains("small_t"));
let bound = partial
.assign_parameters(&Some(HashMap::from([("small_t", time)])))
.unwrap();
assert!(frob_dist(&circuit_to_matrix(&bound, None).unwrap(), &expected) < 1e-10);
let actual = circuit_matrix_at_t(&ansatz, "small", "small_t", time);
assert!(
frob_dist(&actual, &expected) < 1e-10,
"{pauli}, coefficient={coefficient}, strategy={strategy:?}"
);
}
}
}
}
#[test]
fn imaginary_residuals_are_projected_before_ansatz_zero_check() {
for pauli in ["I", "Z"] {
let h = Hamiltonian::from_list(vec![(pauli.parse().unwrap(), Complex64::new(0.0, 1e-12))])
.unwrap();
assert!(matches!(
PauliEvolutionAnsatz::new(h),
Err(crate::qis::QisError::InvalidParameterValue(_))
));
}
let h = Hamiltonian::from_list(vec![
("Z".parse().unwrap(), Complex64::new(1.0, 1e-12)),
("Z".parse().unwrap(), Complex64::new(-1.0, 0.0)),
])
.unwrap();
assert!(PauliEvolutionAnsatz::new(h).is_err());
let h =
Hamiltonian::from_list(vec![("Z".parse().unwrap(), Complex64::new(1e-16, 1e-12))]).unwrap();
let ansatz = PauliEvolutionAnsatz::new(h).unwrap();
assert_eq!(ansatz.hamiltonian.terms[0].1, Complex64::new(1e-16, 0.0));
let actual = circuit_matrix_at_t(&ansatz, "residual", "residual_t", 1e16);
let expected = matrix_exp_iht(&hamiltonian_matrix(&ansatz.hamiltonian), 1e16);
assert!(frob_dist(&actual, &expected) < 1e-12);
}
#[test]
fn mapped_small_number_operator_retains_time_and_global_phase() {
use crate::qis::fermion::{FermionToQubitMapper, FermionicOp, JordanWignerMapper};
let op = FermionicOp::number(1, 0)
.unwrap()
.scaled(Complex64::new(1e-16, 0.0))
.unwrap();
let h = JordanWignerMapper::new().map_operator(&op).unwrap();
let ansatz = PauliEvolutionAnsatz::new(h).unwrap();
let actual = circuit_matrix_at_t(&ansatz, "number", "number_t", 1e16);
let mut expected = Array2::<Complex64>::eye(2);
expected[[1, 1]] = Complex64::new(0.0, -1.0).exp();
assert!(frob_dist(&actual, &expected) < 1e-12);
}
#[test]
fn small_noncommuting_auto_evolution_matches_numeric_trotter() {
let h = Hamiltonian::from_list(vec![
("X".parse().unwrap(), Complex64::new(1e-12, 0.0)),
("Z".parse().unwrap(), Complex64::new(-1e-12, 0.0)),
])
.unwrap();
let numeric = h
.to_trotter_circuit(1e12, 512, TrotterMode::FirstOrder)
.unwrap();
let expected = circuit_to_matrix(&numeric, None).unwrap();
let ansatz = PauliEvolutionAnsatz::new(h)
.unwrap()
.with_strategy(EvolutionStrategy::Auto { steps: 512 });
let actual = circuit_matrix_at_t(&ansatz, "auto", "auto_t", 1e12);
assert!(frob_dist(&actual, &expected) < 1e-12);
}
#[test]
fn test_new_simplifies_hamiltonian() {
let mut h = Hamiltonian::new(1);
h.add_term("Z".parse().unwrap(), Complex64::new(0.5, 0.0))
.unwrap();
h.add_term("Z".parse().unwrap(), Complex64::new(0.5, 0.0))
.unwrap();
let ansatz = PauliEvolutionAnsatz::new(h).unwrap();
assert_eq!(ansatz.hamiltonian.terms.len(), 1);
let (_, coeff) = &ansatz.hamiltonian.terms[0];
assert_abs_diff_eq!(coeff.re, 1.0, epsilon = 1e-12);
}
#[test]
fn test_new_rejects_empty_hamiltonian() {
let h = Hamiltonian::new(2);
let result = PauliEvolutionAnsatz::new(h);
assert!(result.is_err(), "Empty Hamiltonian should fail");
}
#[test]
fn test_new_rejects_non_hermitian_hamiltonian() {
let mut h = Hamiltonian::new(1);
h.add_term("Z".parse().unwrap(), Complex64::new(0.0, 1.0))
.unwrap();
let result = PauliEvolutionAnsatz::new(h);
assert!(
result.is_err(),
"Non-Hermitian Hamiltonian (imaginary coeff) should be rejected"
);
}
#[test]
fn test_new_accepts_real_coeff_after_simplify() {
let mut ps: crate::qis::pauli::PauliString = "Z".parse().unwrap();
use crate::qis::pauli::Phase;
ps.phase = Phase::Minus;
let mut h = Hamiltonian::new(1);
h.add_term(ps, Complex64::new(0.5, 0.0)).unwrap();
let result = PauliEvolutionAnsatz::new(h);
assert!(
result.is_ok(),
"Hermitian after simplify() should be accepted"
);
}
#[test]
fn test_all_terms_commute_true_for_diagonal_hamiltonian() {
let mut h = Hamiltonian::new(2);
h.add_term("ZZ".parse().unwrap(), 1.0.into()).unwrap();
h.add_term("IZ".parse().unwrap(), 0.5.into()).unwrap();
assert!(h.all_terms_commute());
}
#[test]
fn test_all_terms_commute_false_for_x_z() {
let mut h = Hamiltonian::new(1);
h.add_term("X".parse().unwrap(), 1.0.into()).unwrap();
h.add_term("Z".parse().unwrap(), 1.0.into()).unwrap();
assert!(!h.all_terms_commute());
}
#[test]
fn test_all_terms_commute_empty_is_true() {
let h = Hamiltonian::new(2);
assert!(h.all_terms_commute());
}
#[test]
fn test_exact_strategy_rejects_noncommuting_hamiltonian() {
let mut h = Hamiltonian::new(1);
h.add_term("X".parse().unwrap(), 1.0.into()).unwrap();
h.add_term("Z".parse().unwrap(), 1.0.into()).unwrap();
let ansatz = PauliEvolutionAnsatz::new(h)
.unwrap()
.with_strategy(EvolutionStrategy::Exact);
let err = ansatz.validate();
assert!(
err.is_err(),
"Exact strategy on non-commuting H must return error"
);
}
#[test]
fn test_auto_selects_exact_path_for_commuting_hamiltonian() {
let mut h = Hamiltonian::new(2);
h.add_term("ZZ".parse().unwrap(), 1.0.into()).unwrap();
h.add_term("IZ".parse().unwrap(), 0.5.into()).unwrap();
let ansatz = PauliEvolutionAnsatz::new(h)
.unwrap()
.with_strategy(EvolutionStrategy::Auto { steps: 3 });
let info = ansatz.evolution_info();
assert!(info.is_exact, "Auto should choose exact for commuting H");
assert!(info.all_terms_commute);
assert_eq!(info.steps, 1, "Exact → 1 step");
assert!(info.trotter_mode.is_none());
}
#[test]
fn test_auto_selects_trotter_for_noncommuting_hamiltonian() {
let mut h = Hamiltonian::new(1);
h.add_term("X".parse().unwrap(), 1.0.into()).unwrap();
h.add_term("Z".parse().unwrap(), 1.0.into()).unwrap();
let ansatz = PauliEvolutionAnsatz::new(h)
.unwrap()
.with_strategy(EvolutionStrategy::Auto { steps: 5 });
let info = ansatz.evolution_info();
assert!(
!info.is_exact,
"Auto should choose Trotter for non-commuting H"
);
assert!(!info.all_terms_commute);
assert_eq!(info.steps, 5);
}
#[test]
fn test_zero_steps_returns_error() {
let mut h = Hamiltonian::new(1);
h.add_term("Z".parse().unwrap(), 1.0.into()).unwrap();
let ansatz = PauliEvolutionAnsatz::new(h)
.unwrap()
.with_strategy(EvolutionStrategy::Auto { steps: 0 });
assert!(ansatz.validate().is_err(), "steps=0 must be rejected");
let mut h2 = Hamiltonian::new(1);
h2.add_term("Z".parse().unwrap(), 1.0.into()).unwrap();
let ansatz2 =
PauliEvolutionAnsatz::new(h2)
.unwrap()
.with_strategy(EvolutionStrategy::Trotter {
mode: TrotterMode::FirstOrder,
steps: 0,
});
assert!(
ansatz2.validate().is_err(),
"Trotter steps=0 must be rejected"
);
}
#[test]
fn test_num_parameters_always_one() {
let mut h = Hamiltonian::new(2);
h.add_term("ZZ".parse().unwrap(), 1.0.into()).unwrap();
h.add_term("XX".parse().unwrap(), 0.5.into()).unwrap();
for strategy in [
EvolutionStrategy::Auto { steps: 3 },
EvolutionStrategy::Trotter {
mode: TrotterMode::FirstOrder,
steps: 5,
},
EvolutionStrategy::Trotter {
mode: TrotterMode::SecondOrder,
steps: 2,
},
] {
let ansatz = PauliEvolutionAnsatz::new(h.clone())
.unwrap()
.with_strategy(strategy);
assert_eq!(
ansatz.num_parameters(),
1,
"PauliEvolutionAnsatz always has exactly 1 parameter"
);
}
}
#[test]
fn test_num_qubits_matches_hamiltonian() {
for n in [1, 2, 3] {
let mut h = Hamiltonian::new(n);
let mut ps = crate::qis::pauli::PauliString::new(n);
ps.set_pauli(0, crate::qis::pauli::Pauli::Z);
h.add_term(ps, 1.0.into()).unwrap();
let ansatz = PauliEvolutionAnsatz::new(h).unwrap();
assert_eq!(ansatz.num_qubits(), n);
}
}
#[test]
fn test_time_param_name_default_uses_prefix() {
let mut h = Hamiltonian::new(1);
h.add_term("Z".parse().unwrap(), 1.0.into()).unwrap();
let ansatz = PauliEvolutionAnsatz::new(h).unwrap();
let circuit = ansatz.build_circuit("myevo").unwrap();
let symbols: std::collections::HashSet<String> = circuit
.parameters()
.iter()
.flat_map(|p| p.get_symbols())
.collect();
assert!(
symbols.contains("myevo_t"),
"Default param name must be '{{prefix}}_t'; got {:?}",
symbols
);
}
#[test]
fn test_time_param_name_override() {
let mut h = Hamiltonian::new(1);
h.add_term("Z".parse().unwrap(), 1.0.into()).unwrap();
let ansatz = PauliEvolutionAnsatz::new(h)
.unwrap()
.with_time_param_name("tau");
let circuit = ansatz.build_circuit("ignored_prefix").unwrap();
let symbols: std::collections::HashSet<String> = circuit
.parameters()
.iter()
.flat_map(|p| p.get_symbols())
.collect();
assert!(
symbols.contains("tau"),
"Explicit time param name 'tau' must appear; got {:?}",
symbols
);
assert!(
!symbols.contains("ignored_prefix_t"),
"Prefix-based name must NOT appear when explicit name is set"
);
}
#[test]
fn test_trotter1_gate_structure_1qubit() {
let mut h = Hamiltonian::new(1);
h.add_term("X".parse().unwrap(), 0.5.into()).unwrap();
h.add_term("Z".parse().unwrap(), 0.3.into()).unwrap();
let steps = 4;
let ansatz = PauliEvolutionAnsatz::new(h)
.unwrap()
.with_strategy(EvolutionStrategy::Trotter {
mode: TrotterMode::FirstOrder,
steps,
});
let circuit = ansatz.build_circuit("test").unwrap();
assert_eq!(circuit.num_qubits(), 1);
assert!(!circuit.operations().is_empty());
let expected_ops = steps * 4;
assert_eq!(
circuit.operations().len(),
expected_ops,
"Trotter-1 with steps={steps} on H=0.5X+0.3Z should produce {expected_ops} ops"
);
}
#[test]
fn test_trotter2_has_double_gate_count_vs_trotter1() {
let mut h = Hamiltonian::new(1);
h.add_term("Z".parse().unwrap(), 1.0.into()).unwrap();
let steps = 3;
let trotter1_ansatz =
PauliEvolutionAnsatz::new(h.clone())
.unwrap()
.with_strategy(EvolutionStrategy::Trotter {
mode: TrotterMode::FirstOrder,
steps,
});
let trotter2_ansatz =
PauliEvolutionAnsatz::new(h)
.unwrap()
.with_strategy(EvolutionStrategy::Trotter {
mode: TrotterMode::SecondOrder,
steps,
});
let c1 = trotter1_ansatz.build_circuit("t1").unwrap();
let c2 = trotter2_ansatz.build_circuit("t2").unwrap();
assert_eq!(
c2.operations().len(),
2 * c1.operations().len(),
"Suzuki-2 should have 2× the gate count of Trotter-1 for a single-term H"
);
}
#[test]
fn test_angle_convention_single_qubit_z() {
let coeff = 0.7_f64;
let t_val = 1.0_f64;
let mut h = Hamiltonian::new(1);
h.add_term("Z".parse().unwrap(), coeff.into()).unwrap();
let ansatz = PauliEvolutionAnsatz::new(h)
.unwrap()
.with_strategy(EvolutionStrategy::Exact);
let circuit = ansatz.build_circuit("ang").unwrap();
let mut bindings = HashMap::new();
bindings.insert("ang_t", t_val);
let bound = circuit.assign_parameters(&Some(bindings)).unwrap();
let expected_phase = -coeff * t_val;
let expected_diag_0 = Complex64::from_polar(1.0, expected_phase);
let expected_diag_1 = Complex64::from_polar(1.0, -expected_phase);
let mat = circuit_to_matrix(&bound, None).unwrap();
assert_abs_diff_eq!(mat[[0, 0]].re, expected_diag_0.re, epsilon = 1e-10);
assert_abs_diff_eq!(mat[[0, 0]].im, expected_diag_0.im, epsilon = 1e-10);
assert_abs_diff_eq!(mat[[1, 1]].re, expected_diag_1.re, epsilon = 1e-10);
assert_abs_diff_eq!(mat[[1, 1]].im, expected_diag_1.im, epsilon = 1e-10);
assert_abs_diff_eq!(mat[[0, 1]].norm(), 0.0, epsilon = 1e-10);
assert_abs_diff_eq!(mat[[1, 0]].norm(), 0.0, epsilon = 1e-10);
}
#[test]
fn test_matrix_exact_single_qubit_z() {
let t_val = 0.8_f64;
let mut h = Hamiltonian::new(1);
h.add_term("Z".parse().unwrap(), 1.0.into()).unwrap();
let ansatz = PauliEvolutionAnsatz::new(h.clone())
.unwrap()
.with_strategy(EvolutionStrategy::Exact);
let u_circuit = circuit_matrix_at_t(&ansatz, "ex", "ex_t", t_val);
let h_mat = hamiltonian_matrix(&h);
let u_exact = matrix_exp_iht(&h_mat, t_val);
let dist = frob_dist(&u_circuit, &u_exact);
assert!(
dist < 1e-10,
"Exact 1-qubit Z: Frobenius distance = {dist:.2e}, expected < 1e-10"
);
}
#[test]
fn test_matrix_exact_single_qubit_x() {
let coeff = 0.5_f64;
let t_val = 1.2_f64;
let mut h = Hamiltonian::new(1);
h.add_term("X".parse().unwrap(), coeff.into()).unwrap();
let ansatz = PauliEvolutionAnsatz::new(h.clone())
.unwrap()
.with_strategy(EvolutionStrategy::Exact);
let u_circuit = circuit_matrix_at_t(&ansatz, "ex", "ex_t", t_val);
let h_mat = hamiltonian_matrix(&h);
let u_exact = matrix_exp_iht(&h_mat, t_val);
let dist = frob_dist(&u_circuit, &u_exact);
assert!(
dist < 1e-10,
"Exact 1-qubit X: Frobenius distance = {dist:.2e}, expected < 1e-10"
);
}
#[test]
fn test_matrix_exact_commuting_2qubit() {
let t_val = 0.6_f64;
let mut h = Hamiltonian::new(2);
h.add_term("ZZ".parse().unwrap(), 1.0.into()).unwrap();
h.add_term("IZ".parse().unwrap(), 0.5.into()).unwrap();
let ansatz = PauliEvolutionAnsatz::new(h.clone())
.unwrap()
.with_strategy(EvolutionStrategy::Exact);
let u_circuit = circuit_matrix_at_t(&ansatz, "c2", "c2_t", t_val);
let h_mat = hamiltonian_matrix(&h);
let u_exact = matrix_exp_iht(&h_mat, t_val);
let dist = frob_dist(&u_circuit, &u_exact);
assert!(
dist < 1e-10,
"Exact 2-qubit commuting ZZ+IZ: Frobenius distance = {dist:.2e}, expected < 1e-10"
);
}
#[test]
fn test_matrix_auto_exact_path() {
let t_val = 0.5;
let mut h = Hamiltonian::new(2);
h.add_term("ZZ".parse().unwrap(), 0.8.into()).unwrap();
h.add_term("ZI".parse().unwrap(), 0.3.into()).unwrap();
let ansatz = PauliEvolutionAnsatz::new(h.clone())
.unwrap()
.with_strategy(EvolutionStrategy::Auto { steps: 5 });
assert!(ansatz.evolution_info().is_exact);
let u_circuit = circuit_matrix_at_t(&ansatz, "auto", "auto_t", t_val);
let h_mat = hamiltonian_matrix(&h);
let u_exact = matrix_exp_iht(&h_mat, t_val);
let dist = frob_dist(&u_circuit, &u_exact);
assert!(
dist < 1e-10,
"Auto-exact 2-qubit: distance = {dist:.2e}, expected < 1e-10"
);
}
#[test]
fn test_matrix_trotter1_error_decreases_with_steps() {
let t_val = 0.5_f64;
let mut h = Hamiltonian::new(1);
h.add_term("X".parse().unwrap(), 1.0.into()).unwrap();
h.add_term("Z".parse().unwrap(), 1.0.into()).unwrap();
let h_mat = hamiltonian_matrix(&h);
let u_exact = matrix_exp_iht(&h_mat, t_val);
let steps_list = [1usize, 5, 20, 100];
let mut prev_dist = f64::INFINITY;
for &steps in &steps_list {
let ansatz = PauliEvolutionAnsatz::new(h.clone()).unwrap().with_strategy(
EvolutionStrategy::Trotter {
mode: TrotterMode::FirstOrder,
steps,
},
);
let u_circuit = circuit_matrix_at_t(&ansatz, "tr1", "tr1_t", t_val);
let dist = frob_dist(&u_circuit, &u_exact);
assert!(
dist < prev_dist,
"Trotter-1 error should decrease as steps increase: \
steps={steps} dist={dist:.2e} >= prev_dist={prev_dist:.2e}"
);
prev_dist = dist;
}
}
#[test]
fn test_matrix_suzuki2_better_than_trotter1_equal_steps() {
let t_val = 0.8_f64;
let steps = 4;
let mut h = Hamiltonian::new(1);
h.add_term("X".parse().unwrap(), 1.0.into()).unwrap();
h.add_term("Z".parse().unwrap(), 0.5.into()).unwrap();
let h_mat = hamiltonian_matrix(&h);
let u_exact = matrix_exp_iht(&h_mat, t_val);
let trotter1_ansatz =
PauliEvolutionAnsatz::new(h.clone())
.unwrap()
.with_strategy(EvolutionStrategy::Trotter {
mode: TrotterMode::FirstOrder,
steps,
});
let suzuki2_ansatz =
PauliEvolutionAnsatz::new(h.clone())
.unwrap()
.with_strategy(EvolutionStrategy::Trotter {
mode: TrotterMode::SecondOrder,
steps,
});
let err1 = frob_dist(
&circuit_matrix_at_t(&trotter1_ansatz, "t1", "t1_t", t_val),
&u_exact,
);
let err2 = frob_dist(
&circuit_matrix_at_t(&suzuki2_ansatz, "t2", "t2_t", t_val),
&u_exact,
);
assert!(
err2 < err1,
"Suzuki-2 error ({err2:.2e}) should be smaller than Trotter-1 error ({err1:.2e}) \
for the same number of steps"
);
}
#[test]
fn test_matrix_suzuki2_error_decreases_with_steps() {
let t_val = 0.4_f64;
let mut h = Hamiltonian::new(1);
h.add_term("X".parse().unwrap(), 1.0.into()).unwrap();
h.add_term("Y".parse().unwrap(), 0.7.into()).unwrap();
h.add_term("Z".parse().unwrap(), 0.5.into()).unwrap();
let h_mat = hamiltonian_matrix(&h);
let u_exact = matrix_exp_iht(&h_mat, t_val);
let steps_list = [1usize, 4, 16];
let mut prev_dist = f64::INFINITY;
for &steps in &steps_list {
let ansatz = PauliEvolutionAnsatz::new(h.clone()).unwrap().with_strategy(
EvolutionStrategy::Trotter {
mode: TrotterMode::SecondOrder,
steps,
},
);
let u_circuit = circuit_matrix_at_t(&ansatz, "su2", "su2_t", t_val);
let dist = frob_dist(&u_circuit, &u_exact);
assert!(
dist < prev_dist,
"Suzuki-2 error should decrease as steps increase: \
steps={steps} dist={dist:.2e} >= prev_dist={prev_dist:.2e}"
);
prev_dist = dist;
}
}
#[test]
fn test_randomized_trotter_is_deterministic() {
let t_val = 0.5;
let seed = 42;
let mut h = Hamiltonian::new(1);
h.add_term("X".parse().unwrap(), 1.0.into()).unwrap();
h.add_term("Z".parse().unwrap(), 0.5.into()).unwrap();
let make_ansatz = || {
PauliEvolutionAnsatz::new(h.clone())
.unwrap()
.with_strategy(EvolutionStrategy::Trotter {
mode: TrotterMode::Randomized(seed),
steps: 5,
})
};
let m1 = circuit_matrix_at_t(&make_ansatz(), "rng", "rng_t", t_val);
let m2 = circuit_matrix_at_t(&make_ansatz(), "rng", "rng_t", t_val);
assert_abs_diff_eq!(frob_dist(&m1, &m2), 0.0, epsilon = 1e-15);
}
#[test]
fn test_matrix_trotter1_converges_for_large_steps() {
let t_val = 0.1_f64; let steps = 200;
let mut h = Hamiltonian::new(1);
h.add_term("X".parse().unwrap(), 1.0.into()).unwrap();
h.add_term("Z".parse().unwrap(), 1.0.into()).unwrap();
let ansatz =
PauliEvolutionAnsatz::new(h.clone())
.unwrap()
.with_strategy(EvolutionStrategy::Trotter {
mode: TrotterMode::FirstOrder,
steps,
});
let u_circuit = circuit_matrix_at_t(&ansatz, "conv", "conv_t", t_val);
let h_mat = hamiltonian_matrix(&h);
let u_exact = matrix_exp_iht(&h_mat, t_val);
let dist = frob_dist(&u_circuit, &u_exact);
assert!(
dist < 1e-4,
"Trotter-1 with steps={steps} at t={t_val}: distance={dist:.2e}, expected < 1e-4"
);
}
#[test]
fn test_matrix_suzuki2_converges_faster_than_trotter1() {
let t_val = 0.5_f64;
let steps = 10;
let mut h = Hamiltonian::new(1);
h.add_term("X".parse().unwrap(), 1.0.into()).unwrap();
h.add_term("Y".parse().unwrap(), 0.8.into()).unwrap();
let h_mat = hamiltonian_matrix(&h);
let u_exact = matrix_exp_iht(&h_mat, t_val);
let tr1 =
PauliEvolutionAnsatz::new(h.clone())
.unwrap()
.with_strategy(EvolutionStrategy::Trotter {
mode: TrotterMode::FirstOrder,
steps,
});
let su2 =
PauliEvolutionAnsatz::new(h.clone())
.unwrap()
.with_strategy(EvolutionStrategy::Trotter {
mode: TrotterMode::SecondOrder,
steps,
});
let err1 = frob_dist(&circuit_matrix_at_t(&tr1, "tr1", "tr1_t", t_val), &u_exact);
let err2 = frob_dist(&circuit_matrix_at_t(&su2, "su2", "su2_t", t_val), &u_exact);
assert!(
err2 < err1,
"Suzuki-2 (err={err2:.2e}) must beat Trotter-1 (err={err1:.2e}) at steps={steps}"
);
}
#[test]
fn test_matrix_exact_xx_zz_2qubit() {
let t_val = 0.4;
let mut h = Hamiltonian::new(2);
h.add_term("XX".parse().unwrap(), 0.6.into()).unwrap();
h.add_term("ZZ".parse().unwrap(), 0.4.into()).unwrap();
assert!(h.all_terms_commute(), "XX and ZZ commute");
let ansatz = PauliEvolutionAnsatz::new(h.clone())
.unwrap()
.with_strategy(EvolutionStrategy::Exact);
let u_circuit = circuit_matrix_at_t(&ansatz, "xxzz", "xxzz_t", t_val);
let h_mat = hamiltonian_matrix(&h);
let u_exact = matrix_exp_iht(&h_mat, t_val);
let dist = frob_dist(&u_circuit, &u_exact);
assert!(
dist < 1e-10,
"Exact 2-qubit XX+ZZ: Frobenius distance = {dist:.2e}, expected < 1e-10"
);
}
#[test]
fn test_evolution_info_exact_strategy_noncommuting_reports_not_exact() {
let mut h = Hamiltonian::new(1);
h.add_term("X".parse().unwrap(), 1.0.into()).unwrap();
h.add_term("Z".parse().unwrap(), 1.0.into()).unwrap();
let ansatz = PauliEvolutionAnsatz::new(h)
.unwrap()
.with_strategy(EvolutionStrategy::Exact);
let info = ansatz.evolution_info();
assert!(
!info.all_terms_commute,
"X and Z do not commute; all_terms_commute must be false"
);
assert!(
!info.is_exact,
"is_exact must be false when terms are non-commuting, even with Exact strategy. \
Got: is_exact={}, all_terms_commute={}",
info.is_exact, info.all_terms_commute
);
assert!(
ansatz.validate().is_err(),
"validate() must fail for Exact strategy on non-commuting Hamiltonian"
);
}
#[test]
fn test_evolution_info_exact_strategy_commuting_reports_exact() {
let mut h = Hamiltonian::new(2);
h.add_term("ZI".parse().unwrap(), 1.0.into()).unwrap();
h.add_term("ZZ".parse().unwrap(), 0.5.into()).unwrap();
let ansatz = PauliEvolutionAnsatz::new(h)
.unwrap()
.with_strategy(EvolutionStrategy::Exact);
let info = ansatz.evolution_info();
assert!(
info.all_terms_commute,
"ZI and ZZ commute; all_terms_commute must be true"
);
assert!(
info.is_exact,
"Commuting Hamiltonian with Exact strategy must report is_exact=true"
);
assert_eq!(info.steps, 1, "Exact evolution uses 1 effective step");
assert!(
info.trotter_mode.is_none(),
"Exact evolution has no Trotter mode"
);
assert!(
ansatz.validate().is_ok(),
"validate() must succeed for commuting Hamiltonian with Exact strategy"
);
}
#[test]
fn test_evolution_info_trotter_strategy_commuting_reports_exact() {
let mut h = Hamiltonian::new(2);
h.add_term("ZI".parse().unwrap(), 1.0.into()).unwrap();
h.add_term("ZZ".parse().unwrap(), 0.5.into()).unwrap();
for (mode, steps) in [
(TrotterMode::FirstOrder, 3),
(TrotterMode::SecondOrder, 2),
(TrotterMode::Randomized(7), 4),
] {
let ansatz = PauliEvolutionAnsatz::new(h.clone())
.unwrap()
.with_strategy(EvolutionStrategy::Trotter { mode, steps });
let info = ansatz.evolution_info();
assert!(
info.all_terms_commute,
"ZI and ZZ commute; all_terms_commute must be true"
);
assert!(
info.is_exact,
"Explicit Trotter decomposition of a commuting Hamiltonian is still mathematically exact"
);
assert_eq!(info.steps, steps);
assert_eq!(info.trotter_mode, Some(mode));
}
}
#[test]
fn test_identity_term_sets_correct_global_phase() {
let alpha = 0.7_f64;
let t_val = 0.5_f64;
let mut h = Hamiltonian::new(2);
h.add_term("II".parse().unwrap(), alpha.into()).unwrap();
let ansatz = PauliEvolutionAnsatz::new(h).unwrap();
let circuit = ansatz.build_circuit("id").unwrap();
assert_eq!(
circuit.operations().len(),
0,
"Identity Hamiltonian must produce zero gates"
);
let mut bindings = HashMap::new();
bindings.insert("id_t", t_val);
let bound = circuit.assign_parameters(&Some(bindings)).unwrap();
let expected_phase = -(alpha * t_val);
let actual_phase = bound
.global_phase()
.evaluate(&None)
.expect("global_phase must be concrete after binding");
assert_abs_diff_eq!(actual_phase, expected_phase, epsilon = 1e-12);
assert!(
(actual_phase - expected_phase).abs() < 1e-12,
"Global phase should be -α*t = {:.6}; got {:.6}",
expected_phase,
actual_phase
);
}
#[test]
fn test_identity_term_mixed_with_physical() {
let t_val = 0.4_f64;
let mut h_full = Hamiltonian::new(2);
h_full.add_term("ZZ".parse().unwrap(), 0.5.into()).unwrap();
h_full.add_term("II".parse().unwrap(), 0.3.into()).unwrap();
let ansatz = PauliEvolutionAnsatz::new(h_full).unwrap();
let circuit = ansatz.build_circuit("mix").unwrap();
let mut bindings = HashMap::new();
bindings.insert("mix_t", t_val);
let bound = circuit.assign_parameters(&Some(bindings.clone())).unwrap();
let expected_phase = -(0.3_f64 * t_val);
let actual_phase = bound
.global_phase()
.evaluate(&None)
.expect("global_phase must be concrete after binding");
assert!(
(actual_phase - expected_phase).abs() < 1e-12,
"Global phase from II term: expected {:.6}, got {:.6}",
expected_phase,
actual_phase
);
let mut h_zz = Hamiltonian::new(2);
h_zz.add_term("ZZ".parse().unwrap(), 0.5.into()).unwrap();
let u_circuit = circuit_matrix_at_t(&ansatz, "mix", "mix_t", t_val);
let h_zz_mat = hamiltonian_matrix(&h_zz);
let mut expected = matrix_exp_iht(&h_zz_mat, t_val);
let global_factor = Complex64::from_polar(1.0, expected_phase);
expected.mapv_inplace(|value| global_factor * value);
let dist = frob_dist(&u_circuit, &expected);
assert!(
dist < 1e-10,
"Circuit matrix should equal e^{{-i·0.3·t}}·e^{{-i·0.5·t·ZZ}}; \
Frobenius distance = {dist:.2e}"
);
}
#[test]
fn test_time_param_name_rejects_reserved_names_and_expressions() {
let mut h = Hamiltonian::new(1);
h.add_term("Z".parse().unwrap(), 1.0.into()).unwrap();
for invalid_name in ["e", "π", "a+b", "1.0"] {
let ansatz = PauliEvolutionAnsatz::new(h.clone())
.unwrap()
.with_time_param_name(invalid_name);
let err = ansatz.build_circuit("ignored").unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("Invalid time parameter name"),
"expected invalid time parameter error for '{invalid_name}', got: {msg}"
);
}
}