Primitives

Qiskit’s V2 primitives are the standard interface for sampling and expectation-value workloads. mimiq-qiskit ships native implementations that talk to MIMIQ directly, rather than relying on Qiskit’s generic BackendSamplerV2 / BackendEstimatorV2 wrappers.

The native primitives differ from the generic wrappers in two ways:

  • MimiqEstimatorV2 reads each Pauli term straight off the simulator state. The generic estimator can only estimate a term by sampling in a rotated basis; MIMIQ evaluates \(\langle \psi | P | \psi \rangle\) directly, so a deterministic circuit carries no shot noise at any term weight.

  • Both primitives submit every circuit in a pub as a single MIMIQ job instead of one round trip per circuit, and the estimator goes further: observables asked for at the same parameter binding are read from one evolution rather than one each.

Sampling

from qiskit import QuantumCircuit
from mimiqlink import MimiqConnection
from mimiq_qiskit import MimiqBackend, MimiqSamplerV2

conn = MimiqConnection(); conn.connect()
sampler = MimiqSamplerV2(MimiqBackend(conn))

qc = QuantumCircuit(2)
qc.h(0)
qc.cx(0, 1)
qc.measure_all()

result = sampler.run([qc], shots=1000).result()
# Per-register bit arrays; ``measure_all`` writes the "meas" register.
print(result[0].data.meas.get_counts())

Estimating expectation values

from qiskit import QuantumCircuit
from qiskit.quantum_info import SparsePauliOp
from mimiq_qiskit import MimiqBackend, MimiqEstimatorV2

estimator = MimiqEstimatorV2(MimiqBackend(conn))

qc = QuantumCircuit(2)
qc.h(0)
qc.cx(0, 1)

observable = SparsePauliOp(["ZZ", "XX"], [1.0, 0.5])
result = estimator.run([(qc, observable)]).result()
print(result[0].data.evs)   # exact expectation value

Parameterised circuits broadcast the usual way: pass an array of bindings as the third pub element, and the result’s evs / bit arrays take the broadcast shape.

An estimator pub carries state preparation, not readout, so a measurement left at the end of the circuit is dropped rather than collapsing the state the observable is about to be read from.

Circuits that end in an ensemble

The exactness above holds for a circuit that ends in one definite state. A circuit with a mid-circuit measurement, a reset, a noise channel, or qubit loss does not: MIMIQ re-evolves it once per shot, and each of those trajectories has its own expectation value. The quantity you want, \(\mathrm{Tr}(\rho O)\), is their average.

So the expectation value of such a circuit is a statistical estimate, and the estimator needs a budget for it. Three methods are available, selected by what you pass:

trajectories=N

Average N trajectories. Each contributes an exact value, so only the ensemble is sampled, and stds reports the standard error of the mean. The most accurate use of a given budget.

shots=N

Estimate from measurements in rotated bases, N shots per basis, the way hardware and Qiskit’s BackendEstimatorV2 do. Correct for any circuit, and the right choice for comparing against a shot-based reference, but for the same N it is noisier than averaging trajectories, because it samples the observable and the ensemble. Terms that commute qubit-wise share a basis, so an observable needing several bases costs N shots in each.

precision=p

Size either of the above automatically, as ceil(1/p**2), following Qiskit’s convention.

qc = QuantumCircuit(1, 1)
qc.h(0)
qc.measure(0, 0)     # state preparation, not readout: the x follows it
qc.x(0)

observable = SparsePauliOp(["Z"])

# <Z> is -1 or +1 on any one trajectory, and 0 over the ensemble.
averaged = MimiqEstimatorV2(backend, trajectories=4000)
result = averaged.run([(qc, observable)]).result()[0]
print(result.data.evs, "+/-", result.data.stds)

# The same quantity, estimated the way a device would.
sampled = MimiqEstimatorV2(backend, shots=4000)

Given no budget, a stochastic circuit raises rather than returning one trajectory as though it were exact. method="exact" forces the single-trajectory read anyway, with a warning: that value is an unbiased draw, but its spread is the observable’s own range, not a small error.

A noise model passed through run_options never appears in the circuit, so the estimator cannot see it in the instructions. Any run carrying noisemodel is therefore treated as stochastic.

Reading the metadata

Each pub result says how its numbers were produced:

{'target_precision': 0.0,
 'method': 'trajectories',   # or 'exact', 'shots'
 'exact': False,             # True only with no statistical error at all
 'stochastic': True,         # one evolution per shot was needed
 'trajectories': 4000,       # or 'shots': N
 'min_fidelity': 0.998}      # lowest simulator fidelity behind the pub

min_fidelity is the simulator’s own error, not a statistical one. On the MPS backend it is the truncation fidelity, which averaging more trajectories does not improve; raise bonddim for that.

Emulating shot noise cheaply

To see plausible shot noise on a deterministic circuit without paying a shot budget, emulate_shot_noise=True adds Gaussian noise of width precision to the exact value, as Qiskit’s StatevectorEstimator does. It costs one evolution. The flag is ignored when the value already carries a real statistical error, since synthetic noise on top of that would misreport the total.

estimator = MimiqEstimatorV2(backend, emulate_shot_noise=True, seed=7)
result = estimator.run([(qc, observable)], precision=0.02).result()[0]
print(result.data.evs, "+/-", result.data.stds)   # stds == 0.02