1use std::borrow::Cow;
18
19use num_complex::Complex64;
20
21use crate::backend::statevector::StatevectorBackend;
22use crate::backend::{Backend, max_statevector_qubits, reserve_dense_output};
23use crate::circuit::parameter::{Parameters, angle_mut};
24use crate::circuit::{Circuit, Instruction, SmallVec, smallvec};
25use crate::error::{PrismError, Result};
26use crate::gates::{
27 BatchRzzData, DiagEntry, DiagonalBatchData, Gate, GeneratorKind, MultiFusedData,
28 is_diagonal_2x2, pauli_rot_masks,
29};
30
31use super::noise::NoiseModel;
32use super::unified_pauli::PauliTerm;
33use super::{BackendKind, i_pow, pauli_masks, pauli_sandwiches_from_masks};
34
35#[derive(Debug, Clone, PartialEq)]
37pub struct ExpectationGradient {
38 pub value: f64,
40 pub gradient: Vec<f64>,
42}
43
44pub fn run_expectation_gradient(
70 circuit: &Circuit,
71 hamiltonian: &[(f64, Vec<PauliTerm>)],
72 params: &Parameters,
73 seed: u64,
74) -> Result<ExpectationGradient> {
75 if super::has_nonunitary_or_classical_ops(circuit) {
76 return Err(PrismError::IncompatibleBackend {
77 backend: "Statevector".into(),
78 reason: "adjoint gradients require a unitary circuit without measurements, resets, or conditionals".into(),
79 });
80 }
81
82 if circuit.instructions.iter().any(|inst| {
83 matches!(
84 inst,
85 Instruction::Gate {
86 gate: Gate::QftBlock { .. },
87 ..
88 }
89 )
90 }) {
91 return Err(PrismError::IncompatibleBackend {
92 backend: "Statevector".into(),
93 reason: "adjoint gradients do not support QftBlock; expand it to primitive gates first"
94 .into(),
95 });
96 }
97
98 params.validate(circuit)?;
99
100 if circuit.num_qubits > max_statevector_qubits() {
101 return Err(PrismError::IncompatibleBackend {
102 backend: "Statevector".into(),
103 reason: format!(
104 "adjoint gradients for {} qubits exceed the statevector cap ({} qubits); the gradient path holds two statevectors",
105 circuit.num_qubits,
106 max_statevector_qubits()
107 ),
108 });
109 }
110
111 let mut masked = Vec::with_capacity(hamiltonian.len());
113 for (coeff, terms) in hamiltonian {
114 let (xmask, zmask, num_y) = pauli_masks(terms, circuit.num_qubits)?;
115 masked.push((*coeff, xmask, zmask, num_y));
116 }
117
118 let in_cone = observable_light_cone(circuit, hamiltonian);
122 let kept: Vec<usize> = (0..circuit.instructions.len())
123 .filter(|&i| in_cone[i])
124 .collect();
125
126 let mut phi = StatevectorBackend::new(seed);
132 phi.init(circuit.num_qubits, circuit.num_classical_bits)?;
133 let forward = kept_subcircuit(circuit, &kept);
134 let expanded = super::expand_for_backend(&phi, &forward);
135 let fused = super::fuse_for_backend(&phi, &expanded);
136 phi.apply_instructions(&fused.instructions)?;
137
138 let (value, lambda_state) = build_lambda_and_value(phi.state_vector(), &masked)?;
139
140 let mut gradient = vec![0.0; params.num_slots()];
141
142 let mut links: Vec<_> = params
147 .links()
148 .iter()
149 .filter(|l| in_cone[l.instruction])
150 .copied()
151 .collect();
152 links.sort_unstable_by_key(|l| std::cmp::Reverse(l.instruction));
153
154 let Some(earliest) = links.last().map(|l| l.instruction) else {
159 return Ok(ExpectationGradient { value, gradient });
160 };
161
162 let mut lambda = StatevectorBackend::new(seed);
163 lambda.init_from_state(lambda_state, circuit.num_classical_bits)?;
164
165 let sweep = &kept[kept.partition_point(|&i| i < earliest)..];
170 let mut cursor = 0;
171 let mut end = sweep.len();
172 let mut run: Vec<RunGate> = Vec::new();
173 let mut masks: Vec<(usize, usize, u32)> = Vec::new();
174
175 while end > 0 {
176 let i = sweep[end - 1];
177 if cursor >= links.len() || links[cursor].instruction != i {
178 let inverse = inverse_instruction(&circuit.instructions[i]);
179 phi.apply(&inverse)?;
180 lambda.apply(&inverse)?;
181 end -= 1;
182 continue;
183 }
184
185 run.clear();
186 let mut start = end;
187 let mut lookahead = cursor;
188 while start > 0 {
189 let index = sweep[start - 1];
190 if lookahead >= links.len() || links[lookahead].instruction != index {
191 break;
192 }
193 let Instruction::Gate { gate, targets } = &circuit.instructions[index] else {
194 unreachable!("the light cone keeps gate instructions only")
195 };
196 let kind = gate
197 .pauli_generator()
198 .expect("trainable instruction validated as differentiable");
199 let candidate = RunGate::new(index, kind, targets);
200 if run.iter().any(|member| !member.commutes_with(&candidate)) {
201 break;
202 }
203 while lookahead < links.len() && links[lookahead].instruction == index {
204 lookahead += 1;
205 }
206 run.push(candidate);
207 start -= 1;
208 }
209
210 masks.clear();
211 masks.extend(run.iter().map(|g| (g.xmask, g.zmask, g.num_y)));
212 let values = pauli_sandwiches_from_masks(lambda.state_vector(), phi.state_vector(), &masks);
213
214 for (g, value) in run.iter().zip(&values) {
215 while cursor < links.len() && links[cursor].instruction == g.index {
216 gradient[links[cursor].slot] += value.im;
217 cursor += 1;
218 }
219 }
220
221 let applied = run.len() - usize::from(sweep[start] == earliest);
224 for inverse in run_inverse_instructions(circuit, &run[..applied]) {
225 phi.apply(&inverse)?;
226 lambda.apply(&inverse)?;
227 }
228 end = start;
229 }
230
231 Ok(ExpectationGradient { value, gradient })
232}
233
234struct RunGate {
237 index: usize,
238 xmask: usize,
239 zmask: usize,
240 num_y: u32,
241}
242
243impl RunGate {
244 fn new(index: usize, kind: GeneratorKind<'_>, targets: &[usize]) -> Self {
245 let (xmask, zmask, num_y) = match kind {
246 GeneratorKind::RotX => (1usize << targets[0], 0, 0),
247 GeneratorKind::RotY => {
248 let bit = 1usize << targets[0];
249 (bit, bit, 1)
250 }
251 GeneratorKind::RotZ => (0, 1usize << targets[0], 0),
252 GeneratorKind::RotZz => (0, (1usize << targets[0]) | (1usize << targets[1]), 0),
253 GeneratorKind::Phase => (0, 1usize << targets[0], 0),
257 GeneratorKind::RotPauli(axes) => pauli_rot_masks(targets, axes),
258 };
259 Self {
260 index,
261 xmask,
262 zmask,
263 num_y,
264 }
265 }
266
267 fn commutes_with(&self, other: &RunGate) -> bool {
274 let anticommuting =
275 (self.xmask & other.zmask).count_ones() + (self.zmask & other.xmask).count_ones();
276 anticommuting.is_multiple_of(2)
277 }
278}
279
280fn inverse_instruction(instruction: &Instruction) -> Instruction {
281 let Instruction::Gate { gate, targets } = instruction else {
282 unreachable!("the light cone keeps gate instructions only")
283 };
284 Instruction::Gate {
285 gate: gate.inverse(),
286 targets: targets.clone(),
287 }
288}
289
290fn run_inverse_instructions(circuit: &Circuit, run: &[RunGate]) -> Vec<Instruction> {
296 let gates: Vec<(&Gate, &[usize])> = run
297 .iter()
298 .map(|g| {
299 let Instruction::Gate { gate, targets } = &circuit.instructions[g.index] else {
300 unreachable!("the light cone keeps gate instructions only")
301 };
302 (gate, targets.as_slice())
303 })
304 .collect();
305
306 if gates.len() > 1 {
307 if let Some(fused) = multi_fused_inverse(&gates) {
308 return vec![fused];
309 }
310 if let Some(batched) = batch_rzz_inverse(&gates) {
311 return batched;
312 }
313 if let Some(diagonal) = diagonal_batch_inverse(&gates) {
314 return vec![diagonal];
315 }
316 }
317
318 gates
319 .iter()
320 .map(|&(gate, targets)| Instruction::Gate {
321 gate: gate.inverse(),
322 targets: targets.iter().copied().collect(),
323 })
324 .collect()
325}
326
327fn multi_fused_inverse(gates: &[(&Gate, &[usize])]) -> Option<Instruction> {
328 let mut fused: Vec<(usize, [[Complex64; 2]; 2])> = Vec::with_capacity(gates.len());
329 for &(gate, targets) in gates {
330 if !matches!(gate, Gate::Rx(_) | Gate::Ry(_) | Gate::Rz(_) | Gate::P(_))
331 || fused.iter().any(|&(q, _)| q == targets[0])
332 {
333 return None;
334 }
335 fused.push((targets[0], gate.inverse().matrix_2x2()));
336 }
337 let all_diagonal = fused.iter().all(|(_, mat)| is_diagonal_2x2(mat));
338 let targets: SmallVec<[usize; 4]> = fused.iter().map(|&(q, _)| q).collect();
339 Some(Instruction::Gate {
340 gate: Gate::MultiFused(Box::new(MultiFusedData {
341 gates: fused,
342 all_diagonal,
343 })),
344 targets,
345 })
346}
347
348fn batch_rzz_inverse(gates: &[(&Gate, &[usize])]) -> Option<Vec<Instruction>> {
349 let mut edges: Vec<(usize, usize, f64)> = Vec::with_capacity(gates.len());
350 for &(gate, targets) in gates {
351 let Gate::Rzz(theta) = gate else {
352 return None;
353 };
354 edges.push((targets[0], targets[1], -theta));
355 }
356 Some(
357 edges
358 .chunks(BatchRzzData::MAX_EDGES)
359 .map(|chunk| match chunk {
360 [(q0, q1, theta)] => Instruction::Gate {
361 gate: Gate::Rzz(*theta),
362 targets: smallvec![*q0, *q1],
363 },
364 _ => {
365 let mut targets: SmallVec<[usize; 4]> = SmallVec::new();
366 for &(q0, q1, _) in chunk {
367 for q in [q0, q1] {
368 if !targets.contains(&q) {
369 targets.push(q);
370 }
371 }
372 }
373 Instruction::Gate {
374 gate: Gate::BatchRzz(Box::new(BatchRzzData {
375 edges: chunk.to_vec(),
376 })),
377 targets,
378 }
379 }
380 })
381 .collect(),
382 )
383}
384
385fn diagonal_batch_inverse(gates: &[(&Gate, &[usize])]) -> Option<Instruction> {
389 let mut entries: Vec<DiagEntry> = Vec::with_capacity(gates.len());
390 let mut targets: SmallVec<[usize; 4]> = SmallVec::new();
391 for &(gate, gate_targets) in gates {
392 if !matches!(gate, Gate::Rz(_) | Gate::P(_) | Gate::Rzz(_)) {
393 return None;
394 }
395 entries.extend(gate.inverse().diag_entries(gate_targets));
396 for &q in gate_targets {
397 if !targets.contains(&q) {
398 targets.push(q);
399 }
400 }
401 }
402 targets.sort_unstable();
403 Some(Instruction::Gate {
404 gate: Gate::DiagonalBatch(Box::new(DiagonalBatchData { entries })),
405 targets,
406 })
407}
408
409fn kept_subcircuit<'a>(circuit: &'a Circuit, kept: &[usize]) -> Cow<'a, Circuit> {
413 if kept.len() == circuit.instructions.len() {
414 return Cow::Borrowed(circuit);
415 }
416 let instructions = kept
417 .iter()
418 .map(|&i| circuit.instructions[i].clone())
419 .collect();
420 Cow::Owned(circuit.with_instructions(instructions))
421}
422
423fn observable_light_cone(circuit: &Circuit, hamiltonian: &[(f64, Vec<PauliTerm>)]) -> Vec<bool> {
427 let union: Vec<PauliTerm> = hamiltonian
428 .iter()
429 .flat_map(|(_, terms)| terms.iter().copied())
430 .collect();
431 super::unified_pauli::inverse_light_cone(circuit, &union)
432}
433
434struct TermGroup {
438 xmask: usize,
439 terms: Vec<(usize, Complex64)>,
440}
441
442fn group_terms_by_xmask(masked: &[(f64, usize, usize, u32)]) -> Vec<TermGroup> {
445 let mut flat: Vec<(usize, usize, Complex64)> = masked
446 .iter()
447 .map(|&(coeff, xmask, zmask, num_y)| {
448 (xmask, zmask, Complex64::new(coeff, 0.0) * i_pow(num_y))
449 })
450 .collect();
451 flat.sort_by_key(|&(xmask, _, _)| xmask);
452
453 let mut groups: Vec<TermGroup> = Vec::with_capacity(flat.len());
454 for (xmask, zmask, factor) in flat {
455 match groups.last_mut() {
456 Some(group) if group.xmask == xmask => group.terms.push((zmask, factor)),
457 _ => groups.push(TermGroup {
458 xmask,
459 terms: vec![(zmask, factor)],
460 }),
461 }
462 }
463 groups
464}
465
466#[inline(always)]
474fn gather_lambda_chunk(
475 groups: &[TermGroup],
476 phi: &[Complex64],
477 base: usize,
478 out: &mut [Complex64],
479) -> f64 {
480 let mut value = 0.0;
481 for (offset, slot) in out.iter_mut().enumerate() {
482 let i = base + offset;
483 let mut acc = Complex64::new(0.0, 0.0);
484 for group in groups {
485 let j = i ^ group.xmask;
486 let mut weight = Complex64::new(0.0, 0.0);
487 for &(zmask, factor) in &group.terms {
488 let sign = if (j & zmask).count_ones() & 1 == 1 {
489 -1.0
490 } else {
491 1.0
492 };
493 weight += factor * sign;
494 }
495 let amp = unsafe { *phi.get_unchecked(j) };
500 acc += weight * amp;
501 }
502 let p = unsafe { *phi.get_unchecked(i) };
504 value += p.re * acc.re + p.im * acc.im;
505 *slot = acc;
506 }
507 value
508}
509
510fn build_lambda_and_value(
517 phi: &[Complex64],
518 masked: &[(f64, usize, usize, u32)],
519) -> Result<(f64, Vec<Complex64>)> {
520 let dim = phi.len();
521 let mut lambda: Vec<Complex64> = Vec::new();
522 reserve_dense_output(
523 &mut lambda,
524 dim,
525 "Statevector",
526 "adjoint gradient lambda state",
527 )?;
528 lambda.resize(dim, Complex64::new(0.0, 0.0));
529
530 let groups = group_terms_by_xmask(masked);
531 assert!(
532 dim.is_power_of_two() && groups.iter().all(|g| g.xmask < dim),
533 "observable masks must index the state dimension"
534 );
535
536 #[cfg(feature = "parallel")]
537 if dim >= (1 << crate::backend::PARALLEL_THRESHOLD_QUBITS) {
538 use crate::backend::MIN_PAR_ELEMS;
539 use rayon::prelude::*;
540
541 let value = lambda
542 .par_chunks_mut(MIN_PAR_ELEMS)
543 .enumerate()
544 .map(|(chunk, out)| gather_lambda_chunk(&groups, phi, chunk * MIN_PAR_ELEMS, out))
545 .sum();
546 return Ok((value, lambda));
547 }
548
549 let value = gather_lambda_chunk(&groups, phi, 0, &mut lambda);
550 Ok((value, lambda))
551}
552
553pub fn run_expectation_gradient_shift(
579 circuit: &Circuit,
580 hamiltonian: &[(f64, Vec<PauliTerm>)],
581 params: &Parameters,
582 seed: u64,
583) -> Result<ExpectationGradient> {
584 shift_gradient(
585 &BackendKind::Auto,
586 circuit,
587 hamiltonian,
588 params,
589 None,
590 None,
591 seed,
592 )
593}
594
595pub(crate) fn shift_gradient(
615 kind: &BackendKind,
616 circuit: &Circuit,
617 hamiltonian: &[(f64, Vec<PauliTerm>)],
618 params: &Parameters,
619 noise: Option<&NoiseModel>,
620 initial_state: Option<&[Complex64]>,
621 seed: u64,
622) -> Result<ExpectationGradient> {
623 params.validate(circuit)?;
624 if initial_state.is_some() || noise.is_some() {
625 super::require_unitary_circuit(kind, circuit, "expectation values require")?;
626 }
627
628 let observables: Vec<Vec<PauliTerm>> =
629 hamiltonian.iter().map(|(_, terms)| terms.clone()).collect();
630 let evaluate = |c: &Circuit| -> Result<f64> {
631 if let BackendKind::PauliPath { epsilon, max_terms } = kind {
632 let per_term =
633 super::pauli_path_expectations(c, noise, &observables, *epsilon, *max_terms)?
634 .into_values();
635 return Ok(hamiltonian
636 .iter()
637 .zip(per_term)
638 .map(|((coeff, _), v)| coeff * v)
639 .sum());
640 }
641 let per_term = match (noise, initial_state) {
642 (Some(noise), _) => super::noise::dm_expectation_values(
643 kind,
644 c,
645 &observables,
646 Some(noise),
647 initial_state,
648 seed,
649 )?,
650 (None, Some(state)) => {
651 super::expectation_values_from_initial_state(kind, c, state, &observables, seed)?
652 .into_values()
653 }
654 (None, None) => {
655 super::run_expectation_values_with(kind.clone(), c, &observables, seed)?
656 }
657 };
658 Ok(hamiltonian
659 .iter()
660 .zip(per_term)
661 .map(|((coeff, _), v)| coeff * v)
662 .sum())
663 };
664
665 let value = evaluate(circuit)?;
666 let mut gradient = vec![0.0; params.num_slots()];
667 if params.is_empty() {
668 return Ok(ExpectationGradient { value, gradient });
669 }
670
671 let shift = std::f64::consts::FRAC_PI_2;
672 let mut shifted = circuit.clone();
673 for link in params.links() {
674 let base = *angle_mut(&mut shifted.instructions[link.instruction]);
675 *angle_mut(&mut shifted.instructions[link.instruction]) = base + shift;
676 let plus = evaluate(&shifted)?;
677 *angle_mut(&mut shifted.instructions[link.instruction]) = base - shift;
678 let minus = evaluate(&shifted)?;
679 *angle_mut(&mut shifted.instructions[link.instruction]) = base;
680 gradient[link.slot] += 0.5 * (plus - minus);
681 }
682
683 Ok(ExpectationGradient { value, gradient })
684}
685
686#[cfg(test)]
687mod tests {
688 use super::*;
689 use crate::sim::unified_pauli::PauliTerm;
690
691 fn z_obs(qubit: usize) -> Vec<(f64, Vec<PauliTerm>)> {
692 vec![(1.0, vec![PauliTerm::z(qubit)])]
693 }
694
695 #[test]
696 fn single_rx_gradient_matches_analytic() {
697 let theta = 0.7;
699 let mut c = Circuit::new(1, 0);
700 c.add_gate(Gate::Rx(theta), &[0]);
701 let mut params = Parameters::new(1);
702 params.link(0, 0);
703
704 let g = run_expectation_gradient(&c, &z_obs(0), ¶ms, 42).unwrap();
705 assert!((g.value - theta.cos()).abs() < 1e-12);
706 assert!((g.gradient[0] - (-theta.sin())).abs() < 1e-9);
707 }
708
709 #[test]
710 fn ry_generator_carries_num_y() {
711 let theta = 1.3;
713 let mut c = Circuit::new(1, 0);
714 c.add_gate(Gate::Ry(theta), &[0]);
715 let mut params = Parameters::new(1);
716 params.link(0, 0);
717
718 let g = run_expectation_gradient(&c, &z_obs(0), ¶ms, 42).unwrap();
719 assert!((g.gradient[0] - (-theta.sin())).abs() < 1e-9);
720 }
721
722 #[test]
723 fn phase_projector_gradient() {
724 let theta = 0.9;
726 let mut c = Circuit::new(1, 0);
727 c.add_gate(Gate::H, &[0]);
728 c.add_gate(Gate::P(theta), &[0]);
729 let mut params = Parameters::new(1);
730 params.link(1, 0);
731
732 let obs = vec![(1.0, vec![PauliTerm::x(0)])];
733 let g = run_expectation_gradient(&c, &obs, ¶ms, 42).unwrap();
734 assert!((g.value - theta.cos()).abs() < 1e-12);
735 assert!((g.gradient[0] - (-theta.sin())).abs() < 1e-9);
736 }
737
738 #[test]
739 fn shared_parameter_accumulates() {
740 let theta = 0.4;
743 let mut c = Circuit::new(2, 0);
744 c.add_gate(Gate::Rx(theta), &[0]);
745 c.add_gate(Gate::Rx(theta), &[1]);
746 let mut params = Parameters::new(1);
747 params.link(0, 0);
748 params.link(1, 0);
749
750 let obs = vec![(1.0, vec![PauliTerm::z(0)]), (1.0, vec![PauliTerm::z(1)])];
751 let g = run_expectation_gradient(&c, &obs, ¶ms, 42).unwrap();
752 assert_eq!(g.gradient.len(), 1);
753 assert!((g.gradient[0] - (-2.0 * theta.sin())).abs() < 1e-9);
754 }
755
756 #[test]
757 fn empty_params_returns_value_only() {
758 let mut c = Circuit::new(1, 0);
759 c.add_gate(Gate::Rx(0.5), &[0]);
760 let g = run_expectation_gradient(&c, &z_obs(0), &Parameters::new(0), 42).unwrap();
761 assert!(g.gradient.is_empty());
762 assert!((g.value - 0.5f64.cos()).abs() < 1e-12);
763 }
764
765 #[test]
766 fn nondifferentiable_trainable_gate_is_rejected() {
767 let mut c = Circuit::new(1, 0);
768 c.add_gate(Gate::H, &[0]);
769 let mut params = Parameters::new(1);
770 params.link(0, 0);
771 assert!(run_expectation_gradient(&c, &z_obs(0), ¶ms, 42).is_err());
772 }
773
774 #[test]
775 fn nonunitary_circuit_is_rejected() {
776 let mut c = Circuit::new(1, 1);
777 c.add_gate(Gate::Rx(0.3), &[0]);
778 c.add_measure(0, 0);
779 assert!(run_expectation_gradient(&c, &z_obs(0), &Parameters::new(0), 42).is_err());
780 }
781
782 #[test]
783 fn single_term_hamiltonian_matches_analytic() {
784 let theta = 0.6;
786 let mut c = Circuit::new(1, 0);
787 c.add_gate(Gate::Ry(theta), &[0]);
788 let params = Parameters::all_rotations(&c);
789
790 let obs = vec![(1.0, vec![PauliTerm::x(0)])];
791 let g = run_expectation_gradient(&c, &obs, ¶ms, 42).unwrap();
792 assert!((g.value - theta.sin()).abs() < 1e-12);
793 assert!((g.gradient[0] - theta.cos()).abs() < 1e-12);
794 }
795
796 #[test]
797 fn diagonal_hamiltonian_matches_analytic() {
798 let (a, b) = (0.4, 1.1);
801 let mut c = Circuit::new(2, 0);
802 c.add_gate(Gate::Ry(a), &[0]);
803 c.add_gate(Gate::Ry(b), &[1]);
804 let params = Parameters::all_rotations(&c);
805
806 let obs = vec![
807 (1.0, vec![PauliTerm::z(0)]),
808 (0.5, vec![PauliTerm::z(1)]),
809 (0.25, vec![PauliTerm::z(0), PauliTerm::z(1)]),
810 ];
811 let g = run_expectation_gradient(&c, &obs, ¶ms, 42).unwrap();
812 let value = a.cos() + 0.5 * b.cos() + 0.25 * a.cos() * b.cos();
813 assert!((g.value - value).abs() < 1e-12);
814 assert!((g.gradient[0] - (-a.sin() - 0.25 * a.sin() * b.cos())).abs() < 1e-12);
815 assert!((g.gradient[1] - (-0.5 * b.sin() - 0.25 * a.cos() * b.sin())).abs() < 1e-12);
816 }
817
818 #[test]
819 fn grouped_x_mask_terms_match_parameter_shift() {
820 let mut c = Circuit::new(3, 0);
823 c.add_gate(Gate::Ry(0.4), &[0]);
824 c.add_gate(Gate::Cx, &[0, 1]);
825 c.add_gate(Gate::Rx(0.9), &[1]);
826 c.add_gate(Gate::Cx, &[1, 2]);
827 c.add_gate(Gate::Rz(0.3), &[2]);
828 let params = Parameters::all_rotations(&c);
829
830 let obs = vec![
831 (1.0, vec![PauliTerm::x(0)]),
832 (-0.5, vec![PauliTerm::y(0)]),
833 (0.75, vec![PauliTerm::x(0), PauliTerm::z(1)]),
834 (0.25, vec![PauliTerm::y(0), PauliTerm::z(2)]),
835 ];
836 let adjoint = run_expectation_gradient(&c, &obs, ¶ms, 42).unwrap();
837 let shift = run_expectation_gradient_shift(&c, &obs, ¶ms, 42).unwrap();
838 assert!((adjoint.value - shift.value).abs() < 1e-12);
839 for (slot, (&got, &want)) in adjoint.gradient.iter().zip(&shift.gradient).enumerate() {
840 assert!((got - want).abs() < 1e-12, "slot {slot}: {got} vs {want}");
841 }
842 }
843
844 #[test]
845 fn parallel_threshold_width_matches_parameter_shift() {
846 let mut c = Circuit::new(14, 0);
848 c.add_gate(Gate::Ry(0.3), &[0]);
849 c.add_gate(Gate::Cx, &[0, 7]);
850 c.add_gate(Gate::Rx(0.8), &[7]);
851 c.add_gate(Gate::Cx, &[7, 13]);
852 c.add_gate(Gate::Rz(0.5), &[13]);
853 let params = Parameters::all_rotations(&c);
854
855 let obs = vec![
856 (1.0, vec![PauliTerm::z(0)]),
857 (0.5, vec![PauliTerm::x(7)]),
858 (-0.25, vec![PauliTerm::y(7), PauliTerm::z(13)]),
859 (0.75, vec![PauliTerm::x(7), PauliTerm::z(0)]),
860 ];
861 let adjoint = run_expectation_gradient(&c, &obs, ¶ms, 42).unwrap();
862 let shift = run_expectation_gradient_shift(&c, &obs, ¶ms, 42).unwrap();
863 assert!((adjoint.value - shift.value).abs() < 1e-12);
864 for (slot, (&got, &want)) in adjoint.gradient.iter().zip(&shift.gradient).enumerate() {
865 assert!((got - want).abs() < 1e-12, "slot {slot}: {got} vs {want}");
866 }
867 }
868
869 #[test]
870 fn the_gather_matches_a_term_by_term_reference() {
871 let dim = 1usize << 6;
875 let phi: Vec<Complex64> = (0..dim)
876 .map(|i| Complex64::new((i as f64 * 0.37).sin(), (i as f64 * 0.11).cos()))
877 .collect();
878 let masked = vec![
879 (1.0, 0usize, 0b101usize, 0u32),
880 (-0.5, 0b010, 0b100, 1),
881 (0.75, 0b010, 0b001, 0),
882 (0.25, 0b110, 0b011, 2),
883 ];
884
885 let mut want = vec![Complex64::new(0.0, 0.0); dim];
886 for &(coeff, xmask, zmask, num_y) in &masked {
887 let factor = Complex64::new(coeff, 0.0) * i_pow(num_y);
888 for (j, &) in phi.iter().enumerate() {
889 let sign = if (j & zmask).count_ones() & 1 == 1 {
890 -1.0
891 } else {
892 1.0
893 };
894 want[j ^ xmask] += factor * sign * amp;
895 }
896 }
897 let want_value: f64 = phi.iter().zip(&want).map(|(p, l)| (p.conj() * l).re).sum();
898
899 let groups = group_terms_by_xmask(&masked);
900 let mut got = vec![Complex64::new(0.0, 0.0); dim];
901 let (lo, hi) = got.split_at_mut(dim / 2);
902 let got_value = gather_lambda_chunk(&groups, &phi, 0, lo)
903 + gather_lambda_chunk(&groups, &phi, dim / 2, hi);
904
905 assert!((want_value - got_value).abs() < 1e-12);
906 for (i, (w, g)) in want.iter().zip(&got).enumerate() {
907 assert!((w - g).norm() < 1e-12, "slot {i}: {w} vs {g}");
908 }
909 }
910
911 #[test]
912 fn pauli_rot_generators_commute_on_an_even_anticommuting_count() {
913 use crate::sim::unified_pauli::PauliAxis;
914 let gate = |index: usize, axes: &[PauliAxis], targets: &[usize]| {
915 RunGate::new(index, GeneratorKind::RotPauli(axes), targets)
916 };
917 let x0y1 = gate(0, &[PauliAxis::X, PauliAxis::Y], &[0, 1]);
918 let y0x1 = gate(1, &[PauliAxis::Y, PauliAxis::X], &[0, 1]);
919 let wide = gate(
920 2,
921 &[PauliAxis::Y, PauliAxis::X, PauliAxis::X, PauliAxis::X],
922 &[0, 1, 2, 3],
923 );
924 let x1y2 = gate(3, &[PauliAxis::X, PauliAxis::Y], &[1, 2]);
925
926 assert!(x0y1.commutes_with(&y0x1));
927 assert!(x0y1.commutes_with(&x0y1));
928 assert!(!wide.commutes_with(&x1y2));
929 assert!(wide.commutes_with(&y0x1));
930 }
931
932 #[test]
933 fn a_run_collapses_to_one_batch_gate_only_when_its_type_has_one() {
934 let mut c = Circuit::new(4, 0);
935 c.add_gate(Gate::Rz(0.3), &[0]);
936 c.add_gate(Gate::Rz(0.5), &[1]);
937 c.add_gate(Gate::Rz(0.7), &[0]);
938 c.add_gate(Gate::Rzz(0.9), &[0, 1]);
939 c.add_gate(Gate::Rzz(1.1), &[2, 3]);
940 c.add_gate(Gate::Rx(1.3), &[0]);
941
942 let run = |indices: &[usize]| -> Vec<RunGate> {
943 indices
944 .iter()
945 .map(|&i| {
946 let Instruction::Gate { gate, targets } = &c.instructions[i] else {
947 unreachable!()
948 };
949 RunGate::new(i, gate.pauli_generator().unwrap(), targets)
950 })
951 .collect()
952 };
953
954 let distinct = run_inverse_instructions(&c, &run(&[1, 0]));
955 assert!(matches!(
956 distinct.as_slice(),
957 [Instruction::Gate {
958 gate: Gate::MultiFused(_),
959 ..
960 }]
961 ));
962
963 let rzz_layer = run_inverse_instructions(&c, &run(&[4, 3]));
964 assert!(matches!(
965 rzz_layer.as_slice(),
966 [Instruction::Gate {
967 gate: Gate::BatchRzz(_),
968 ..
969 }]
970 ));
971
972 let repeated_qubit = run_inverse_instructions(&c, &run(&[2, 0]));
973 assert!(matches!(
974 repeated_qubit.as_slice(),
975 [Instruction::Gate {
976 gate: Gate::DiagonalBatch(_),
977 ..
978 }]
979 ));
980
981 let non_diagonal = run_inverse_instructions(&c, &run(&[5, 4]));
982 assert_eq!(non_diagonal.len(), 2);
983 }
984
985 #[test]
986 fn a_mixed_diagonal_run_collapses_into_one_diagonal_batch() {
987 let n = 6;
990 let mut c = Circuit::new(n, 0);
991 for q in 0..n - 1 {
992 c.add_gate(Gate::Rzz(0.31 + 0.07 * q as f64), &[q, q + 1]);
993 }
994 for q in 0..3 {
995 c.add_gate(Gate::Rz(0.4 + 0.11 * q as f64), &[q]);
996 }
997
998 let run: Vec<RunGate> = (0..c.instructions.len())
999 .rev()
1000 .map(|i| {
1001 let Instruction::Gate { gate, targets } = &c.instructions[i] else {
1002 unreachable!()
1003 };
1004 RunGate::new(i, gate.pauli_generator().unwrap(), targets)
1005 })
1006 .collect();
1007
1008 let inverses = run_inverse_instructions(&c, &run);
1009 let [
1010 Instruction::Gate {
1011 gate: Gate::DiagonalBatch(data),
1012 targets,
1013 },
1014 ] = inverses.as_slice()
1015 else {
1016 panic!("expected one DiagonalBatch, got {inverses:?}")
1017 };
1018 assert_eq!(data.entries.len(), 8);
1019 assert_eq!(targets.as_slice(), &[0, 1, 2, 3, 4, 5]);
1020 }
1021
1022 #[test]
1023 fn a_full_light_cone_borrows_the_circuit_it_was_cut_from() {
1024 let mut c = Circuit::new(2, 1);
1025 c.add_gate(Gate::Rx(0.3), &[0]);
1026 c.add_gate(Gate::Cx, &[0, 1]);
1027 c.add_gate(Gate::Ry(0.7), &[1]);
1028
1029 let all: Vec<usize> = (0..c.instructions.len()).collect();
1030 assert!(matches!(kept_subcircuit(&c, &all), Cow::Borrowed(_)));
1031
1032 let pruned = kept_subcircuit(&c, &[0, 2]);
1033 assert!(matches!(pruned, Cow::Owned(_)));
1034 assert_eq!(pruned.num_qubits, c.num_qubits);
1035 assert_eq!(pruned.num_classical_bits, c.num_classical_bits);
1036 let gates: Vec<&Gate> = pruned
1037 .instructions
1038 .iter()
1039 .map(|inst| {
1040 let Instruction::Gate { gate, .. } = inst else {
1041 unreachable!()
1042 };
1043 gate
1044 })
1045 .collect();
1046 assert!(matches!(gates.as_slice(), [Gate::Rx(_), Gate::Ry(_)]));
1047 }
1048}