Skip to main content

prism_q/sim/
gradient.rs

1//! Gradients of expectation values, by the adjoint method and by parameter
2//! shift.
3//!
4//! Both compute `⟨H⟩ = ⟨0|U†HU|0⟩` and `d⟨H⟩/dθ` for a Hermitian
5//! `H = Σ c_k P_k`. The adjoint method ([`run_expectation_gradient`]) is exact
6//! in one pair of statevectors and costs one circuit evaluation regardless of
7//! the parameter count, but runs only on the statevector backend. Parameter
8//! shift ([`run_expectation_gradient_shift`]) costs two evaluations per
9//! trainable gate and reaches any backend with a native observable path,
10//! including widths past the statevector cap.
11//!
12//! The differentiated circuit must be unitary (no measurement, reset, or
13//! conditional) on both paths. Differentiable gates are `Rx`, `Ry`, `Rz`,
14//! `Rzz`, `P`, and `PauliRot` for both: those are the `Gate` variants carrying
15//! a rotation angle, so the shift rule reaches no gate the adjoint rejects.
16
17use 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/// Expectation value and its gradient with respect to each parameter slot.
36#[derive(Debug, Clone, PartialEq)]
37pub struct ExpectationGradient {
38    /// `⟨H⟩` at the circuit's current parameter values.
39    pub value: f64,
40    /// `d⟨H⟩/dθ`, one entry per parameter slot.
41    pub gradient: Vec<f64>,
42}
43
44/// Compute `⟨H⟩` and its exact gradient with respect to the trainable
45/// parameters using the adjoint method on the statevector backend.
46///
47/// `hamiltonian` is a weighted Pauli sum `Σ c_k P_k` with real coefficients;
48/// each `P_k` is a joint Pauli string (identity factors omitted). `params`
49/// declares which gate instructions are trainable and how they map to the
50/// gradient vector. The returned gradient has length `params.num_slots()`.
51///
52/// # Examples
53///
54/// ```
55/// use prism_q::{Circuit, Gate, Parameters, PauliTerm, run_expectation_gradient};
56///
57/// let theta = 0.5_f64;
58/// let mut circuit = Circuit::new(2, 0);
59/// circuit.add_gate(Gate::Rx(theta), &[0]);
60///
61/// let hamiltonian = vec![(1.0, vec![PauliTerm::z(0)])];
62/// let params = Parameters::all_rotations(&circuit);
63/// let g = run_expectation_gradient(&circuit, &hamiltonian, &params, 42)?;
64/// // <Z0> = cos(theta), d<Z0>/dtheta = -sin(theta).
65/// assert!((g.value - theta.cos()).abs() < 1e-12);
66/// assert!((g.gradient[0] + theta.sin()).abs() < 1e-9);
67/// # Ok::<(), prism_q::PrismError>(())
68/// ```
69pub 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    // Validate and reduce observables before the 2^n simulation.
112    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    // A gate outside the Hamiltonian's inverse light cone conjugates the
119    // back-propagated observable trivially, so ⟨H⟩ and every gradient entry
120    // are unchanged when it is dropped.
121    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    // The forward pass has to land |φ⟩ = U|0...0⟩ and nothing else, so it runs
127    // the kept gates through the ordinary fusion pipeline. The sweep below
128    // rebuilds every intermediate state by inverting `circuit.instructions` one
129    // entry at a time, so its 1:1 gate-to-generator view of the trainable gates
130    // survives whatever shape the fused stream takes.
131    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    // In-cone links sorted by descending instruction index, matching the
143    // reverse sweep. A cursor walks this list so the per-instruction lookup
144    // stays O(params), not O(instructions). An out-of-cone trainable gate has
145    // a provably zero gradient, so its links carry nothing to accumulate.
146    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    // The sweep stops at the earliest in-cone trainable gate: nothing before
155    // it contributes, so a non-trainable prefix costs no inverse applications.
156    // If no trainable gate reaches the observable, the gradient is zero
157    // everywhere.
158    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    // The sweep is the in-cone tail from the earliest trainable gate, walked
166    // backwards. Its trainable gates group into commuting runs, which share
167    // one state pair for their sandwiches and one fused pass for their
168    // inverses; a non-trainable gate carries no contribution and splits runs.
169    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        // The earliest in-cone trainable gate is the last one evaluated; its
222        // inverse and every gate before it can be skipped.
223        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
234/// One trainable gate staged in a commuting run: its instruction index and the
235/// Pauli masks of its generator.
236struct 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            // The projector generator differs from Z by the identity, whose
254            // sandwich `⟨λ|φ⟩ = ⟨φ|H|φ⟩` is real and contributes nothing to the
255            // imaginary part, so `P(θ) = e^{iθ/2} Rz(θ)` differentiates as `Rz`.
256            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    /// Two Pauli strings commute exactly when they anticommute on an even
268    /// number of qubits. `exp(-iθP/2)` then commutes with the other string as
269    /// well, which is what lets a run share one state pair: conjugating a
270    /// member's generator by the inverses of the members that follow it leaves
271    /// the generator, so every sandwich in the run reads the same `⟨λ|` and
272    /// `|φ⟩` as it would at its own position.
273    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
290/// Inverses of a commuting run, collapsed into a batch gate where the run's
291/// gate type has one: `MultiFused` for single-qubit rotations on distinct
292/// qubits, `BatchRzz` for an Rzz layer, `DiagonalBatch` for a mixed diagonal
293/// run. Anything else falls back to one instruction per gate. Order within a
294/// run is free because its gates commute.
295fn 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
385/// A run of diagonal rotations collapsed into one `DiagonalBatch` sweep. The
386/// kernel has no entry cap: a payload whose connected components outgrow the
387/// phase tables falls back to a per-element pass, still one traversal.
388fn 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
409/// The gates `kept` indexes, as a circuit of the original width so the fusion
410/// floors read the same qubit count. Borrowed when the cone keeps every
411/// instruction.
412fn 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
423/// Per-instruction flag: true if the gate lies in the Hamiltonian's inverse
424/// light cone (its support is connected to some observable term through the
425/// gates that follow it).
426fn 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
434/// Hamiltonian terms sharing one X mask. The gather reads `phi[i ^ xmask]`
435/// once per group; each `(Zmask, factor)` pair then contributes its own sign to
436/// that one amplitude.
437struct TermGroup {
438    xmask: usize,
439    terms: Vec<(usize, Complex64)>,
440}
441
442/// Group masked terms by X mask, ascending, so the diagonal terms (X mask 0)
443/// lead and the gather's first read is the sequential stream.
444fn 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/// Gather `|λ⟩ = Σ c_k P_k|φ⟩` over `out`, the slice of `λ` starting at `base`,
467/// and return that slice's share of `Re⟨φ|λ⟩`.
468///
469/// Each output element is written once, from
470/// `Σ_k factor_k · (-1)^popcount((i ⊕ Xmask_k) & Zmask_k) · phi[i ⊕ Xmask_k]`, so
471/// the terms batch into one pass over the register instead of one scattering
472/// pass each.
473#[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            // SAFETY: callers pass `out` as a chunk of a buffer of `phi`'s
496            // length starting at `base`, so `i < phi.len()`, and every X mask
497            // below the power-of-two `phi.len()`, so `i ^ xmask` stays in range.
498            // `build_lambda_and_value` asserts both before it fans out.
499            let amp = unsafe { *phi.get_unchecked(j) };
500            acc += weight * amp;
501        }
502        // SAFETY: same range argument with an X mask of zero.
503        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
510/// Build `|λ⟩ = Σ c_k P_k|φ⟩` into a fresh buffer and return `(⟨H⟩, |λ⟩)`,
511/// where `⟨H⟩ = Re⟨φ|λ⟩`.
512///
513/// # Panics
514///
515/// If `phi`'s length is not a power of two or a term's X mask indexes past it.
516fn 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
553/// Compute `⟨H⟩` and its gradient by the parameter-shift rule, routing every
554/// evaluation through automatic backend selection.
555///
556/// Unlike [`run_expectation_gradient`] this places no ceiling on the qubit
557/// count of its own: it holds one backend state at a time and inherits whatever
558/// the selected backend can represent. It also accepts `QftBlock`. The price is
559/// `1 + 2 * params.links().len()` circuit evaluations against the adjoint's
560/// one, so prefer the adjoint wherever it applies. Select an explicit backend
561/// with [`crate::simulate`] and `expectation_gradient_shift`.
562///
563/// # Examples
564///
565/// ```
566/// use prism_q::{Circuit, Gate, Parameters, PauliTerm, run_expectation_gradient_shift};
567///
568/// let theta = 0.5_f64;
569/// let mut circuit = Circuit::new(2, 0);
570/// circuit.add_gate(Gate::Rx(theta), &[0]);
571///
572/// let hamiltonian = vec![(1.0, vec![PauliTerm::z(0)])];
573/// let params = Parameters::all_rotations(&circuit);
574/// let g = run_expectation_gradient_shift(&circuit, &hamiltonian, &params, 42)?;
575/// assert!((g.gradient[0] + theta.sin()).abs() < 1e-9);
576/// # Ok::<(), prism_q::PrismError>(())
577/// ```
578pub 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
595/// Parameter-shift gradient on the backend `kind` selects, optionally from a
596/// start state.
597///
598/// Every differentiable gate is `exp(-iθG/2)` with `G` of eigenvalues `±1`, so
599/// `⟨H⟩` is a degree-1 trigonometric polynomial in each angle and
600/// `d⟨H⟩/dθ = (f(θ+π/2) - f(θ-π/2)) / 2` is exact. `P(θ) = diag(1, e^{iθ})` has
601/// the projector `|1⟩⟨1|` for a generator, eigenvalues `{0, 1}` rather than
602/// `{-1, +1}`, but `P(θ) = e^{iθ/2} Rz(θ)`: the θ-dependent factor is a scalar
603/// wherever the gate sits, so it cancels against its conjugate in `⟨ψ|H|ψ⟩` and
604/// the same shift applies unchanged.
605///
606/// Gates sharing a parameter slot are shifted one at a time and summed. Shifting
607/// them together is a different quantity: two `Rx(θ)` on one qubit under `⟨Z⟩`
608/// give `cos 2θ`, whose joint ±π/2 shift is zero rather than `-2 sin 2θ`.
609///
610/// Under `noise` every forward evaluation reads the exact mixture, so `kind`
611/// must be a density-matrix kind, which the caller checks. The channels do not
612/// depend on the shifted angle, so `⟨H⟩` stays a degree-1 trigonometric
613/// polynomial in it and the shift is still exact.
614pub(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        // Rx(θ)|0>, <Z> = cos θ, d/dθ = -sin θ.
698        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), &params, 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        // Ry(θ)|0>, <Z> = cos θ, d/dθ = -sin θ. Generator Y has num_y = 1.
712        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), &params, 42).unwrap();
719        assert!((g.gradient[0] - (-theta.sin())).abs() < 1e-9);
720    }
721
722    #[test]
723    fn phase_projector_gradient() {
724        // H then P(θ): |ψ> = (|0> + e^{iθ}|1>)/√2, <X> = cos θ, d/dθ = -sin θ.
725        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, &params, 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        // Two Rx gates on separate qubits sharing one slot; each contributes
741        // -sin θ to <Z0 + Z1>, so the shared gradient is -2 sin θ.
742        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, &params, 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), &params, 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        // Ry(theta)|0>, <X> = sin theta, d/dtheta = cos theta.
785        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, &params, 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        // Two independent Ry rotations under Z0, Z1 and Z0Z1: every term is
799        // diagonal, so nothing moves an amplitude.
800        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, &params, 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        // X0, Y0, X0Z1 and Y0Z2 all carry the X mask of qubit 0, four terms
821        // reading the same amplitude.
822        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, &params, 42).unwrap();
837        let shift = run_expectation_gradient_shift(&c, &obs, &params, 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        // 2^14 amplitudes, the width at which the gather fans out to Rayon.
847        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, &params, 42).unwrap();
862        let shift = run_expectation_gradient_shift(&c, &obs, &params, 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        // Sum the terms one at a time, the shape the gather replaces, and
872        // compare on one register. Split across two chunks so the gather's base
873        // arithmetic is covered as well.
874        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, &amp) 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        // The Rz layer and the Rzz chain of an Ising ansatz merge into one run:
988        // every generator is Z type, so nothing splits them.
989        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}