Skip to main content

prism_q/sim/
braket.rs

1//! Evaluating the result requests a Braket program declared, exactly or from a
2//! shot record.
3//!
4//! Values come back in Braket's conventions, where qubit 0 is the most
5//! significant bit of a basis index and `q[0]` is the least significant one
6//! here, so an exported vector is reversed on the way out.
7
8use std::collections::{BTreeMap, HashMap};
9
10use num_complex::Complex64;
11
12use crate::circuit::Instruction;
13use crate::circuit::braket::{MeasuredFactor, ObservableFactor, ResultSpec, Targets};
14use crate::error::{PrismError, Result};
15use crate::sim::observable::PauliObservable;
16use crate::sim::unified_pauli::PauliTerm;
17use crate::sim::{Seeded, Simulate};
18
19/// One computed `#pragma braket result`, in Braket's conventions.
20#[derive(Debug, Clone, PartialEq)]
21#[non_exhaustive]
22pub enum ResultValue {
23    /// Amplitudes with qubit 0 in the most significant bit.
24    StateVector(Vec<Complex64>),
25    /// Row major over the requested targets, `targets[0]` the most significant
26    /// bit of both indices.
27    DensityMatrix(Vec<Vec<Complex64>>),
28    /// One amplitude per requested basis state, in the order requested.
29    Amplitude(Vec<(String, Complex64)>),
30    /// Joint probabilities over the requested targets, `targets[0]` the most
31    /// significant bit.
32    Probability(Vec<f64>),
33    /// One value, or one per qubit in qubit order when the observable named no
34    /// targets.
35    Expectation(Vec<f64>),
36    /// `<O^2> - <O>^2`, shaped like [`ResultValue::Expectation`].
37    Variance(Vec<f64>),
38    /// One eigenvalue per shot, in one series per value
39    /// [`ResultValue::Expectation`] would report.
40    Sample(Vec<Vec<f64>>),
41}
42
43/// The Pauli sums one result request reports, each beside what a variance
44/// needs: the constant term held out of the square, and the square of the rest.
45type Requested = Vec<(PauliObservable, Option<(f64, PauliObservable)>)>;
46
47impl<'c> Simulate<'c, Seeded> {
48    /// Evaluate the result requests a Braket program declared.
49    ///
50    /// Every `expectation` and `variance` request is served by a single
51    /// traversal: their observables are lowered to Pauli sums, the distinct
52    /// strings across all of them are evaluated together through
53    /// [`Simulate::expectation_values`], and each requested value is then a
54    /// weighted sum over that one evaluation. `state_vector` and `amplitude`
55    /// share one export, and a `probability` or `density_matrix` request beside
56    /// them is read off that same export. Without one it runs on its own,
57    /// keeping the routing its own width earns: a subset marginal of a wide
58    /// Clifford circuit stays on the tableau rather than forcing a dense
59    /// export.
60    ///
61    /// `sample` is declined here: it reports per-shot eigenvalues, which
62    /// [`Simulate::braket_results_sampled`] answers. So is a circuit carrying a
63    /// measurement, reset or conditional, which has no one exact output state
64    /// to read: Braket rejects the same programs at zero shots.
65    ///
66    /// # Errors
67    /// Returns `BackendUnsupported` for a `sample` request,
68    /// `IncompatibleBackend` for a circuit that is not unitary, and whatever
69    /// the underlying terminal returns for a route that cannot serve a request.
70    pub fn braket_results(self, specs: &[ResultSpec]) -> Result<Vec<ResultValue>> {
71        let num_qubits = self.circuit.num_qubits;
72        crate::sim::require_unitary_circuit(
73            &self.kind,
74            self.circuit,
75            "an exact result request reads",
76        )?;
77        let requested = specs
78            .iter()
79            .map(|spec| observables_of(spec, num_qubits))
80            .collect::<Result<Vec<_>>>()?;
81
82        let mut index: BTreeMap<Vec<PauliTerm>, usize> = BTreeMap::new();
83        for sums in requested.iter().flatten() {
84            for (sum, squared) in sums {
85                let squared = squared.as_ref().map(|(_, square)| square);
86                for source in [Some(sum), squared].into_iter().flatten() {
87                    for (_, string) in source.terms() {
88                        if !string.is_empty() {
89                            let next = index.len();
90                            index.entry(string.clone()).or_insert(next);
91                        }
92                    }
93                }
94            }
95        }
96        let values = if index.is_empty() {
97            Vec::new()
98        } else {
99            let mut strings = vec![Vec::new(); index.len()];
100            for (string, &slot) in &index {
101                strings[slot] = string.clone();
102            }
103            self.fork().expectation_values(&strings)?
104        };
105
106        let mut state = None;
107        if specs
108            .iter()
109            .any(|spec| matches!(spec, ResultSpec::StateVector | ResultSpec::Amplitude(_)))
110        {
111            self.state_once(&mut state)?;
112        }
113        let mut computed = Vec::with_capacity(specs.len());
114        for (spec, sums) in specs.iter().zip(&requested) {
115            computed.push(match (spec, sums) {
116                (ResultSpec::StateVector, _) => {
117                    let amplitudes = self.state_once(&mut state)?;
118                    ResultValue::StateVector(reverse_state(amplitudes, num_qubits))
119                }
120                (ResultSpec::Amplitude(states), _) => {
121                    let amplitudes = self.state_once(&mut state)?;
122                    ResultValue::Amplitude(
123                        states
124                            .iter()
125                            .map(|label| {
126                                let at = basis_index(label, num_qubits)?;
127                                Ok((label.clone(), amplitudes[at]))
128                            })
129                            .collect::<Result<Vec<_>>>()?,
130                    )
131                }
132                (ResultSpec::Probability(targets), _) => {
133                    let targets = resolve(targets, num_qubits);
134                    let joint = match state.as_deref() {
135                        Some(amplitudes) => {
136                            crate::backend::schmidt::validate_qubit_set(&targets, num_qubits)?;
137                            marginal_of(amplitudes, &targets, num_qubits)
138                        }
139                        None => self.fork().probabilities_of(&targets)?,
140                    };
141                    ResultValue::Probability(reverse_state(&joint, targets.len()))
142                }
143                (ResultSpec::DensityMatrix(targets), _) => {
144                    let targets = resolve(targets, num_qubits);
145                    let data = match state.as_deref() {
146                        Some(amplitudes) => {
147                            crate::backend::schmidt::validate_qubit_set(&targets, num_qubits)?;
148                            reduced_of(amplitudes, &targets, num_qubits)
149                        }
150                        None => self.fork().reduced_density_matrix(&targets)?.data,
151                    };
152                    ResultValue::DensityMatrix(reverse_matrix(&data, targets.len()))
153                }
154                (ResultSpec::Expectation(_), Some(sums)) => ResultValue::Expectation(
155                    sums.iter()
156                        .map(|(sum, _)| weighted(sum, &index, &values))
157                        .collect(),
158                ),
159                (ResultSpec::Variance(_), Some(sums)) => ResultValue::Variance(
160                    sums.iter()
161                        .map(|(sum, squared)| {
162                            let (offset, square) =
163                                squared.as_ref().expect("a variance carries its square");
164                            let centered = weighted(sum, &index, &values) - offset;
165                            weighted(square, &index, &values) - centered * centered
166                        })
167                        .collect(),
168                ),
169                (spec, _) => {
170                    return Err(PrismError::BackendUnsupported {
171                        backend: format!("{:?}", self.kind),
172                        operation: format!(
173                            "`{}`, which reports per-shot eigenvalues and so needs measurement \
174                             in the observable's own basis rather than an exact value",
175                            spec.name()
176                        ),
177                    });
178                }
179            });
180        }
181        Ok(computed)
182    }
183
184    /// Evaluate the result requests a Braket program declared, from a shot
185    /// record rather than from the exact state.
186    ///
187    /// Each observable is diagonalized and the rotations carrying them onto the
188    /// computational basis are appended to the circuit once, so a single
189    /// sampling pass answers every `sample`, `expectation` and `variance`
190    /// request together. Two observables reading one qubit in different bases
191    /// cannot share a record and are rejected rather than answered from
192    /// whichever basis was applied first. A `probability` request reads the
193    /// computational basis and so takes its own unrotated pass whenever any
194    /// rotation was applied.
195    ///
196    /// # Errors
197    /// Returns `BackendUnsupported` for `state_vector`, `density_matrix` and
198    /// `amplitude`, which report the state itself and which Braket admits only
199    /// at zero shots, and `InvalidParameter` for zero shots or for observables
200    /// that cannot share one measurement.
201    pub fn braket_results_sampled(
202        self,
203        specs: &[ResultSpec],
204        shots: usize,
205    ) -> Result<Vec<ResultValue>> {
206        let num_qubits = self.circuit.num_qubits;
207        if shots == 0 {
208            return Err(PrismError::InvalidParameter {
209                message: "a shot-based evaluation needs at least one shot".into(),
210            });
211        }
212        if let Some(spec) = specs.iter().find(|spec| spec.requires_exact()) {
213            return Err(PrismError::BackendUnsupported {
214                backend: format!("{:?}", self.kind),
215                operation: format!(
216                    "`{}` above zero shots, since it reports the state itself rather than a \
217                     measurement of it",
218                    spec.name()
219                ),
220            });
221        }
222
223        let measured = specs
224            .iter()
225            .map(|spec| match spec {
226                ResultSpec::Expectation(observable)
227                | ResultSpec::Variance(observable)
228                | ResultSpec::Sample(observable) => observable.diagonalize(num_qubits).map(Some),
229                _ => Ok(None),
230            })
231            .collect::<Result<Vec<_>>>()?;
232
233        let rotations = merge_rotations(&measured)?;
234        let record = self.sample_record(&rotations, shots)?;
235        let unrotated = if rotations.is_empty()
236            || !specs
237                .iter()
238                .any(|spec| matches!(spec, ResultSpec::Probability(_)))
239        {
240            None
241        } else {
242            Some(self.sample_record(&Rotations::new(), shots)?)
243        };
244
245        let series = |groups: &[Vec<MeasuredFactor>]| -> Vec<Vec<f64>> {
246            groups
247                .iter()
248                .map(|group| record.iter().map(|bits| shot_value(group, bits)).collect())
249                .collect()
250        };
251        let mut computed = Vec::with_capacity(specs.len());
252        for (spec, groups) in specs.iter().zip(&measured) {
253            computed.push(match (spec, groups) {
254                (ResultSpec::Sample(_), Some(groups)) => ResultValue::Sample(series(groups)),
255                (ResultSpec::Expectation(_), Some(groups)) => ResultValue::Expectation(
256                    series(groups).iter().map(|values| mean(values)).collect(),
257                ),
258                (ResultSpec::Variance(_), Some(groups)) => ResultValue::Variance(
259                    series(groups)
260                        .iter()
261                        .map(|values| variance(values))
262                        .collect(),
263                ),
264                (ResultSpec::Probability(targets), _) => {
265                    let targets = resolve(targets, num_qubits);
266                    crate::backend::schmidt::validate_qubit_set(&targets, num_qubits)?;
267                    if targets.len() > crate::backend::schmidt::export_cap() {
268                        return Err(crate::backend::schmidt::export_cap_exceeded(
269                            &format!("{:?}", self.kind),
270                            "a probability histogram".to_string(),
271                            targets.len(),
272                        ));
273                    }
274                    let source = unrotated.as_ref().unwrap_or(&record);
275                    ResultValue::Probability(histogram(&targets, source))
276                }
277                (spec, _) => unreachable!("`{}` was screened above", spec.name()),
278            });
279        }
280        Ok(computed)
281    }
282
283    /// One shot record of every qubit, taken after `rotations`, with only the
284    /// bits this call appended.
285    ///
286    /// An attached noise model is indexed per instruction, so it is extended
287    /// with empty slots over the appended rotation and readout: the basis
288    /// change is a reading device rather than part of the program, and the
289    /// program's own readout error stays on the bits it was declared for.
290    fn sample_record(&self, rotations: &Rotations, shots: usize) -> Result<Vec<Vec<bool>>> {
291        let num_qubits = self.circuit.num_qubits;
292        let mut circuit = self.circuit.clone();
293        for instrs in rotations.values() {
294            circuit.instructions.extend(instrs.iter().cloned());
295        }
296        let base = circuit.num_classical_bits;
297        circuit.num_classical_bits = base + num_qubits;
298        for qubit in 0..num_qubits {
299            circuit.add_measure(qubit, base + qubit);
300        }
301        let extended = self.noise_model.map(|model| {
302            let mut model = model.clone();
303            model
304                .after_gate
305                .resize(circuit.instructions.len(), Vec::new());
306            model.readout.resize(circuit.num_classical_bits, None);
307            model
308        });
309        let sampled = Simulate::<Seeded> {
310            circuit: &circuit,
311            kind: self.kind.clone(),
312            seed: self.seed,
313            noise_model: extended.as_ref(),
314            initial_state: self.initial_state,
315            require_exact: self.require_exact,
316        }
317        .shots(shots)?;
318        // The appended readout sits at the end of the record, so trimming in
319        // place leaves the bits this call wrote without copying every shot.
320        Ok(sampled
321            .shots
322            .into_iter()
323            .map(|mut bits| {
324                bits.truncate(base + num_qubits);
325                bits.drain(..base);
326                bits
327            })
328            .collect())
329    }
330
331    /// Export the output state, reusing an earlier export within the same call.
332    fn state_once<'s>(&self, cache: &'s mut Option<Vec<Complex64>>) -> Result<&'s [Complex64]> {
333        if cache.is_none() {
334            *cache = Some(self.fork().state_vector()?);
335        }
336        Ok(cache.as_deref().expect("just filled"))
337    }
338
339    /// A copy of the request, so one builder can drive several terminals.
340    fn fork(&self) -> Simulate<'c, Seeded> {
341        Simulate {
342            circuit: self.circuit,
343            kind: self.kind.clone(),
344            seed: self.seed,
345            noise_model: self.noise_model,
346            initial_state: self.initial_state,
347            require_exact: self.require_exact,
348        }
349    }
350}
351
352/// Pauli sums a request needs, `None` for a request that reads the state
353/// rather than an observable.
354fn observables_of(spec: &ResultSpec, num_qubits: usize) -> Result<Option<Requested>> {
355    let (observable, squared) = match spec {
356        ResultSpec::Expectation(observable) => (observable, false),
357        ResultSpec::Variance(observable) => (observable, true),
358        _ => return Ok(None),
359    };
360    Ok(Some(
361        observable
362            .lower(num_qubits)?
363            .into_iter()
364            .map(|(_, sum)| {
365                let square = squared.then(|| {
366                    let (offset, traceless) = sum.split_identity();
367                    (offset, traceless.square())
368                });
369                (sum, square)
370            })
371            .collect(),
372    ))
373}
374
375fn weighted(sum: &PauliObservable, index: &BTreeMap<Vec<PauliTerm>, usize>, values: &[f64]) -> f64 {
376    sum.terms()
377        .iter()
378        .map(|(coefficient, string)| {
379            if string.is_empty() {
380                *coefficient
381            } else {
382                coefficient * values[index[string]]
383            }
384        })
385        .sum()
386}
387
388/// The rotation each measured qubit set takes before it is read, keyed by the
389/// qubits so the order a circuit receives them in is fixed.
390type Rotations = BTreeMap<Vec<usize>, Vec<Instruction>>;
391
392/// One rotation per qubit set, rejecting observables that cannot share a
393/// measurement: two reading a qubit in different bases, or reading overlapping
394/// but unequal qubit sets.
395fn merge_rotations(measured: &[Option<Vec<Vec<MeasuredFactor>>>]) -> Result<Rotations> {
396    let conflict = |qubit: usize| PrismError::InvalidParameter {
397        message: format!(
398            "two observables read qubit {qubit} in different bases, which one measurement cannot \
399             serve; request them separately"
400        ),
401    };
402    let mut rotations = Rotations::new();
403    let mut bases: BTreeMap<Vec<usize>, ObservableFactor> = BTreeMap::new();
404    let mut claimed: HashMap<usize, Vec<usize>> = HashMap::new();
405    for factor in measured.iter().flatten().flatten().flatten() {
406        let Some(gates) = &factor.rotation else {
407            continue;
408        };
409        for &qubit in &factor.targets {
410            match claimed.get(&qubit) {
411                Some(owner) if *owner != factor.targets => return Err(conflict(qubit)),
412                Some(_) => {}
413                None => {
414                    claimed.insert(qubit, factor.targets.clone());
415                }
416            }
417        }
418        // Two factors on the same qubits share a measurement only when they
419        // read the same basis, which the factor itself decides rather than the
420        // instructions it happened to lower to.
421        match bases.get(&factor.targets) {
422            Some(existing) if *existing != factor.basis => {
423                return Err(conflict(factor.targets[0]));
424            }
425            Some(_) => {}
426            None => {
427                bases.insert(factor.targets.clone(), factor.basis.clone());
428                rotations.insert(factor.targets.clone(), gates.clone());
429            }
430        }
431    }
432    Ok(rotations)
433}
434
435/// Eigenvalue one shot gives a tensor product: the product over its factors of
436/// the eigenvalue each reads.
437fn shot_value(group: &[MeasuredFactor], bits: &[bool]) -> f64 {
438    group
439        .iter()
440        .map(|factor| factor.eigenvalues[outcome(&factor.targets, bits)])
441        .product()
442}
443
444/// Basis index measured bits give a target list, `targets[0]` the most
445/// significant, which is how Braket and a gate matrix both pack one.
446fn outcome(targets: &[usize], bits: &[bool]) -> usize {
447    targets.iter().fold(0usize, |index, &qubit| {
448        index << 1 | usize::from(bits[qubit])
449    })
450}
451
452fn histogram(targets: &[usize], record: &[Vec<bool>]) -> Vec<f64> {
453    let mut counts = vec![0.0f64; 1usize << targets.len()];
454    for bits in record {
455        counts[outcome(targets, bits)] += 1.0;
456    }
457    let shots = record.len() as f64;
458    for count in &mut counts {
459        *count /= shots;
460    }
461    counts
462}
463
464fn mean(values: &[f64]) -> f64 {
465    values.iter().sum::<f64>() / values.len() as f64
466}
467
468/// Population variance, the estimator Braket reports for a shot series.
469fn variance(values: &[f64]) -> f64 {
470    let mean = mean(values);
471    values
472        .iter()
473        .map(|value| (value - mean) * (value - mean))
474        .sum::<f64>()
475        / values.len() as f64
476}
477
478fn resolve(targets: &Targets, num_qubits: usize) -> Vec<usize> {
479    match targets {
480        Targets::All => (0..num_qubits).collect(),
481        Targets::These(qubits) => qubits.clone(),
482    }
483}
484
485/// Index of the basis state a Braket bitstring names: the leftmost character is
486/// qubit 0, which is the lowest bit of a PRISM-Q basis index.
487fn basis_index(label: &str, num_qubits: usize) -> Result<usize> {
488    if label.len() != num_qubits {
489        return Err(PrismError::InvalidParameter {
490            message: format!(
491                "basis state `{label}` names {} qubit(s) of {num_qubits}",
492                label.len()
493            ),
494        });
495    }
496    label
497        .chars()
498        .enumerate()
499        .try_fold(0usize, |index, (qubit, bit)| match bit {
500            '0' => Ok(index),
501            '1' => Ok(index | 1 << qubit),
502            other => Err(PrismError::InvalidParameter {
503                message: format!("basis state `{label}` has `{other}` where a bit belongs"),
504            }),
505        })
506}
507
508/// Basis index `targets` reads out of a full index, `targets[0]` the lowest
509/// bit, beside the index the untargeted qubits carry in their own order.
510fn split_index(index: usize, targets: &[usize], num_qubits: usize) -> (usize, usize) {
511    let mut read = 0usize;
512    for (bit, &qubit) in targets.iter().enumerate() {
513        read |= (index >> qubit & 1) << bit;
514    }
515    let mut rest = 0usize;
516    let mut bit = 0usize;
517    for qubit in 0..num_qubits {
518        if targets.contains(&qubit) {
519            continue;
520        }
521        rest |= (index >> qubit & 1) << bit;
522        bit += 1;
523    }
524    (read, rest)
525}
526
527/// Joint distribution over `targets` read off an exported state, shaped like
528/// [`Simulate::probabilities_of`].
529fn marginal_of(amplitudes: &[Complex64], targets: &[usize], num_qubits: usize) -> Vec<f64> {
530    let mut joint = vec![0.0f64; 1usize << targets.len()];
531    for (index, amplitude) in amplitudes.iter().enumerate() {
532        joint[split_index(index, targets, num_qubits).0] += amplitude.norm_sqr();
533    }
534    joint
535}
536
537/// Reduced density matrix of `targets` read off an exported state, shaped like
538/// [`ReducedDensityMatrix::data`](crate::ReducedDensityMatrix::data).
539fn reduced_of(amplitudes: &[Complex64], targets: &[usize], num_qubits: usize) -> Vec<Complex64> {
540    let side = 1usize << targets.len();
541    let mut blocks = vec![Complex64::new(0.0, 0.0); amplitudes.len()];
542    for (index, amplitude) in amplitudes.iter().enumerate() {
543        let (read, rest) = split_index(index, targets, num_qubits);
544        blocks[rest * side + read] = *amplitude;
545    }
546    let mut reduced = vec![Complex64::new(0.0, 0.0); side * side];
547    for block in blocks.chunks(side) {
548        for row in 0..side {
549            if block[row] == Complex64::new(0.0, 0.0) {
550                continue;
551            }
552            for column in 0..side {
553                reduced[row * side + column] += block[row] * block[column].conj();
554            }
555        }
556    }
557    reduced
558}
559
560/// Width of one table-driven reversal step. A table of this many bits costs
561/// 4 KiB and covers a 33-qubit index in three lookups.
562const CHUNK_BITS: usize = 11;
563
564/// Reversal of every `CHUNK_BITS`-bit value, so reversing a whole index costs
565/// one lookup per chunk rather than one pass per bit.
566fn chunk_table() -> Vec<u16> {
567    (0..1u32 << CHUNK_BITS)
568        .map(|value| {
569            (0..CHUNK_BITS).fold(0u16, |acc, bit| {
570                acc | ((value as u16 >> bit) & 1) << (CHUNK_BITS - 1 - bit)
571            })
572        })
573        .collect()
574}
575
576fn reverse_bits_with(table: &[u16], index: usize, width: usize) -> usize {
577    let mut reversed = 0usize;
578    let mut remaining = width;
579    let mut rest = index;
580    while remaining > 0 {
581        let take = remaining.min(CHUNK_BITS);
582        let chunk = rest & ((1usize << take) - 1);
583        reversed |= ((table[chunk] >> (CHUNK_BITS - take)) as usize) << (remaining - take);
584        rest >>= take;
585        remaining -= take;
586    }
587    reversed
588}
589
590fn reverse_state<T: Copy>(values: &[T], width: usize) -> Vec<T> {
591    let table = chunk_table();
592    (0..values.len())
593        .map(|index| values[reverse_bits_with(&table, index, width)])
594        .collect()
595}
596
597fn reverse_matrix(data: &[Complex64], width: usize) -> Vec<Vec<Complex64>> {
598    let side = 1usize << width;
599    let table = chunk_table();
600    let reversed: Vec<usize> = (0..side)
601        .map(|index| reverse_bits_with(&table, index, width))
602        .collect();
603    reversed
604        .iter()
605        .map(|&source| {
606            reversed
607                .iter()
608                .map(|&column| data[source * side + column])
609                .collect()
610        })
611        .collect()
612}