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:
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.