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)]
21#[non_exhaustive]
22pub enum ResultValue {
23 StateVector(Vec<Complex64>),
25 DensityMatrix(Vec<Vec<Complex64>>),
28 Amplitude(Vec<(String, Complex64)>),
30 Probability(Vec<f64>),
33 Expectation(Vec<f64>),
36 Variance(Vec<f64>),
38 Sample(Vec<Vec<f64>>),
41}
42
43type Requested = Vec<(PauliObservable, Option<(f64, PauliObservable)>)>;
46
47impl<'c> Simulate<'c, Seeded> {
48 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 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 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 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 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 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
352fn 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
388type Rotations = BTreeMap<Vec<usize>, Vec<Instruction>>;
391
392fn 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 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
435fn shot_value(group: &[MeasuredFactor], bits: &[bool]) -> f64 {
438 group
439 .iter()
440 .map(|factor| factor.eigenvalues[outcome(&factor.targets, bits)])
441 .product()
442}
443
444fn 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
468fn 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
485fn 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
508fn 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
527fn 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
537fn 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
560const CHUNK_BITS: usize = 11;
563
564fn 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}