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>(()) }