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