Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Simulate and Sample

Goal: build a three-qubit GHZ circuit, read its exact distribution, sample it like hardware would, and run it on a backend of your choosing.

Build and run

from prism_q import CircuitBuilder, simulate

ghz = CircuitBuilder(3).h(0).cx(0, 1).cx(1, 2).build()
outcome = simulate(ghz).seed(42).run()
print(outcome.probabilities)       # [0.5 0.  0.  0.  0.  0.  0.  0.5]
print(outcome.metadata.backend)    # Stabilizer

probabilities is a NumPy array over the 2**3 basis states, with qubit 0 as the least significant bit of the index. Half the weight sits on |000> and half on |111>.

metadata.backend names the engine that ran. Nothing asked for a stabilizer tableau: the default BackendKind.auto() saw only Clifford gates and picked one.

Sample shots

Hardware returns measurement records, not amplitudes. Add classical bits, measure, and sample:

measured = CircuitBuilder(3, 3).h(0).cx(0, 1).cx(1, 2).measure_all().build()
counts = simulate(measured).seed(42).sample_counts(1000).counts()
print(sorted(counts.items()))      # [('000', 485), ('111', 515)]

shots = simulate(measured).seed(42).shots(5).shots
print(shots.shape, shots.dtype)    # (5, 3) bool

The seed fixes the draw, so the same seed gives the same counts on every run. In a count key, character i is classical bit i, so a key reads with bit 0 on the left:

flipped = CircuitBuilder(3, 3).x(0).measure_all().build()
print(simulate(flipped).seed(42).sample_counts(10).counts())   # {'100': 10}

Choose a backend

Pass a BackendKind to override the automatic choice. The answer stays the same; the representation, and so the cost, changes:

from prism_q import BackendKind

for kind in [BackendKind.statevector(), BackendKind.mps(16), BackendKind.sparse()]:
    result = simulate(ghz).backend(kind).seed(42).run()
    print(result.metadata.backend, result.probabilities[[0, 7]])
# Statevector [0.5 0.5]
# Mps [0.5 0.5]
# Sparse [0.5 0.5]

A backend that cannot hold the circuit says so instead of guessing. The product-state backend keeps one state per qubit and declines anything entangling:

from prism_q import PrismError

try:
    simulate(ghz).backend(BackendKind.product_state()).seed(42).run()
except PrismError as exc:
    print(exc.kind)                # incompatible_backend

Choosing a Backend has the dispatch tree and a table from circuit shape to backend.

In Rust

#![allow(unused)]
fn main() {
use prism_q::{BackendKind, CircuitBuilder, bitstring, simulate};

let ghz = CircuitBuilder::new(3).h(0).cx(0, 1).cx(1, 2).build();
let outcome = simulate(&ghz).seed(42).run()?;
let probs = outcome.probabilities.expect("no probabilities");
assert!((probs.get(0b000) - 0.5).abs() < 1e-12);

let measured = CircuitBuilder::new_with_classical(3, 3)
    .h(0)
    .cx(0, 1)
    .cx(1, 2)
    .measure_all()
    .build();
let counts = simulate(&measured).seed(42).sample_counts(1000)?;
for (key, n) in &counts.counts {
    println!("{}: {n}", bitstring(key, counts.num_classical_bits));
}

let sv = simulate(&ghz).backend(BackendKind::Statevector).seed(42).run()?;
println!("{:?}", sv.metadata.backend);
Ok::<(), prism_q::PrismError>(())
}

Next: Variational Circuits and Gradients.