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::{ParamLink, Parameters, angle_mut, angle_of};
24use crate::circuit::{Circuit, Instruction, PreparedCircuit, SmallVec, smallvec};
25use crate::error::{PrismError, Result};
26use crate::gates::{Gate, GeneratorKind, is_diagonal_2x2, pauli_rot_masks};
27
28use super::noise::NoiseModel;
29use super::unified_pauli::PauliTerm;
30use super::{BackendKind, i_pow, pauli_masks, pauli_sandwiches_from_masks};
31
32#[derive(Debug, Clone, PartialEq)]
34pub struct ExpectationGradient {
35 pub value: f64,
37 pub gradient: Vec<f64>,
39}
40
41pub fn run_expectation_gradient(
67 circuit: &Circuit,
68 hamiltonian: &[(f64, Vec<PauliTerm>)],
69 params: &Parameters,
70 seed: u64,
71) -> Result<ExpectationGradient> {
72 if super::has_nonunitary_or_classical_ops(circuit) {
73 return Err(PrismError::IncompatibleBackend {
74 backend: "Statevector".into(),
75 reason: "adjoint gradients require a unitary circuit without measurements, resets, or conditionals".into(),
76 });
77 }
78
79 if circuit.instructions.iter().any(|inst| {
80 matches!(
81 inst,
82 Instruction::Gate {
83 gate: Gate::QftBlock { .. },
84 ..
85 }
86 )
87 }) {
88 return Err(PrismError::IncompatibleBackend {
89 backend: "Statevector".into(),
90 reason: "adjoint gradients do not support QftBlock; expand it to primitive gates first"
91 .into(),
92 });
93 }
94
95 params.validate(circuit)?;
96
97 if circuit.num_qubits > max_statevector_qubits() {
98 return Err(PrismError::ResourceLimit(Box::new(
99 crate::error::ResourceLimit {
100 backend: "Statevector".into(),
101 operation: "adjoint gradients, which hold two statevectors,".to_string(),
102 resource: crate::error::ResourceKind::Qubits,
103 required: circuit.num_qubits as u128,
104 limit: max_statevector_qubits() as u128,
105 env_var: Some("PRISM_MAX_SV_QUBITS"),
106 },
107 )));
108 }
109
110 let mut masked = Vec::with_capacity(hamiltonian.len());
112 for (coeff, terms) in hamiltonian {
113 let (xmask, zmask, num_y) = pauli_masks(terms, circuit.num_qubits)?;
114 masked.push((*coeff, xmask, zmask, num_y));
115 }
116
117 let in_cone = observable_light_cone(circuit, hamiltonian);
121 let kept: Vec<usize> = (0..circuit.instructions.len())
122 .filter(|&i| in_cone[i])
123 .collect();
124
125 let mut phi = StatevectorBackend::new(seed);
131 phi.init(circuit.num_qubits, circuit.num_classical_bits)?;
132 let forward = kept_subcircuit(circuit, &kept);
133 let expanded = super::expand_for_backend(&phi, &forward);
134 let fused = super::fuse_for_backend(&phi, &expanded);
135 phi.apply_instructions(&fused.instructions)?;
136
137 let (value, lambda_state) = build_lambda_and_value(phi.state_vector(), &masked)?;
138
139 let mut gradient = vec![0.0; params.num_slots()];
140
141 let mut links: Vec<_> = params
144 .links()
145 .iter()
146 .filter(|l| in_cone[l.instruction])
147 .copied()
148 .collect();
149 links.sort_by_key(|l| l.instruction);
150
151 let Some(earliest) = links.first().map(|l| l.instruction) else {
156 return Ok(ExpectationGradient { value, gradient });
157 };
158
159 let mut lambda = StatevectorBackend::new(seed);
160 lambda.init_from_state(lambda_state, circuit.num_classical_bits)?;
161
162 let sweep = &kept[kept.partition_point(|&i| i < earliest)..];
165 let mut first_link = Vec::with_capacity(sweep.len() + 1);
166 let mut cursor = 0;
167 for &i in sweep {
168 first_link.push(cursor);
169 while cursor < links.len() && links[cursor].instruction == i {
170 cursor += 1;
171 }
172 }
173 first_link.push(cursor);
174
175 let trainable: Vec<bool> = first_link.windows(2).map(|w| w[0] < w[1]).collect();
176 let footprints: Vec<Footprint> = sweep
177 .iter()
178 .map(|&i| {
179 let Instruction::Gate { gate, targets } = &circuit.instructions[i] else {
180 unreachable!("the light cone keeps gate instructions only")
181 };
182 Footprint::of(gate, targets)
183 })
184 .collect();
185 let plan = plan_sweep(&footprints, &trainable, circuit.num_qubits);
186
187 let mut masks: Vec<(usize, usize, u32)> = Vec::new();
188 let (mut evaluated, mut inverted) = (0, 0);
189 for step in 0..=plan.last_step {
190 let evaluate = evaluated..plan.evaluate.partition_point(|&(t, _)| t <= step);
191 evaluated = evaluate.end;
192 if !evaluate.is_empty() {
193 masks.clear();
194 masks.extend(plan.evaluate[evaluate.clone()].iter().map(|&(_, s)| {
195 let f = &footprints[s];
196 (f.xmask, f.zmask, f.num_y)
197 }));
198 let values =
199 pauli_sandwiches_from_masks(lambda.state_vector(), phi.state_vector(), &masks);
200 for (&(_, s), value) in plan.evaluate[evaluate].iter().zip(&values) {
201 for link in &links[first_link[s]..first_link[s + 1]] {
202 gradient[link.slot] += value.im;
203 }
204 }
205 }
206
207 let invert = inverted..plan.invert.partition_point(|&(t, _)| t <= step);
208 inverted = invert.end;
209 if !invert.is_empty() {
210 let stretch = circuit.with_instructions(
211 plan.invert[invert]
212 .iter()
213 .map(|&(_, s)| inverse_instruction(&circuit.instructions[sweep[s]]))
214 .collect(),
215 );
216 let expanded = super::expand_for_backend(&phi, &stretch);
217 let fused = super::fuse_for_backend(&phi, &expanded);
218 phi.apply_instructions(&fused.instructions)?;
219 lambda.apply_instructions(&fused.instructions)?;
220 }
221 }
222
223 Ok(ExpectationGradient { value, gradient })
224}
225
226#[derive(Debug, Clone, Copy, PartialEq, Eq)]
230enum LocalAction {
231 Z,
232 X,
233 Y,
234 Any,
235}
236
237impl LocalAction {
238 fn from_bits(x: bool, z: bool) -> Option<Self> {
239 match (x, z) {
240 (false, false) => None,
241 (true, false) => Some(Self::X),
242 (false, true) => Some(Self::Z),
243 (true, true) => Some(Self::Y),
244 }
245 }
246
247 fn bit(self) -> u8 {
248 match self {
249 Self::Z => 1,
250 Self::X => 2,
251 Self::Y => 4,
252 Self::Any => 7,
253 }
254 }
255}
256
257struct Footprint {
265 pauli: bool,
266 xmask: usize,
267 zmask: usize,
268 num_y: u32,
269 support: SmallVec<[(usize, LocalAction); 4]>,
270}
271
272impl Footprint {
273 fn pauli((xmask, zmask, num_y): (usize, usize, u32), targets: &[usize]) -> Self {
274 let support = targets
275 .iter()
276 .filter_map(|&q| {
277 LocalAction::from_bits(xmask >> q & 1 == 1, zmask >> q & 1 == 1).map(|a| (q, a))
278 })
279 .collect();
280 Self {
281 pauli: true,
282 xmask,
283 zmask,
284 num_y,
285 support,
286 }
287 }
288
289 fn of(gate: &Gate, targets: &[usize]) -> Self {
290 use LocalAction::{Any, X, Z};
291 if let Some(kind) = gate.pauli_generator() {
292 return Self::pauli(generator_masks(kind, targets), targets);
293 }
294 let bit = || 1usize << targets[0];
295 let diagonal_or_any = |diagonal: bool| if diagonal { Z } else { Any };
296 let support: SmallVec<[(usize, LocalAction); 4]> = match gate {
297 Gate::Id => SmallVec::new(),
298 Gate::X | Gate::SX | Gate::SXdg => return Self::pauli((bit(), 0, 0), targets),
299 Gate::Y => return Self::pauli((bit(), bit(), 1), targets),
300 Gate::Z | Gate::S | Gate::Sdg | Gate::T | Gate::Tdg => {
301 return Self::pauli((0, bit(), 0), targets);
302 }
303 Gate::Cx => smallvec![(targets[0], Z), (targets[1], X)],
304 Gate::Cu(mat) => smallvec![
305 (targets[0], Z),
306 (targets[1], diagonal_or_any(is_diagonal_2x2(mat)))
307 ],
308 Gate::Mcu(data) => {
309 let (&target, controls) = targets.split_last().expect("Mcu has a target");
310 controls
311 .iter()
312 .map(|&q| (q, Z))
313 .chain([(target, diagonal_or_any(is_diagonal_2x2(&data.mat)))])
314 .collect()
315 }
316 Gate::Fused(mat) => smallvec![(targets[0], diagonal_or_any(is_diagonal_2x2(mat)))],
317 Gate::BatchPhase(data) => targets
318 .iter()
319 .copied()
320 .chain(data.phases.iter().map(|&(q, _)| q))
321 .map(|q| (q, Z))
322 .collect(),
323 Gate::Cz | Gate::BatchRzz(_) | Gate::DiagonalBatch(_) => {
324 targets.iter().map(|&q| (q, Z)).collect()
325 }
326 Gate::MultiFused(data) => {
327 let action = diagonal_or_any(data.all_diagonal);
328 targets.iter().map(|&q| (q, action)).collect()
329 }
330 _ => targets.iter().map(|&q| (q, Any)).collect(),
331 };
332 Self {
333 pauli: false,
334 xmask: 0,
335 zmask: 0,
336 num_y: 0,
337 support,
338 }
339 }
340
341 fn commutes_with(&self, other: &Footprint) -> bool {
342 if self.pauli && other.pauli {
343 let anticommuting =
344 (self.xmask & other.zmask).count_ones() + (self.zmask & other.xmask).count_ones();
345 return anticommuting.is_multiple_of(2);
346 }
347 self.support.iter().all(|&(q, a)| {
348 other
349 .support
350 .iter()
351 .all(|&(p, b)| p != q || (a == b && a != LocalAction::Any))
352 })
353 }
354
355 fn decides_locally(&self) -> bool {
359 !self.pauli || self.support.len() == 1
360 }
361
362 fn action_on(&self, qubit: usize) -> LocalAction {
363 self.support
364 .iter()
365 .find_map(|&(q, a)| (q == qubit).then_some(a))
366 .expect("a gate is listed only under qubits it acts on")
367 }
368}
369
370fn generator_masks(kind: GeneratorKind<'_>, targets: &[usize]) -> (usize, usize, u32) {
372 match kind {
373 GeneratorKind::RotX => (1usize << targets[0], 0, 0),
374 GeneratorKind::RotY => {
375 let bit = 1usize << targets[0];
376 (bit, bit, 1)
377 }
378 GeneratorKind::RotZ => (0, 1usize << targets[0], 0),
379 GeneratorKind::RotZz => (0, (1usize << targets[0]) | (1usize << targets[1]), 0),
380 GeneratorKind::Phase => (0, 1usize << targets[0], 0),
384 GeneratorKind::RotPauli(axes) => pauli_rot_masks(targets, axes),
385 }
386}
387
388fn inverse_instruction(instruction: &Instruction) -> Instruction {
389 let Instruction::Gate { gate, targets } = instruction else {
390 unreachable!("the light cone keeps gate instructions only")
391 };
392 Instruction::Gate {
393 gate: gate.inverse(),
394 targets: targets.clone(),
395 }
396}
397
398const SWEEP_SCAN_LIMIT: usize = 64;
402
403struct SweepPlan {
409 evaluate: Vec<(usize, usize)>,
410 invert: Vec<(usize, usize)>,
411 last_step: usize,
412}
413
414fn plan_sweep(footprints: &[Footprint], trainable: &[bool], num_qubits: usize) -> SweepPlan {
426 let len = footprints.len();
427 let mut on_qubit: Vec<Vec<usize>> = vec![Vec::new(); num_qubits];
428 let rows: Vec<SmallVec<[usize; 4]>> = footprints
429 .iter()
430 .enumerate()
431 .map(|(s, f)| {
432 f.support
433 .iter()
434 .map(|&(q, _)| {
435 on_qubit[q].push(s);
436 on_qubit[q].len() - 1
437 })
438 .collect()
439 })
440 .collect();
441
442 let mut blocker_start = Vec::with_capacity(len + 1);
449 let mut blockers: Vec<usize> = Vec::new();
450 let mut spills: Vec<(usize, usize, usize)> = Vec::new();
451 blocker_start.push(0);
452 for (s, f) in footprints.iter().enumerate() {
453 for (&(q, _), &row) in f.support.iter().zip(&rows[s]) {
454 let list = &on_qubit[q];
455 let limit = list.len().min(row + 1 + SWEEP_SCAN_LIMIT);
456 let mut covered = 0u8;
457 let mut next = row + 1;
458 while next < limit && covered.count_ones() < 2 {
459 let above = &footprints[list[next]];
460 if !f.commutes_with(above) {
461 blockers.push(list[next]);
462 if above.decides_locally() {
463 covered |= above.action_on(q).bit();
464 }
465 }
466 next += 1;
467 }
468 if covered.count_ones() < 2 && next < list.len() {
469 spills.push((s, q, next));
470 }
471 }
472 blocker_start.push(blockers.len());
473 }
474
475 let mut step = vec![0usize; len];
479 let mut latest_from: Vec<Vec<usize>> = on_qubit.iter().map(|l| vec![0; l.len()]).collect();
480 let mut spill = spills.len();
481 for s in (0..len).rev() {
482 let mut latest = blockers[blocker_start[s]..blocker_start[s + 1]]
483 .iter()
484 .map(|&i| step[i])
485 .max();
486 while spill > 0 && spills[spill - 1].0 == s {
487 spill -= 1;
488 let (_, q, row) = spills[spill];
489 latest = latest.max(Some(latest_from[q][row]));
490 }
491 step[s] = match latest {
492 Some(t) if trainable[s] => t + 1,
493 Some(t) => t,
494 None => 0,
495 };
496 for (&(q, _), &row) in footprints[s].support.iter().zip(&rows[s]) {
497 let above = latest_from[q].get(row + 1).copied().unwrap_or(0);
498 latest_from[q][row] = step[s].max(above);
499 }
500 }
501
502 let mut needed = vec![false; len];
503 let mut needed_from = vec![usize::MAX; num_qubits];
504 let mut spill = 0;
505 for s in 0..len {
506 needed[s] = needed[s]
507 || footprints[s]
508 .support
509 .iter()
510 .zip(&rows[s])
511 .any(|(&(q, _), &row)| row >= needed_from[q]);
512 let waits = needed[s] || trainable[s];
513 if waits {
514 for &i in &blockers[blocker_start[s]..blocker_start[s + 1]] {
515 needed[i] = true;
516 }
517 }
518 while spill < spills.len() && spills[spill].0 == s {
519 let (_, q, row) = spills[spill];
520 if waits {
521 needed_from[q] = needed_from[q].min(row);
522 }
523 spill += 1;
524 }
525 }
526
527 let last_step = (0..len)
528 .filter(|&s| trainable[s])
529 .map(|s| step[s])
530 .max()
531 .unwrap_or(0);
532 let mut evaluate: Vec<(usize, usize)> = (0..len)
533 .filter(|&s| trainable[s])
534 .map(|s| (step[s], s))
535 .collect();
536 evaluate.sort_unstable();
537 let mut invert: Vec<(usize, usize)> = (0..len)
538 .filter(|&s| needed[s] && step[s] < last_step)
539 .map(|s| (step[s], s))
540 .collect();
541 invert.sort_unstable_by_key(|&(t, s)| (t, std::cmp::Reverse(s)));
542 SweepPlan {
543 evaluate,
544 invert,
545 last_step,
546 }
547}
548
549fn kept_subcircuit<'a>(circuit: &'a Circuit, kept: &[usize]) -> Cow<'a, Circuit> {
553 if kept.len() == circuit.instructions.len() {
554 return Cow::Borrowed(circuit);
555 }
556 let instructions = kept
557 .iter()
558 .map(|&i| circuit.instructions[i].clone())
559 .collect();
560 Cow::Owned(circuit.with_instructions(instructions))
561}
562
563fn observable_light_cone(circuit: &Circuit, hamiltonian: &[(f64, Vec<PauliTerm>)]) -> Vec<bool> {
567 let union: Vec<PauliTerm> = hamiltonian
568 .iter()
569 .flat_map(|(_, terms)| terms.iter().copied())
570 .collect();
571 super::unified_pauli::inverse_light_cone(circuit, &union)
572}
573
574struct TermGroup {
578 xmask: usize,
579 terms: Vec<(usize, Complex64)>,
580}
581
582fn group_terms_by_xmask(masked: &[(f64, usize, usize, u32)]) -> Vec<TermGroup> {
585 let mut flat: Vec<(usize, usize, Complex64)> = masked
586 .iter()
587 .map(|&(coeff, xmask, zmask, num_y)| {
588 (xmask, zmask, Complex64::new(coeff, 0.0) * i_pow(num_y))
589 })
590 .collect();
591 flat.sort_by_key(|&(xmask, _, _)| xmask);
592
593 let mut groups: Vec<TermGroup> = Vec::with_capacity(flat.len());
594 for (xmask, zmask, factor) in flat {
595 match groups.last_mut() {
596 Some(group) if group.xmask == xmask => group.terms.push((zmask, factor)),
597 _ => groups.push(TermGroup {
598 xmask,
599 terms: vec![(zmask, factor)],
600 }),
601 }
602 }
603 groups
604}
605
606#[inline(always)]
614fn gather_lambda_chunk(
615 groups: &[TermGroup],
616 phi: &[Complex64],
617 base: usize,
618 out: &mut [Complex64],
619) -> f64 {
620 let mut value = 0.0;
621 for (offset, slot) in out.iter_mut().enumerate() {
622 let i = base + offset;
623 let mut acc = Complex64::new(0.0, 0.0);
624 for group in groups {
625 let j = i ^ group.xmask;
626 let mut weight = Complex64::new(0.0, 0.0);
627 for &(zmask, factor) in &group.terms {
628 let sign = if (j & zmask).count_ones() & 1 == 1 {
629 -1.0
630 } else {
631 1.0
632 };
633 weight += factor * sign;
634 }
635 let amp = unsafe { *phi.get_unchecked(j) };
640 acc += weight * amp;
641 }
642 let p = unsafe { *phi.get_unchecked(i) };
644 value += p.re * acc.re + p.im * acc.im;
645 *slot = acc;
646 }
647 value
648}
649
650fn build_lambda_and_value(
657 phi: &[Complex64],
658 masked: &[(f64, usize, usize, u32)],
659) -> Result<(f64, Vec<Complex64>)> {
660 let dim = phi.len();
661 let mut lambda: Vec<Complex64> = Vec::new();
662 reserve_dense_output(
663 &mut lambda,
664 dim,
665 "Statevector",
666 "adjoint gradient lambda state",
667 )?;
668 lambda.resize(dim, Complex64::new(0.0, 0.0));
669
670 let groups = group_terms_by_xmask(masked);
671 assert!(
672 dim.is_power_of_two() && groups.iter().all(|g| g.xmask < dim),
673 "observable masks must index the state dimension"
674 );
675
676 #[cfg(feature = "parallel")]
677 if dim >= (1 << crate::backend::PARALLEL_THRESHOLD_QUBITS) {
678 use crate::backend::MIN_PAR_ELEMS;
679 use rayon::prelude::*;
680
681 let value = lambda
682 .par_chunks_mut(MIN_PAR_ELEMS)
683 .enumerate()
684 .map(|(chunk, out)| gather_lambda_chunk(&groups, phi, chunk * MIN_PAR_ELEMS, out))
685 .sum();
686 return Ok((value, lambda));
687 }
688
689 let value = gather_lambda_chunk(&groups, phi, 0, &mut lambda);
690 Ok((value, lambda))
691}
692
693pub fn run_expectation_gradient_shift(
722 circuit: &Circuit,
723 hamiltonian: &[(f64, Vec<PauliTerm>)],
724 params: &Parameters,
725 seed: u64,
726) -> Result<ExpectationGradient> {
727 shift_gradient(
728 &BackendKind::Auto,
729 circuit,
730 hamiltonian,
731 params,
732 None,
733 None,
734 seed,
735 )
736}
737
738pub(crate) fn shift_gradient(
758 kind: &BackendKind,
759 circuit: &Circuit,
760 hamiltonian: &[(f64, Vec<PauliTerm>)],
761 params: &Parameters,
762 noise: Option<&NoiseModel>,
763 initial_state: Option<super::StartState<'_>>,
764 seed: u64,
765) -> Result<ExpectationGradient> {
766 params.validate(circuit)?;
767 if initial_state.is_none()
770 && noise.is_none()
771 && circuit.num_qubits >= crate::circuit::fusion::MIN_QUBITS_FOR_FUSION
772 {
773 return prepared_shift_gradient(kind, circuit, hamiltonian, params, seed);
774 }
775 if initial_state.is_some() || noise.is_some() {
776 super::require_unitary_circuit(kind, circuit, "expectation values require")?;
777 }
778
779 let observables: Vec<Vec<PauliTerm>> =
780 hamiltonian.iter().map(|(_, terms)| terms.clone()).collect();
781 let evaluate = |c: &Circuit| -> Result<f64> {
782 if let BackendKind::PauliPath { epsilon, max_terms } = kind {
783 let per_term =
784 super::pauli_path_expectations(c, noise, &observables, *epsilon, *max_terms)?
785 .into_values();
786 return Ok(hamiltonian
787 .iter()
788 .zip(per_term)
789 .map(|((coeff, _), v)| coeff * v)
790 .sum());
791 }
792 let per_term = match (noise, initial_state) {
793 (Some(noise), _) => super::noise::dm_expectation_values(
794 kind,
795 c,
796 &observables,
797 Some(noise),
798 initial_state,
799 seed,
800 )?,
801 (None, Some(state)) => {
802 super::expectation_values_from_initial_state(kind, c, state, &observables, seed)?
803 .into_values()
804 }
805 (None, None) => {
806 super::run_expectation_values_with(kind.clone(), c, &observables, seed)?
807 }
808 };
809 Ok(hamiltonian
810 .iter()
811 .zip(per_term)
812 .map(|((coeff, _), v)| coeff * v)
813 .sum())
814 };
815
816 let value = evaluate(circuit)?;
817 let mut gradient = vec![0.0; params.num_slots()];
818 if params.is_empty() {
819 return Ok(ExpectationGradient { value, gradient });
820 }
821
822 let shift = std::f64::consts::FRAC_PI_2;
823 let term = |shifted: &mut Circuit, instruction: usize| -> Result<f64> {
824 let base = *angle_mut(&mut shifted.instructions[instruction]);
825 *angle_mut(&mut shifted.instructions[instruction]) = base + shift;
826 let plus = evaluate(shifted)?;
827 *angle_mut(&mut shifted.instructions[instruction]) = base - shift;
828 let minus = evaluate(shifted)?;
829 *angle_mut(&mut shifted.instructions[instruction]) = base;
830 Ok(0.5 * (plus - minus))
831 };
832
833 let links = params.links();
836 #[cfg(feature = "parallel")]
837 if links.len() > 1 && super::runs_split_across_workers(kind, circuit.num_qubits) {
838 use rayon::prelude::*;
839 let terms: Vec<Result<f64>> = links
840 .par_iter()
841 .map_init(
842 || circuit.clone(),
843 |shifted, link| term(shifted, link.instruction),
844 )
845 .collect();
846 for (link, t) in links.iter().zip(terms) {
847 gradient[link.slot] += t?;
848 }
849 return Ok(ExpectationGradient { value, gradient });
850 }
851
852 let mut shifted = circuit.clone();
853 for link in links {
854 gradient[link.slot] += term(&mut shifted, link.instruction)?;
855 }
856
857 Ok(ExpectationGradient { value, gradient })
858}
859
860fn prepared_shift_gradient(
864 kind: &BackendKind,
865 circuit: &Circuit,
866 hamiltonian: &[(f64, Vec<PauliTerm>)],
867 params: &Parameters,
868 seed: u64,
869) -> Result<ExpectationGradient> {
870 let links = params.links();
873 let mut sites: Vec<usize> = links.iter().map(|link| link.instruction).collect();
874 sites.sort_unstable();
875 sites.dedup();
876 let base: Vec<f64> = sites
877 .iter()
878 .map(|&i| angle_of(&circuit.instructions[i]))
879 .collect();
880 let site_params = Parameters::from_links(
881 sites
882 .iter()
883 .enumerate()
884 .map(|(slot, &instruction)| ParamLink { instruction, slot })
885 .collect(),
886 sites.len(),
887 );
888
889 let observables: Vec<Vec<PauliTerm>> =
890 hamiltonian.iter().map(|(_, terms)| terms.clone()).collect();
891 let energy = |prepared: &mut PreparedCircuit, values: &[f64]| -> Result<f64> {
892 let per_term = prepared.expectation_values(values, &observables, seed)?;
893 Ok(hamiltonian
894 .iter()
895 .zip(per_term)
896 .map(|((coeff, _), v)| coeff * v)
897 .sum())
898 };
899
900 let mut prepared = PreparedCircuit::with_backend(circuit.clone(), site_params, kind.clone())?;
901 let value = energy(&mut prepared, &base)?;
902 let mut gradient = vec![0.0; params.num_slots()];
903 if params.is_empty() {
904 return Ok(ExpectationGradient { value, gradient });
905 }
906
907 let shift = std::f64::consts::FRAC_PI_2;
910 let terms = prepared.map_split(links, |worker, link| {
911 let site = sites
912 .binary_search(&link.instruction)
913 .expect("every link names a site");
914 let mut values = base.clone();
915 values[site] = base[site] + shift;
916 let plus = energy(worker, &values)?;
917 values[site] = base[site] - shift;
918 let minus = energy(worker, &values)?;
919 Ok(0.5 * (plus - minus))
920 })?;
921 for (link, term) in links.iter().zip(terms) {
922 gradient[link.slot] += term;
923 }
924
925 Ok(ExpectationGradient { value, gradient })
926}
927
928#[cfg(test)]
929mod tests {
930 use super::*;
931 use crate::sim::unified_pauli::PauliTerm;
932
933 fn z_obs(qubit: usize) -> Vec<(f64, Vec<PauliTerm>)> {
934 vec![(1.0, vec![PauliTerm::z(qubit)])]
935 }
936
937 #[test]
938 fn single_rx_gradient_matches_analytic() {
939 let theta = 0.7;
941 let mut c = Circuit::new(1, 0);
942 c.add_gate(Gate::Rx(theta), &[0]);
943 let mut params = Parameters::new(1);
944 params.link(0, 0);
945
946 let g = run_expectation_gradient(&c, &z_obs(0), ¶ms, 42).unwrap();
947 assert!((g.value - theta.cos()).abs() < 1e-12);
948 assert!((g.gradient[0] - (-theta.sin())).abs() < 1e-9);
949 }
950
951 #[test]
952 fn ry_generator_carries_num_y() {
953 let theta = 1.3;
955 let mut c = Circuit::new(1, 0);
956 c.add_gate(Gate::Ry(theta), &[0]);
957 let mut params = Parameters::new(1);
958 params.link(0, 0);
959
960 let g = run_expectation_gradient(&c, &z_obs(0), ¶ms, 42).unwrap();
961 assert!((g.gradient[0] - (-theta.sin())).abs() < 1e-9);
962 }
963
964 #[test]
965 fn phase_projector_gradient() {
966 let theta = 0.9;
968 let mut c = Circuit::new(1, 0);
969 c.add_gate(Gate::H, &[0]);
970 c.add_gate(Gate::P(theta), &[0]);
971 let mut params = Parameters::new(1);
972 params.link(1, 0);
973
974 let obs = vec![(1.0, vec![PauliTerm::x(0)])];
975 let g = run_expectation_gradient(&c, &obs, ¶ms, 42).unwrap();
976 assert!((g.value - theta.cos()).abs() < 1e-12);
977 assert!((g.gradient[0] - (-theta.sin())).abs() < 1e-9);
978 }
979
980 #[test]
981 fn shared_parameter_accumulates() {
982 let theta = 0.4;
985 let mut c = Circuit::new(2, 0);
986 c.add_gate(Gate::Rx(theta), &[0]);
987 c.add_gate(Gate::Rx(theta), &[1]);
988 let mut params = Parameters::new(1);
989 params.link(0, 0);
990 params.link(1, 0);
991
992 let obs = vec![(1.0, vec![PauliTerm::z(0)]), (1.0, vec![PauliTerm::z(1)])];
993 let g = run_expectation_gradient(&c, &obs, ¶ms, 42).unwrap();
994 assert_eq!(g.gradient.len(), 1);
995 assert!((g.gradient[0] - (-2.0 * theta.sin())).abs() < 1e-9);
996 }
997
998 fn hea_with_shared_slots(n: usize) -> (Circuit, Parameters) {
999 let circuit = crate::circuits::hardware_efficient_ansatz(n, 2, 7);
1000 let mut params = Parameters::new(3);
1001 let rotations = circuit.instructions.iter().enumerate().filter(|(_, inst)| {
1002 matches!(
1003 inst,
1004 Instruction::Gate {
1005 gate: Gate::Ry(_) | Gate::Rz(_),
1006 ..
1007 }
1008 )
1009 });
1010 for (k, (i, _)) in rotations.enumerate() {
1011 params.link(i, k % 3);
1012 }
1013 (circuit, params)
1014 }
1015
1016 fn slot_per_link(params: &Parameters) -> Parameters {
1017 let links = params.links();
1018 Parameters::from_links(
1019 links
1020 .iter()
1021 .enumerate()
1022 .map(|(slot, link)| ParamLink {
1023 instruction: link.instruction,
1024 slot,
1025 })
1026 .collect(),
1027 links.len(),
1028 )
1029 }
1030
1031 fn weighted_energy(ham: &[(f64, Vec<PauliTerm>)], per_term: Vec<f64>) -> f64 {
1032 ham.iter()
1033 .zip(per_term)
1034 .map(|((coeff, _), v)| coeff * v)
1035 .sum()
1036 }
1037
1038 #[test]
1042 fn shift_gradient_matches_a_sequential_sum_bit_for_bit() {
1043 let (circuit, params) = hea_with_shared_slots(10);
1044 let ham = vec![
1045 (0.5, vec![PauliTerm::z(0), PauliTerm::z(1)]),
1046 (1.5, vec![PauliTerm::x(3)]),
1047 ];
1048 let observables: Vec<Vec<PauliTerm>> = ham.iter().map(|(_, t)| t.clone()).collect();
1049 let links = params.links();
1050 let sites = slot_per_link(¶ms);
1051 let base = sites.values(&circuit).unwrap();
1052 let mut prepared = PreparedCircuit::new(circuit.clone(), sites).unwrap();
1053 assert!(prepared.reuses_fusion_plan());
1054 let mut energy = |values: &[f64]| {
1055 weighted_energy(
1056 &ham,
1057 prepared
1058 .expectation_values(values, &observables, 42)
1059 .unwrap(),
1060 )
1061 };
1062 let shift = std::f64::consts::FRAC_PI_2;
1063 let mut expected = vec![0.0; 3];
1064 for (k, link) in links.iter().enumerate() {
1065 let mut values = base.clone();
1066 values[k] = base[k] + shift;
1067 let plus = energy(&values);
1068 values[k] = base[k] - shift;
1069 let minus = energy(&values);
1070 expected[link.slot] += 0.5 * (plus - minus);
1071 }
1072
1073 let g = run_expectation_gradient_shift(&circuit, &ham, ¶ms, 42).unwrap();
1074 assert_eq!(g.gradient, expected);
1075 }
1076
1077 #[test]
1081 fn shift_gradient_matches_fresh_fusion_on_degenerate_shifts() {
1082 for n in [6, 10] {
1083 let (mut circuit, params) = hea_with_shared_slots(n);
1084 for link in params.links().iter().step_by(2) {
1085 *angle_mut(&mut circuit.instructions[link.instruction]) =
1086 std::f64::consts::FRAC_PI_2;
1087 }
1088 let ham: Vec<(f64, Vec<PauliTerm>)> = (0..n - 1)
1089 .map(|q| (1.0, vec![PauliTerm::z(q), PauliTerm::z(q + 1)]))
1090 .chain([(0.7, vec![PauliTerm::x(2)])])
1091 .collect();
1092 let observables: Vec<Vec<PauliTerm>> = ham.iter().map(|(_, t)| t.clone()).collect();
1093 let energy = |c: &Circuit| {
1094 weighted_energy(
1095 &ham,
1096 super::super::run_expectation_values_with(
1097 BackendKind::Auto,
1098 c,
1099 &observables,
1100 42,
1101 )
1102 .unwrap(),
1103 )
1104 };
1105 let shift = std::f64::consts::FRAC_PI_2;
1106 let mut expected = vec![0.0; params.num_slots()];
1107 for link in params.links() {
1108 let mut shifted = circuit.clone();
1109 let base = *angle_mut(&mut shifted.instructions[link.instruction]);
1110 *angle_mut(&mut shifted.instructions[link.instruction]) = base + shift;
1111 let plus = energy(&shifted);
1112 *angle_mut(&mut shifted.instructions[link.instruction]) = base - shift;
1113 let minus = energy(&shifted);
1114 expected[link.slot] += 0.5 * (plus - minus);
1115 }
1116
1117 if n == 10 {
1118 let sites = slot_per_link(¶ms);
1119 let base = sites.values(&circuit).unwrap();
1120 let mut prepared = PreparedCircuit::new(circuit.clone(), sites).unwrap();
1121 assert!(prepared.reuses_fusion_plan());
1122 let replayed = prepared.bind_fused(&base).unwrap().instructions.len();
1123 let fell_back = (0..base.len()).any(|k| {
1124 let mut values = base.clone();
1125 values[k] -= shift;
1126 prepared.bind_fused(&values).unwrap().instructions.len() != replayed
1127 });
1128 assert!(fell_back, "no shifted binding left the captured plan");
1129 }
1130
1131 let g = run_expectation_gradient_shift(&circuit, &ham, ¶ms, 42).unwrap();
1132 assert!((g.value - energy(&circuit)).abs() < 1e-10, "{n}q value");
1133 for (slot, (got, want)) in g.gradient.iter().zip(&expected).enumerate() {
1134 assert!(
1135 (got - want).abs() < 1e-10,
1136 "{n}q slot {slot}: {got} vs {want}"
1137 );
1138 }
1139 }
1140 }
1141
1142 #[test]
1143 fn empty_params_returns_value_only() {
1144 let mut c = Circuit::new(1, 0);
1145 c.add_gate(Gate::Rx(0.5), &[0]);
1146 let g = run_expectation_gradient(&c, &z_obs(0), &Parameters::new(0), 42).unwrap();
1147 assert!(g.gradient.is_empty());
1148 assert!((g.value - 0.5f64.cos()).abs() < 1e-12);
1149 }
1150
1151 #[test]
1152 fn nondifferentiable_trainable_gate_is_rejected() {
1153 let mut c = Circuit::new(1, 0);
1154 c.add_gate(Gate::H, &[0]);
1155 let mut params = Parameters::new(1);
1156 params.link(0, 0);
1157 assert!(run_expectation_gradient(&c, &z_obs(0), ¶ms, 42).is_err());
1158 }
1159
1160 #[test]
1161 fn nonunitary_circuit_is_rejected() {
1162 let mut c = Circuit::new(1, 1);
1163 c.add_gate(Gate::Rx(0.3), &[0]);
1164 c.add_measure(0, 0);
1165 assert!(run_expectation_gradient(&c, &z_obs(0), &Parameters::new(0), 42).is_err());
1166 }
1167
1168 #[test]
1169 fn single_term_hamiltonian_matches_analytic() {
1170 let theta = 0.6;
1172 let mut c = Circuit::new(1, 0);
1173 c.add_gate(Gate::Ry(theta), &[0]);
1174 let params = Parameters::all_rotations(&c);
1175
1176 let obs = vec![(1.0, vec![PauliTerm::x(0)])];
1177 let g = run_expectation_gradient(&c, &obs, ¶ms, 42).unwrap();
1178 assert!((g.value - theta.sin()).abs() < 1e-12);
1179 assert!((g.gradient[0] - theta.cos()).abs() < 1e-12);
1180 }
1181
1182 #[test]
1183 fn diagonal_hamiltonian_matches_analytic() {
1184 let (a, b) = (0.4, 1.1);
1187 let mut c = Circuit::new(2, 0);
1188 c.add_gate(Gate::Ry(a), &[0]);
1189 c.add_gate(Gate::Ry(b), &[1]);
1190 let params = Parameters::all_rotations(&c);
1191
1192 let obs = vec![
1193 (1.0, vec![PauliTerm::z(0)]),
1194 (0.5, vec![PauliTerm::z(1)]),
1195 (0.25, vec![PauliTerm::z(0), PauliTerm::z(1)]),
1196 ];
1197 let g = run_expectation_gradient(&c, &obs, ¶ms, 42).unwrap();
1198 let value = a.cos() + 0.5 * b.cos() + 0.25 * a.cos() * b.cos();
1199 assert!((g.value - value).abs() < 1e-12);
1200 assert!((g.gradient[0] - (-a.sin() - 0.25 * a.sin() * b.cos())).abs() < 1e-12);
1201 assert!((g.gradient[1] - (-0.5 * b.sin() - 0.25 * a.cos() * b.sin())).abs() < 1e-12);
1202 }
1203
1204 #[test]
1205 fn grouped_x_mask_terms_match_parameter_shift() {
1206 let mut c = Circuit::new(3, 0);
1209 c.add_gate(Gate::Ry(0.4), &[0]);
1210 c.add_gate(Gate::Cx, &[0, 1]);
1211 c.add_gate(Gate::Rx(0.9), &[1]);
1212 c.add_gate(Gate::Cx, &[1, 2]);
1213 c.add_gate(Gate::Rz(0.3), &[2]);
1214 let params = Parameters::all_rotations(&c);
1215
1216 let obs = vec![
1217 (1.0, vec![PauliTerm::x(0)]),
1218 (-0.5, vec![PauliTerm::y(0)]),
1219 (0.75, vec![PauliTerm::x(0), PauliTerm::z(1)]),
1220 (0.25, vec![PauliTerm::y(0), PauliTerm::z(2)]),
1221 ];
1222 let adjoint = run_expectation_gradient(&c, &obs, ¶ms, 42).unwrap();
1223 let shift = run_expectation_gradient_shift(&c, &obs, ¶ms, 42).unwrap();
1224 assert!((adjoint.value - shift.value).abs() < 1e-12);
1225 for (slot, (&got, &want)) in adjoint.gradient.iter().zip(&shift.gradient).enumerate() {
1226 assert!((got - want).abs() < 1e-12, "slot {slot}: {got} vs {want}");
1227 }
1228 }
1229
1230 #[test]
1231 fn parallel_threshold_width_matches_parameter_shift() {
1232 let mut c = Circuit::new(15, 0);
1234 c.add_gate(Gate::Ry(0.3), &[0]);
1235 c.add_gate(Gate::Cx, &[0, 7]);
1236 c.add_gate(Gate::Rx(0.8), &[7]);
1237 c.add_gate(Gate::Cx, &[7, 14]);
1238 c.add_gate(Gate::Rz(0.5), &[14]);
1239 let params = Parameters::all_rotations(&c);
1240
1241 let obs = vec![
1242 (1.0, vec![PauliTerm::z(0)]),
1243 (0.5, vec![PauliTerm::x(7)]),
1244 (-0.25, vec![PauliTerm::y(7), PauliTerm::z(14)]),
1245 (0.75, vec![PauliTerm::x(7), PauliTerm::z(0)]),
1246 ];
1247 let adjoint = run_expectation_gradient(&c, &obs, ¶ms, 42).unwrap();
1248 let shift = run_expectation_gradient_shift(&c, &obs, ¶ms, 42).unwrap();
1249 assert!((adjoint.value - shift.value).abs() < 1e-12);
1250 for (slot, (&got, &want)) in adjoint.gradient.iter().zip(&shift.gradient).enumerate() {
1251 assert!((got - want).abs() < 1e-12, "slot {slot}: {got} vs {want}");
1252 }
1253 }
1254
1255 #[test]
1256 fn the_gather_matches_a_term_by_term_reference() {
1257 let dim = 1usize << 6;
1261 let phi: Vec<Complex64> = (0..dim)
1262 .map(|i| Complex64::new((i as f64 * 0.37).sin(), (i as f64 * 0.11).cos()))
1263 .collect();
1264 let masked = vec![
1265 (1.0, 0usize, 0b101usize, 0u32),
1266 (-0.5, 0b010, 0b100, 1),
1267 (0.75, 0b010, 0b001, 0),
1268 (0.25, 0b110, 0b011, 2),
1269 ];
1270
1271 let mut want = vec![Complex64::new(0.0, 0.0); dim];
1272 for &(coeff, xmask, zmask, num_y) in &masked {
1273 let factor = Complex64::new(coeff, 0.0) * i_pow(num_y);
1274 for (j, &) in phi.iter().enumerate() {
1275 let sign = if (j & zmask).count_ones() & 1 == 1 {
1276 -1.0
1277 } else {
1278 1.0
1279 };
1280 want[j ^ xmask] += factor * sign * amp;
1281 }
1282 }
1283 let want_value: f64 = phi.iter().zip(&want).map(|(p, l)| (p.conj() * l).re).sum();
1284
1285 let groups = group_terms_by_xmask(&masked);
1286 let mut got = vec![Complex64::new(0.0, 0.0); dim];
1287 let (lo, hi) = got.split_at_mut(dim / 2);
1288 let got_value = gather_lambda_chunk(&groups, &phi, 0, lo)
1289 + gather_lambda_chunk(&groups, &phi, dim / 2, hi);
1290
1291 assert!((want_value - got_value).abs() < 1e-12);
1292 for (i, (w, g)) in want.iter().zip(&got).enumerate() {
1293 assert!((w - g).norm() < 1e-12, "slot {i}: {w} vs {g}");
1294 }
1295 }
1296
1297 #[test]
1298 fn pauli_rot_generators_commute_on_an_even_anticommuting_count() {
1299 use crate::sim::unified_pauli::PauliAxis;
1300 let gate = |axes: &[PauliAxis], targets: &[usize]| {
1301 Footprint::pauli(
1302 generator_masks(GeneratorKind::RotPauli(axes), targets),
1303 targets,
1304 )
1305 };
1306 let x0y1 = gate(&[PauliAxis::X, PauliAxis::Y], &[0, 1]);
1307 let y0x1 = gate(&[PauliAxis::Y, PauliAxis::X], &[0, 1]);
1308 let wide = gate(
1309 &[PauliAxis::Y, PauliAxis::X, PauliAxis::X, PauliAxis::X],
1310 &[0, 1, 2, 3],
1311 );
1312 let x1y2 = gate(&[PauliAxis::X, PauliAxis::Y], &[1, 2]);
1313
1314 assert!(x0y1.commutes_with(&y0x1));
1315 assert!(x0y1.commutes_with(&x0y1));
1316 assert!(!wide.commutes_with(&x1y2));
1317 assert!(wide.commutes_with(&y0x1));
1318 }
1319
1320 #[test]
1321 fn footprints_commute_through_control_and_target_actions() {
1322 let cx = Footprint::of(&Gate::Cx, &[0, 1]);
1323 let commutes = |gate: Gate, targets: &[usize]| {
1324 let f = Footprint::of(&gate, targets);
1325 assert_eq!(f.commutes_with(&cx), cx.commutes_with(&f));
1326 f.commutes_with(&cx)
1327 };
1328 assert!(commutes(Gate::Rz(0.3), &[0]));
1329 assert!(commutes(Gate::Rx(0.3), &[1]));
1330 assert!(commutes(Gate::Cz, &[0, 2]));
1331 assert!(commutes(Gate::H, &[2]));
1332 assert!(!commutes(Gate::Rz(0.3), &[1]));
1333 assert!(!commutes(Gate::Ry(0.3), &[0]));
1334 assert!(!commutes(Gate::Rzz(0.3), &[0, 1]));
1335 assert!(!commutes(Gate::H, &[0]));
1336 assert!(!commutes(Gate::Cx, &[1, 0]));
1337 }
1338
1339 fn plan_for(circuit: &Circuit, params: &Parameters) -> SweepPlan {
1340 let mut linked: Vec<usize> = params.links().iter().map(|l| l.instruction).collect();
1341 linked.sort_unstable();
1342 let start = linked[0];
1343 let sweep: Vec<usize> = (start..circuit.instructions.len()).collect();
1344 let trainable: Vec<bool> = sweep
1345 .iter()
1346 .map(|i| linked.binary_search(i).is_ok())
1347 .collect();
1348 let footprints: Vec<Footprint> = sweep
1349 .iter()
1350 .map(|&i| {
1351 let Instruction::Gate { gate, targets } = &circuit.instructions[i] else {
1352 unreachable!()
1353 };
1354 Footprint::of(gate, targets)
1355 })
1356 .collect();
1357 plan_sweep(&footprints, &trainable, circuit.num_qubits)
1358 }
1359
1360 #[test]
1364 fn a_hardware_efficient_layer_takes_two_steps() {
1365 let (n, layers) = (8, 3);
1366 let circuit = crate::circuits::hardware_efficient_ansatz(n, layers, 7);
1367 let plan = plan_for(&circuit, &Parameters::all_rotations(&circuit));
1368 assert_eq!(plan.evaluate.len(), 2 * n * layers);
1369 assert!(
1370 plan.last_step <= 2 * layers,
1371 "{} steps for {layers} layers",
1372 plan.last_step + 1
1373 );
1374 let stretches = plan
1375 .invert
1376 .iter()
1377 .map(|&(t, _)| t)
1378 .collect::<std::collections::BTreeSet<_>>();
1379 assert!(stretches.len() <= 2 * layers);
1380 }
1381
1382 #[test]
1383 fn a_qaoa_layer_takes_two_steps() {
1384 let circuit = crate::circuits::qaoa_circuit(8, 3, 7);
1385 let plan = plan_for(&circuit, &Parameters::all_rotations(&circuit));
1386 assert_eq!(plan.last_step, 5);
1387 }
1388
1389 #[test]
1390 fn the_plan_never_inverts_the_earliest_trainable_gate_or_an_idle_one() {
1391 let mut c = Circuit::new(3, 0);
1392 c.add_gate(Gate::Ry(0.4), &[0]);
1393 c.add_gate(Gate::H, &[2]);
1394 c.add_gate(Gate::Cx, &[0, 1]);
1395 c.add_gate(Gate::Rz(0.7), &[1]);
1396 let mut params = Parameters::new(2);
1397 params.link(0, 0);
1398 params.link(3, 1);
1399
1400 let plan = plan_for(&c, ¶ms);
1401 let inverted: Vec<usize> = plan.invert.iter().map(|&(_, s)| s).collect();
1402 assert_eq!(inverted, [3, 2]);
1403 assert_eq!(plan.last_step, 1);
1404 }
1405
1406 fn assert_adjoint_matches_shift(
1407 circuit: &Circuit,
1408 ham: &[(f64, Vec<PauliTerm>)],
1409 params: &Parameters,
1410 ) {
1411 let adjoint = run_expectation_gradient(circuit, ham, params, 42).unwrap();
1412 let shift = run_expectation_gradient_shift(circuit, ham, params, 42).unwrap();
1413 let n = circuit.num_qubits;
1414 assert!((adjoint.value - shift.value).abs() < 1e-10, "{n}q value");
1415 for (slot, (got, want)) in adjoint.gradient.iter().zip(&shift.gradient).enumerate() {
1416 assert!(
1417 (got - want).abs() < 1e-10,
1418 "{n}q slot {slot}: {got} vs {want}"
1419 );
1420 }
1421 }
1422
1423 fn mixed_hamiltonian(n: usize) -> Vec<(f64, Vec<PauliTerm>)> {
1424 (0..n - 1)
1425 .map(|q| {
1426 (
1427 0.5 + 0.1 * q as f64,
1428 vec![PauliTerm::z(q), PauliTerm::z(q + 1)],
1429 )
1430 })
1431 .chain([
1432 (0.7, vec![PauliTerm::x(1)]),
1433 (-0.4, vec![PauliTerm::y(0), PauliTerm::x(n - 1)]),
1434 ])
1435 .collect()
1436 }
1437
1438 #[test]
1441 fn frontier_sweep_matches_parameter_shift_on_hea_and_qaoa() {
1442 for n in [5, 12, 16] {
1443 let ham = mixed_hamiltonian(n);
1444 let (hea, shared) = hea_with_shared_slots(n);
1445 assert_adjoint_matches_shift(&hea, &ham, &shared);
1446 assert_adjoint_matches_shift(&hea, &ham, &Parameters::all_rotations(&hea));
1447
1448 let qaoa = crate::circuits::qaoa_circuit(n, 2, 11);
1449 let mut params = Parameters::new(4);
1450 for (k, link) in Parameters::all_rotations(&qaoa).links().iter().enumerate() {
1451 let layer = k / (2 * n - 1);
1452 let mixer = k % (2 * n - 1) >= n - 1;
1453 params.link(link.instruction, 2 * layer + usize::from(mixer));
1454 }
1455 assert_adjoint_matches_shift(&qaoa, &ham, ¶ms);
1456 }
1457 }
1458
1459 #[test]
1463 fn frontier_sweep_matches_parameter_shift_with_fixed_gates_between() {
1464 use crate::sim::unified_pauli::PauliAxis;
1465 for n in [4, 12, 16] {
1466 let mut c = Circuit::new(n, 0);
1467 for q in 0..n {
1468 c.add_gate(Gate::H, &[q]);
1469 }
1470 for layer in 0..2 {
1471 for q in 0..n {
1472 let angle = 0.3 + 0.17 * (q + layer * n) as f64;
1473 c.add_gate(Gate::Ry(angle), &[q]);
1474 c.add_gate(if q % 3 == 0 { Gate::T } else { Gate::SX }, &[q]);
1475 c.add_gate(Gate::Rz(angle * 0.5), &[q]);
1476 }
1477 for q in (layer..n - 1).step_by(2) {
1478 c.add_gate(Gate::Cx, &[q, q + 1]);
1479 c.add_gate(Gate::Rzz(0.2 + 0.05 * q as f64), &[q, q + 1]);
1480 }
1481 c.add_gate(Gate::Cz, &[0, n - 1]);
1482 c.add_gate(Gate::Swap, &[1, 2]);
1483 c.add_gate(Gate::P(0.6), &[n - 1]);
1484 c.add_pauli_rotation(
1485 0.45,
1486 &[
1487 PauliTerm::new(0, PauliAxis::X),
1488 PauliTerm::new(2, PauliAxis::Y),
1489 PauliTerm::new(3, PauliAxis::Z),
1490 ],
1491 );
1492 c.add_gate(Gate::Rx(0.9), &[n / 2]);
1493 }
1494
1495 let mut params = Parameters::new(5);
1496 let rotations = c
1497 .instructions
1498 .iter()
1499 .enumerate()
1500 .filter(|(_, inst)| {
1501 matches!(inst, Instruction::Gate { gate, .. } if gate.pauli_generator().is_some())
1502 })
1503 .map(|(i, _)| i)
1504 .collect::<Vec<_>>();
1505 for (k, &i) in rotations.iter().enumerate() {
1506 if k % 7 != 3 {
1507 params.link(i, k % 5);
1508 }
1509 }
1510 assert_adjoint_matches_shift(&c, &mixed_hamiltonian(n), ¶ms);
1511 }
1512 }
1513
1514 #[test]
1515 fn a_full_light_cone_borrows_the_circuit_it_was_cut_from() {
1516 let mut c = Circuit::new(2, 1);
1517 c.add_gate(Gate::Rx(0.3), &[0]);
1518 c.add_gate(Gate::Cx, &[0, 1]);
1519 c.add_gate(Gate::Ry(0.7), &[1]);
1520
1521 let all: Vec<usize> = (0..c.instructions.len()).collect();
1522 assert!(matches!(kept_subcircuit(&c, &all), Cow::Borrowed(_)));
1523
1524 let pruned = kept_subcircuit(&c, &[0, 2]);
1525 assert!(matches!(pruned, Cow::Owned(_)));
1526 assert_eq!(pruned.num_qubits, c.num_qubits);
1527 assert_eq!(pruned.num_classical_bits, c.num_classical_bits);
1528 let gates: Vec<&Gate> = pruned
1529 .instructions
1530 .iter()
1531 .map(|inst| {
1532 let Instruction::Gate { gate, .. } = inst else {
1533 unreachable!()
1534 };
1535 gate
1536 })
1537 .collect();
1538 assert!(matches!(gates.as_slice(), [Gate::Rx(_), Gate::Ry(_)]));
1539 }
1540}