mod common;
use common::SEED;
use prism_q::circuits;
use prism_q::{
Circuit, Gate, Instruction, ParameterMap, PauliTerm, run_expectation_gradient,
run_expectation_values, simulate,
};
type Hamiltonian = Vec<(f64, Vec<PauliTerm>)>;
fn expval(circuit: &Circuit, hamiltonian: &Hamiltonian) -> f64 {
let observables: Vec<Vec<PauliTerm>> = hamiltonian.iter().map(|(_, p)| p.clone()).collect();
let per_term = run_expectation_values(circuit, &observables, SEED).unwrap();
hamiltonian
.iter()
.zip(per_term)
.map(|((c, _), v)| c * v)
.sum()
}
fn shift_slot(circuit: &Circuit, params: &ParameterMap, slot: usize, delta: f64) -> Circuit {
let mut out = circuit.clone();
for link in params.links().iter().filter(|l| l.param == slot) {
if let Instruction::Gate { gate, .. } = &mut out.instructions[link.instruction] {
*gate = shifted_gate(gate, delta);
}
}
out
}
fn shifted_gate(gate: &Gate, delta: f64) -> Gate {
match gate {
Gate::Rx(t) => Gate::Rx(t + delta),
Gate::Ry(t) => Gate::Ry(t + delta),
Gate::Rz(t) => Gate::Rz(t + delta),
Gate::Rzz(t) => Gate::Rzz(t + delta),
Gate::P(t) => Gate::P(t + delta),
other => panic!("gate {} is not differentiable", other.name()),
}
}
fn finite_diff(
circuit: &Circuit,
hamiltonian: &Hamiltonian,
params: &ParameterMap,
slot: usize,
) -> f64 {
let eps = 1e-5;
let plus = expval(&shift_slot(circuit, params, slot, eps), hamiltonian);
let minus = expval(&shift_slot(circuit, params, slot, -eps), hamiltonian);
(plus - minus) / (2.0 * eps)
}
fn param_shift(
circuit: &Circuit,
hamiltonian: &Hamiltonian,
params: &ParameterMap,
slot: usize,
) -> f64 {
let s = std::f64::consts::FRAC_PI_2;
let plus = expval(&shift_slot(circuit, params, slot, s), hamiltonian);
let minus = expval(&shift_slot(circuit, params, slot, -s), hamiltonian);
(plus - minus) / 2.0
}
#[test]
fn single_rx_matches_analytic() {
let theta = 0.6;
let mut c = Circuit::new(1, 0);
c.add_gate(Gate::Rx(theta), &[0]);
let mut params = ParameterMap::new();
params.push(0, 0);
let obs = vec![(1.0, vec![PauliTerm::z(0)])];
let g = run_expectation_gradient(&c, &obs, ¶ms, SEED).unwrap();
assert!((g.value - theta.cos()).abs() < 1e-12);
assert!((g.gradient[0] - (-theta.sin())).abs() < 1e-9);
assert!((g.gradient[0] - finite_diff(&c, &obs, ¶ms, 0)).abs() < 1e-6);
assert!((g.gradient[0] - param_shift(&c, &obs, ¶ms, 0)).abs() < 1e-9);
}
#[test]
fn hea_multiterm_hamiltonian_all_params() {
let c = circuits::hardware_efficient_ansatz(4, 2, SEED);
let params = ParameterMap::all_rotations(&c);
let obs: Hamiltonian = vec![
(1.0, vec![PauliTerm::z(0), PauliTerm::z(1)]),
(0.7, vec![PauliTerm::x(0)]),
(0.5, vec![PauliTerm::y(2)]),
(-0.3, vec![PauliTerm::z(3)]),
];
let g = simulate(&c)
.seed(SEED)
.expectation_gradient(&obs, ¶ms)
.unwrap();
assert!((g.value - expval(&c, &obs)).abs() < 1e-10);
assert_eq!(g.gradient.len(), params.num_params());
for slot in 0..params.num_params() {
let fd = finite_diff(&c, &obs, ¶ms, slot);
let ps = param_shift(&c, &obs, ¶ms, slot);
assert!(
(g.gradient[slot] - fd).abs() < 1e-6,
"slot {slot}: adjoint {} vs finite-diff {fd}",
g.gradient[slot]
);
assert!(
(g.gradient[slot] - ps).abs() < 1e-9,
"slot {slot}: adjoint {} vs param-shift {ps}",
g.gradient[slot]
);
}
}
#[test]
fn qaoa_layer_rzz_and_rx() {
let c = circuits::qaoa_circuit(6, 1, SEED);
let params = ParameterMap::all_rotations(&c);
let obs: Hamiltonian = vec![
(1.0, vec![PauliTerm::z(0), PauliTerm::z(1)]),
(1.0, vec![PauliTerm::z(2), PauliTerm::z(3)]),
(0.4, vec![PauliTerm::x(5)]),
];
let g = run_expectation_gradient(&c, &obs, ¶ms, SEED).unwrap();
assert!((g.value - expval(&c, &obs)).abs() < 1e-10);
for slot in 0..params.num_params() {
let fd = finite_diff(&c, &obs, ¶ms, slot);
assert!(
(g.gradient[slot] - fd).abs() < 1e-6,
"slot {slot}: adjoint {} vs finite-diff {fd}",
g.gradient[slot]
);
}
}
#[test]
fn value_matches_forward_expectation() {
let c = circuits::hardware_efficient_ansatz(5, 3, SEED);
let params = ParameterMap::all_rotations(&c);
let obs: Hamiltonian = vec![
(1.2, vec![PauliTerm::z(0)]),
(-0.5, vec![PauliTerm::x(1), PauliTerm::x(2)]),
];
let g = run_expectation_gradient(&c, &obs, ¶ms, SEED).unwrap();
assert!((g.value - expval(&c, &obs)).abs() < 1e-10);
}
#[test]
fn builder_records_and_differentiates() {
use prism_q::CircuitBuilder;
let theta = 0.9;
let mut b = CircuitBuilder::new(2);
b.h(0)
.rz(theta, 0)
.trainable(0)
.cx(0, 1)
.ry(0.4, 1)
.trainable(1);
let (circuit, params) = b.build_parametric();
let obs: Hamiltonian = vec![(1.0, vec![PauliTerm::z(0), PauliTerm::z(1)])];
let g = run_expectation_gradient(&circuit, &obs, ¶ms, SEED).unwrap();
assert_eq!(g.gradient.len(), 2);
for slot in 0..2 {
let fd = finite_diff(&circuit, &obs, ¶ms, slot);
assert!((g.gradient[slot] - fd).abs() < 1e-6);
}
}
#[test]
fn builder_shared_slot_accumulates() {
use prism_q::CircuitBuilder;
let theta = 0.4;
let mut b = CircuitBuilder::new(2);
b.rx(theta, 0).trainable(0).rx(theta, 1).trainable(0);
let (circuit, params) = b.build_parametric();
assert_eq!(params.num_params(), 1);
let obs: Hamiltonian = vec![(1.0, vec![PauliTerm::z(0)]), (1.0, vec![PauliTerm::z(1)])];
let g = run_expectation_gradient(&circuit, &obs, ¶ms, SEED).unwrap();
assert!((g.gradient[0] - (-2.0 * theta.sin())).abs() < 1e-9);
assert!((g.gradient[0] - finite_diff(&circuit, &obs, ¶ms, 0)).abs() < 1e-6);
}
#[test]
#[should_panic(expected = "differentiable gate")]
fn trainable_on_nondifferentiable_gate_panics() {
use prism_q::CircuitBuilder;
CircuitBuilder::new(1).h(0).trainable(0);
}
#[test]
fn out_of_cone_gate_has_zero_gradient() {
let mut c = Circuit::new(3, 0);
c.add_gate(Gate::Rx(0.5), &[0]);
c.add_gate(Gate::Rx(0.7), &[2]);
let mut params = ParameterMap::new();
params.push(0, 0);
params.push(1, 1);
let obs = vec![(1.0, vec![PauliTerm::z(0)])];
let g = run_expectation_gradient(&c, &obs, ¶ms, SEED).unwrap();
assert!((g.gradient[0] - (-0.5f64.sin())).abs() < 1e-9);
assert_eq!(g.gradient[1], 0.0);
assert!((g.gradient[1] - finite_diff(&c, &obs, ¶ms, 1)).abs() < 1e-6);
}
#[test]
fn nontrainable_prefix_is_skipped_correctly() {
let mut c = Circuit::new(3, 0);
c.add_gate(Gate::H, &[0]);
c.add_gate(Gate::Cx, &[0, 1]);
c.add_gate(Gate::Cx, &[1, 2]);
c.add_gate(Gate::Ry(0.6), &[0]);
c.add_gate(Gate::Rz(0.4), &[1]);
c.add_gate(Gate::Rx(0.9), &[2]);
let mut params = ParameterMap::new();
params.push(3, 0);
params.push(4, 1);
params.push(5, 2);
let obs: Hamiltonian = vec![
(1.0, vec![PauliTerm::z(0), PauliTerm::z(2)]),
(0.5, vec![PauliTerm::x(1)]),
];
let g = run_expectation_gradient(&c, &obs, ¶ms, SEED).unwrap();
for slot in 0..3 {
let fd = finite_diff(&c, &obs, ¶ms, slot);
assert!(
(g.gradient[slot] - fd).abs() < 1e-6,
"slot {slot}: adjoint {} vs finite-diff {fd}",
g.gradient[slot]
);
}
}
#[test]
fn out_of_range_link_is_rejected() {
let mut c = Circuit::new(1, 0);
c.add_gate(Gate::Rx(0.3), &[0]);
let mut params = ParameterMap::new();
params.push(5, 0);
let obs = vec![(1.0, vec![PauliTerm::z(0)])];
assert!(run_expectation_gradient(&c, &obs, ¶ms, SEED).is_err());
}