OpenQARP
QubitraEngine is a qarp.engines.Engine. An OpenQARP program — blocks, primitives, and
the composite algorithms built on them — runs on a Qubitra backend by constructing this
engine in place of a local one; nothing else changes.
pip install 'qubitra-sdk[openqarp]'The base install carries no quantum framework; the openqarp extra brings openqarp in.
Importing qubitra.openqarp without it raises an error naming that command.
The engine
Section titled “The engine”Construct it around an existing QubitraClient, or let it build its own from
QUBITRA_API_KEY / QUBITRA_API_URL. backend_id defaults to sim-statevector-26q,
the exact simulator; sim-openqarp-26 runs the same program on OpenQARP’s own engine at
the same width, and also runs classical control. n_shots is the shot count primitives run at when they carry none
of their own, and session_id groups every job the engine submits under one run.
from qarp.algorithms import Samplerfrom qarp.blocks import SimpleBlockfrom qubitra.openqarp import QubitraEngine
bell = SimpleBlock(2, name="bell")bell.h(0)bell.cx(0, 1)bell.build()
engine = QubitraEngine("sim-statevector-26q", n_shots=4000)engine.build([Sampler(ket=bell)])
print(engine.run())# [{(0, 0): 0.4975, (1, 1): 0.5025}]Keys are OpenQARP’s own bitstring tuples: position q is qubit q, least significant
first, and values are probabilities. The platform reports counts keyed the other way
round — clbit 0 rightmost — and the engine translates at that boundary, so what you read
is what a local engine would have given you.
What runs where
Section titled “What runs where”Each primitive target maps onto one of the platform’s two primitives.
| Target | On the platform |
|---|---|
SAMPLING |
a sampler PUB: a terminal readout of every qubit, counts back |
EXPECTATION_VALUE over a QubitOperator |
an estimator PUB carrying the operator’s Pauli terms |
EXPECTATION_VALUE from sampled measurement groups |
sampler PUBs; the primitive averages them itself |
OVERLAP, TRANSITION_AMPLITUDE |
refused |
A Sampler measures the whole register and the engine marginalises onto
measured_qubits when the result comes back, so a subset costs the same as the full
register. qarp.EXACT asks for the infinite-shot distribution instead: the circuit is
submitted without the measurements that would collapse it, and the backend’s probability
result becomes the distribution.
Mid-circuit measurement
Section titled “Mid-circuit measurement”A sampling primitive reports the qubits’ final state, not the bits the circuit recorded on the way there. So a circuit that measures, acts on what it measured, and measures again keeps its own classical bits, and the engine appends a readout of every qubit past them — extending the one classical register — and reads the distribution from that slice:
block = SimpleBlock(1, name="mcm")block.measure(0, 0)block.x(0)block.measure(0, 1)block.build()
engine.build([Sampler(ket=block)])print(engine.run())# [{(1,): 1.0}] the qubit ends in |1>, whatever c[0] and c[1] recordedResets and classically-conditioned gates are handled the same way, and so is a circuit
that leaves some qubits unmeasured: the readout reports every qubit’s final state, the
unmeasured ones included. A circuit that ends measuring every qubit q into c[q] is
submitted untouched, and its bits are the answer.
qarp.EXACT cannot serve such a circuit: every shot takes a different branch, so there is
no single final state to read a distribution from, and the engine says so rather than
returning the distribution of some other circuit. Run it with a finite shot count.
An expectation value over a QubitOperator is the backend’s own estimator, which does
not sample — shot counts do not apply to it, and the value comes back exact on a
simulator.
Sweeps and gradients: one job
Section titled “Sweeps and gradients: one job”batch_run submits the whole sweep as one job, and run_gradient submits every
stencil point of every parameter as one job:
import numpy as npfrom qarp.algorithms import StateVectorfrom qarp.operators import QubitOperator
primitive = StateVector(ket=ansatz, operator=hamiltonian)points = [{"theta": value} for value in np.linspace(0, np.pi, 40)]
energies = engine.batch_run([primitive], points) # one round trip
engine.build([primitive])(gradient,) = engine.run_gradient({"theta": 0.3}, method="parameter-shift")The platform’s round trip is dominated by orchestration rather than simulation, so a 40-point sweep as one job costs one round trip instead of forty. A parameter-shift gradient over twenty parameters is forty evaluations, and it is likewise one.
gradient_methods is {"default", "parameter-shift", "finite-diff", "spsa"} — every
method that evaluates its points through the sweep. A multi-primitive run is also one
job, with results index-aligned with build.
How circuits travel
Section titled “How circuits travel”Circuits leave in the richest format the backend accepts: OpenQASM 3, else OpenQASM 2. A backend accepting neither is refused when the first primitive is built, with a message naming what it does accept.
Under OpenQASM 3 a free parameter travels as an input declaration and the sweep travels
as parameter rows beside it, so one circuit covers every point. Declarations are sorted
by parameter name, and a row supplies values in that same order — see
Circuit formats. Under OpenQASM 2, which has no parameter
mechanism, each point is bound before it leaves.
Nothing is transpiled client-side: the backend states the gate set it accepts and the platform compiles for it. A gate the chosen format cannot represent is rejected before anything is submitted, with the offending instruction in the message.