Backends and execution¶
Simulation modes¶
One simulator, two execution modes:
tensornet(default): tensor-network contraction viaopt_einsum. Never materializes the full state unless asked to, so wide-but-shallow circuits scale far beyond dense simulation.statevector: dense statevector propagation.
Circuit(20).h[:].run() # tensornet (default)
Circuit(20).h[:].run(backend='statevector') # dense
Circuit(20).h[:].run(mode='statevector') # equivalent
Both modes agree numerically and both preserve autograd graphs.
Return values¶
c = Circuit(2).h[0].cx[0, 1]
c.run() # statevector (torch.Tensor)
c.statevector() # same, explicit
c.m[:].run(shots=100) # Counter of bitstrings
c.shots(100) # same, explicit
c.run(amplitude='11') # a single amplitude
c.m[:].oneshot() # (collapsed state, one outcome)
c.expect(hamiltonian) # <psi|H|psi>
c.probs([1]) # marginal probabilities
Large circuits¶
The dense state has 2**n entries. In tensornet mode, circuits with
more than 28 qubits require shots= or returns='amplitude' instead of
the full vector:
Circuit(50).h[:].run(shots=3)
Circuit(50).h[:].run(returns='amplitude', amplitude='0' * 50)
Sampling uses inverse-CDF search, so there is no category-count limit.
Noise applies to every entry point¶
noise= (and quasi_static=, noise_scale=) selects the density-matrix
backend, whichever way the run is written:
c.run(noise=nm, shots=1000)
c.shots(1000, noise=nm) # the same thing
c.probs(noise=nm) # probabilities from the density matrix
statevector() and oneshot() raise instead: a noisy state has no
statevector, and returning the noiseless one would be worse than an error.
Mid-circuit measurement¶
A measurement collapses the state, so anything acting on that qubit afterwards sees a classical bit. Circuits where that happens are run shot by shot as quantum trajectories, collapsing where the measurement is, and the reported bit is the one the measurement produced – not a value drawn from the final state after later gates have moved it. Measurements that nothing follows keep the fast single-pass path.
Reproducible sampling¶
seed= fixes every random draw a run makes – shot sampling, mid-circuit
collapse and large-n perfect sampling alike – so the same circuit and
seed always give the same counts:
c = Circuit(4).h[:]
c.run(shots=200, seed=42) == c.run(shots=200, seed=42) # True
The seed drives a private torch.Generator, not torch.manual_seed, so
seeding a circuit does not disturb the RNG the rest of your program uses.
Without seed=, runs stay random exactly as before.
Estimating from counts¶
A ratio of two small counts is not an estimate. With 30000 trials and zero failures, the failure rate is not 0 – it is “below roughly 1e-4”, and code that divides by it produces either 0 or a meaningless spike depending on which side of the fraction the zero lands on. Before dividing, decide a minimum count and say so:
failures = counts.get('1', 0)
if failures < 10:
rate = None # not enough events to estimate a ratio
else:
rate = failures / shots
The same applies to comparing two runs: a ratio of two rates each built from a handful of events carries essentially no information, however many total shots were taken.
Counts bit order¶
By default the leftmost character of a counts key is the highest-numbered
qubit, so key[-1] is qubit 0. Cloud APIs (including
qapi.blueqat.app) use the opposite layout; bit_order='q0_first'
returns keys in that order instead of leaving each caller to reverse them by
hand:
Circuit(3).x[0].run(shots=4) # Counter({'001': 4})
Circuit(3).x[0].run(shots=4, bit_order='q0_first') # Counter({'100': 4})
Keys are always zero-padded to exactly n_qubits characters, in both
orders – an unpadded '11' would be ambiguous between qubits 0 and 1 and
qubits 4 and 5 once reversed. apply_bit_order()
applies the same conversion to a Counter obtained elsewhere.
Mid-circuit measurement and reset¶
reset and keyed measurement make outcomes depend on when the collapse
happens, so such circuits automatically run shot-by-shot as quantum
trajectories, collapsing at each measure / reset:
Circuit(2).h[0].cx[0, 1].reset[0].m[:].run(shots=100)
Circuit().x[0].m(key='a')[0].run(shots=3, returns='samples')
# [{'a': [1]}, {'a': [1]}, {'a': [1]}]
Custom initial states¶
import torch
psi0 = torch.tensor([0, 1, 0, 0], dtype=torch.complex128)
Circuit(2).h[0].run(initial=psi0)
Other built-in backends¶
'draw'– matplotlib circuit diagram.'draw_tn'– the tensor-network graph of the circuit.'eo'– exchange-only transpiler (see Exchange-only spin qubits).'cloud'– cloud submission (see Cloud access).'1q_compaction'/'2q_decomposition'– transpilers merging single-qubit gates / rewriting two-qubit gates into a chosen basis.
Registering your own backend¶
from blueqat import register_backend, Backend
class MyBackend(Backend):
def run(self, gates, n_qubits, *args, **kwargs):
...
register_backend('mybackend', MyBackend)
Circuit(2).h[0].run(backend='mybackend')
Circuit(2).h[0].run_with_mybackend() # equivalent