Skip to content

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.

Terminal window
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.

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 Sampler
from qarp.blocks import SimpleBlock
from 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.

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.

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] recorded

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

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 np
from qarp.algorithms import StateVector
from 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.

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.