A complete example on a local simulator¶
Everything in this package works the same against the MIMIQ cloud and
against a local MIMIQ simulator; only the argument you hand
MimiqBackend differs. This page runs the whole
surface — counts, both primitives, mid-circuit measurement, transpilation
— against mimiq-exaqt, a
state-vector simulator published on PyPI, so every snippet below runs from
a fresh environment with no credentials and no network.
Working locally is also how you check a result you do not trust: a small
circuit has a Qiskit reference (Statevector, Operator) to compare
against, and the last section does exactly that.
pip install mimiq-qiskit mimiq-exaqt
Setting up the backend¶
ExaqtQCS is a mimiqcircuits LocalBackend, which is one of the
shapes MimiqBackend accepts (see
Wrapping any MIMIQ backend):
from exaqt import ExaqtQCS
from mimiq_qiskit import MimiqBackend
backend = MimiqBackend(ExaqtQCS(), name="exaqt", num_qubits=24)
num_qubits is the width advertised to Qiskit’s transpiler, not a limit
on run: a simulator is bounded by memory, not by a coupling map. Set it
high enough to cover the circuits you intend to transpile against the
backend.
Counts from backend.run¶
The plain path returns a qiskit.result.Result, exactly as an Aer or IBM
backend would:
from qiskit import QuantumCircuit
qc = QuantumCircuit(2, 2)
qc.h(0)
qc.cx(0, 1)
qc.measure([0, 1], [0, 1])
result = backend.run(qc, shots=1000, seed=42).result()
print(result.get_counts())
{'11': 474, '00': 526}
seed is forwarded to MIMIQ, so a seeded run repeats exactly. Note that
backend.run returns as soon as the job is submitted — the work happens
on a background thread and result() is the blocking call. That matters
much more against the cloud than against a local simulator, but the shape
of the API is the same either way.
Whatever the simulator reports about the run comes back as result metadata:
metadata = result.results[0].metadata
print(metadata["simulator"], metadata["simulator_version"])
print(sorted(metadata["timings"]))
print(metadata["fidelities"])
Exaqt 0.2.0
['apply', 'compile', 'sample', 'total']
[1.0]
A state-vector simulator is exact, hence the fidelity of 1.0. An
approximate backend such as MIMIQ’s MPS engine reports a real estimate
here, which is the number to watch when you raise bonddim.
Sampling with the native primitive¶
MimiqSamplerV2 reads MIMIQ’s sampled bitstrings
into one BitArray per classical
register, so multi-register circuits come back already split:
from qiskit import ClassicalRegister, QuantumCircuit, QuantumRegister
from mimiq_qiskit import MimiqSamplerV2
qr = QuantumRegister(3, "q")
alice = ClassicalRegister(2, "alice")
bob = ClassicalRegister(1, "bob")
circuit = QuantumCircuit(qr, alice, bob)
circuit.h(0)
circuit.cx(0, 1)
circuit.x(2)
circuit.measure(qr[0], alice[0])
circuit.measure(qr[1], alice[1])
circuit.measure(qr[2], bob[0])
data = MimiqSamplerV2(backend, seed=7).run([circuit], shots=1000).result()[0].data
print("alice:", data.alice.get_counts())
print("bob :", data.bob.get_counts())
alice: {'00': 478, '11': 522}
bob : {'1': 1000}
Each register is addressed by name on the result’s data. Alongside
get_counts() a BitArray also gives you the raw per-shot array, if
you would rather post-process the shots than a histogram.
Exact expectation values¶
MimiqEstimatorV2 does not sample. It reads each
Pauli term off the state itself, so the answer carries no shot noise and
the reported standard deviations are zero:
from qiskit.quantum_info import SparsePauliOp
from mimiq_qiskit import MimiqEstimatorV2
psi = QuantumCircuit(2)
psi.ry(0.7, 0)
psi.cx(0, 1)
estimator = MimiqEstimatorV2(backend)
pub_result = estimator.run([(psi, SparsePauliOp(["ZZ", "XX"], [1.0, 0.5]))]).result()[0]
print(pub_result.data.evs, pub_result.data.stds)
1.3221088436188455 0.0
That “no shot noise” is not a cosmetic difference. It means a scan over a parameter gives a smooth curve you can descend, instead of a noisy one you have to average out. Here is a two-qubit Hamiltonian, a one-parameter ansatz, and a coarse-to-fine scan that closes on the ground state:
import numpy as np
from qiskit.circuit import Parameter
hamiltonian = SparsePauliOp(
["II", "ZI", "IZ", "ZZ", "XX"],
[-1.05, 0.40, -0.40, 0.18, 0.18],
)
theta = Parameter("theta")
ansatz = QuantumCircuit(2)
ansatz.x(0)
ansatz.ry(theta, 1)
ansatz.cx(1, 0)
estimator = MimiqEstimatorV2(backend)
def scan(grid):
"""One estimator job covering every angle in ``grid``."""
evs = estimator.run(
[(ansatz, hamiltonian, [[angle] for angle in grid])]
).result()[0].data.evs
best = int(np.argmin(evs))
return grid[best], float(evs[best])
low, high = -np.pi, np.pi
for step in range(4):
grid = np.linspace(low, high, 21)
angle, energy = scan(grid)
print(f"pass {step + 1}: E = {energy:+.9f} at theta = {angle:+.6f}")
width = (high - low) / 20
low, high = angle - width, angle + width
exact = min(np.linalg.eigvalsh(hamiltonian.to_matrix()).real)
print(f"exact ground state: {exact:+.9f}")
pass 1: E = -2.046468272 at theta = -2.827433
pass 2: E = -2.049999193 at theta = -2.921681
pass 3: E = -2.049999193 at theta = -2.921681
pass 4: E = -2.049999991 at theta = -2.920425
exact ground state: -2.050000000
Note what each scan call costs: passing an array of parameter bindings
as the third element of the pub batches all 21 circuits into one MIMIQ
submission, not 21. Against the cloud that is the difference between one
round trip per pass and twenty-one.
Mid-circuit measurement¶
A Qiskit if_test block holding a single gate converts to a MIMIQ
IfStatement, so measurement feed-forward works:
qc = QuantumCircuit(2, 2)
qc.h(0)
qc.measure(0, 0)
with qc.if_test((qc.clbits[0], 1)):
qc.x(1)
qc.measure(1, 1)
print(backend.run(qc, shots=1000, seed=3).result().get_counts())
{'11': 525, '00': 475}
The second qubit tracks the first, which is what a working conditional
looks like: no 01 or 10. Bodies with an else branch, several
instructions, or nested classical operations raise
UnsupportedGateError; split them into
separate conditionals.
Gates outside the advertised Target¶
The Target lists the gates the transpiler may target, but the
converter accepts considerably more than that — see What converts, and how. So
you have a choice, and it is worth knowing which way is cheaper.
from qiskit.circuit.library import XXPlusYYGate
from qiskit import transpile
from mimiq_qiskit import qiskit_to_mimiq
qc = QuantumCircuit(5, 5)
qc.h(range(4))
qc.append(XXPlusYYGate(0.6, 0.2), [0, 1]) # not on the Target
qc.mcx([0, 1, 2, 3], 4) # not on the Target
qc.measure(range(5), range(5))
print("xx_plus_yy on Target:", "xx_plus_yy" in backend.target.operation_names)
print("direct :", len(list(qiskit_to_mimiq(qc))), "MIMIQ instructions")
transpiled = transpile(qc, backend=backend, seed_transpiler=1)
print("transpiled:", len(list(qiskit_to_mimiq(transpiled))), "MIMIQ instructions")
counts_direct = backend.run(qc, shots=4000, seed=11).result().get_counts()
counts_transpiled = backend.run(transpiled, shots=4000, seed=11).result().get_counts()
print("same result:", counts_direct == counts_transpiled)
xx_plus_yy on Target: False
direct : 11 MIMIQ instructions
transpiled: 55 MIMIQ instructions
same result: True
Both give the same distribution, but transpiling first is five times
larger. Qiskit synthesises the four-control mcx into primitives,
whereas the converter hands MIMIQ a native Control and lets MIMIQ
decompose it — which it can do better, knowing its own simulator.
So run circuits directly unless you have a reason to transpile. The
Target earns its place when you want Qiskit’s optimisation passes, or
when a pass in your own pipeline insists on a concrete basis.
Checking a result against Qiskit¶
At small widths Qiskit computes the same quantities independently, which makes a local simulator the right place to confirm a circuit means what you think:
from qiskit.quantum_info import Statevector
probe = QuantumCircuit(3)
probe.ry(0.7, 0)
probe.rx(0.3, 1)
probe.cx(0, 1)
probe.cry(1.1, 1, 2)
observable = SparsePauliOp(
["ZZI", "IXX", "ZII", "IIY"], [1.0, 0.5, -0.25, 0.4]
)
got = float(MimiqEstimatorV2(backend).run([(probe, observable)]).result()[0].data.evs)
want = float(np.real(Statevector(probe).expectation_value(observable)))
print(f"MIMIQ {got:+.12f}")
print(f"Qiskit {want:+.12f}")
MIMIQ +0.847260207297
Qiskit +0.847260207297
They agree to machine precision because both are exact: the estimator does no sampling, so there is no statistical band to allow for. This is the check the test suite runs over every gate in the conversion map.
That holds for a circuit ending in a single state, which is every circuit on this page. A circuit with a mid-circuit measurement, a reset, or a noise model ends in an ensemble instead, and its expectation value is an average the estimator has to be given a budget for. See Circuits that end in an ensemble.
Moving to the cloud¶
One line changes:
from mimiqlink import MimiqConnection
conn = MimiqConnection()
conn.connect() # opens a browser prompt
backend = MimiqBackend(conn) # everything above works unchanged
Two things to know when you do:
Run options are not portable. The cloud accepts every MIMIQ knob;
a local simulator names only the ones it implements. bonddim and the
other MPS settings mean nothing to a state-vector simulator, so passing
one to ExaqtQCS raises a ValueError naming the backend and the
option rather than failing somewhere inside mimiqcircuits. Set them only
on backends that have them. See Quickstart for the full list.
Batching is worth more. backend.run([qc1, qc2, ...]) and a pub
with an array of parameter bindings both become a single MIMIQ
submission. Locally that saves a little overhead; against the cloud it
saves a network round trip per circuit.
examples/local_simulation.py in the repository is a runnable script
covering the same ground.