1use 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#[derive(Debug, Clone, PartialEq)]
21pub enum ResultValue {
22 StateVector(Vec<Complex64>),
24 DensityMatrix(Vec<Vec<Complex64>>),
27 Amplitude(Vec<(String, Complex64)>),
29 Probability(Vec<f64>),
32 Expectation(Vec<f64>),
35 Variance(Vec<f64>),
37 Sample(Vec<Vec<f64>>),
40}
41
42type Requested = Vec<(PauliObservable, Option<(f64, PauliObservable)>)>;
45
46impl<'c> Simulate<'c, Seeded> {
47 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 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 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 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 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 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
350fn 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
386type Rotations = BTreeMap<Vec<usize>, Vec<Instruction>>;
389
390fn 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 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
433fn shot_value(group: &[MeasuredFactor], bits: &[bool]) -> f64 {
436 group
437 .iter()
438 .map(|factor| factor.eigenvalues[outcome(&factor.targets, bits)])
439 .product()
440}
441
442fn 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
466fn 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
483fn 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
506fn 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
525fn 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
535fn 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
558const CHUNK_BITS: usize = 11;
561
562fn 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}