Skip to content

Qiskit integration

The optional tensorweaver.qiskit module runs Qiskit circuits on the MPS engine. A QuantumCircuit is converted to a mimiq circuit and evolved, so your Qiskit code stays unchanged.

Install the extra:

pip install 'mimiq-tensorweaver[qiskit]'

Bit ordering

Qiskit's own convention applies to Qiskit objects: counts keys come back with the highest-indexed qubit on the left, the reverse of the little-endian ordering TensorWeaver uses natively. The conversion handles this, so read Qiskit results as Qiskit results.

Sampling

TensorWeaverBackend is a Qiskit BackendV2. Simulator options (bonddim, seed, algorithm, and the rest of the TwSimulator configuration) are fixed when the backend is built; run forwards only shots and seed.

from qiskit import QuantumCircuit
from tensorweaver.qiskit import TensorWeaverBackend

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

backend = TensorWeaverBackend(bonddim=64, seed=1)
counts = backend.run(qc, shots=4000).result().get_counts()

It also works under Qiskit's V2 primitives:

from qiskit.primitives import BackendSamplerV2

sampler = BackendSamplerV2(backend=backend)
result = sampler.run([qc], shots=4000).result()[0]
counts = result.data.meas.get_counts()

Expectation values

TensorWeaverEstimator is an EstimatorV2. Every Pauli term is read exactly from the MPS, whatever its weight, so there is no shot noise and the reported standard errors are zero. The only error is the MPS truncation itself, set by bonddim and the other simulator options, and reported as min_fidelity in the result metadata.

from qiskit import QuantumCircuit
from qiskit.quantum_info import SparsePauliOp
from tensorweaver.qiskit import TensorWeaverEstimator

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

obs = SparsePauliOp.from_list([("ZZI", 1.0), ("IIZ", 0.5), ("ZZZ", 0.25)])

estimator = TensorWeaverEstimator(bonddim=64)
result = estimator.run([(qc, obs)]).result()[0]
print(result.data.evs, result.data.stds)

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 read from.

Circuits that end in an ensemble

That exactness needs the circuit to end in one definite state. A mid-circuit measurement, a reset, a noise channel, or qubit loss makes TensorWeaver re-evolve the circuit once per shot, and each trajectory has its own expectation value. The quantity you want, Tr(rho O), is their average, so the estimator needs a budget to estimate it:

Argument What it does
trajectories=N Average N trajectories. Each contributes an exact value, so only the ensemble is sampled. stds is the standard error of the mean.
shots=N Estimate from measurements in rotated Pauli bases, N shots per basis, as hardware and Qiskit's BackendEstimatorV2 do. Terms that commute qubit-wise share a basis, so an observable needing several costs N shots in each.
precision=p Size either budget as ceil(1/p**2), Qiskit's convention.
method=... Force "exact", "trajectories", or "shots" instead of letting "auto" choose from the circuit.
qc = QuantumCircuit(1, 1)
qc.h(0)
qc.measure(0, 0)   # state preparation, not readout: the x follows it
qc.x(0)

obs = SparsePauliOp("Z")   # -1 or +1 per trajectory, 0 over the ensemble

averaged = TensorWeaverEstimator(trajectories=4000, seed=1)
result = averaged.run([(qc, obs)]).result()[0]
print(result.data.evs, "+/-", result.data.stds)

sampled = TensorWeaverEstimator(shots=4000, seed=1)   # the same quantity

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

For the same budget, averaging trajectories is never worse than sampling and is usually better, because sampling adds the observable's shot noise on top of the ensemble's, and it runs one basis rather than several. Reach for shots= when you need to reproduce what a shot-based estimator would report, not for accuracy.

Reading the metadata

{'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 MPS fidelity behind the pub

min_fidelity is the truncation error, not a statistical one. Averaging more trajectories does not improve it; raising bonddim does.

precision and default_precision do not change an exact value: a deterministic circuit has one answer and no shot budget to spend on it. To see plausible shot noise on such a circuit without paying for it, emulate_shot_noise=True adds Gaussian noise of width precision to the exact value, the way Qiskit's StatevectorEstimator does, for the cost of one evolution.

To reuse a configured simulator across both interfaces, pass it in rather than repeating the options:

from tensorweaver import TwSimulator
from tensorweaver.qiskit import TensorWeaverEstimator

sim = TwSimulator(bonddim=128, algorithm="vmpoa")
estimator = TensorWeaverEstimator(simulator=sim)

Gate coverage

Conversion covers the standard Qiskit gate set. Custom gates and gates with unbound parameters raise mimiq_qiskit.converter.UnsupportedGateError: bind the parameters and decompose custom gates before running.