prism-q 0.35.0

Fast Rust quantum circuit simulator. OpenQASM 3.0, multiple backends, AVX2 SIMD kernels, optional CUDA and MPI, QEC tooling, Python bindings.
Documentation
# 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

```python
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:

```python
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:

```python
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:

```python
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:

```python
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](../getting-started/choosing-a-backend.md) has the dispatch tree and
a table from circuit shape to backend.

## In Rust

```rust
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](./variational.md).