Qaptiva Access¶
Qaptiva is the device that enables access to the quantum computing resources at JUNIQ facility.
Before proceeding, please ensure you have:
- Joined a project on JuDoor that provides access to Qaptiva resources. (If not see documentation here)
- Set up QLMAAS access. (If not see instructions in QLMAAS setup documentation)
Quick start: Sending a quantum job to Qaptiva¶
The following code snippet shows creating a simple quantum Job, submitting it to Qaptiva and viewing the results. We call this a cloud job, because no classical resources are requested from JSC (eg HPC resources).
# The most relevant object are defined in "qat.core" and "qat.lang"
# (qat = "Qaptiva Application Toolset"):
# (qlmaas = "QLM as a Service" or "Qaptiva Access")
from qlmaas.qpus import LinAlg # Note that we are importing remote qpu from qlmaas and not local one from qat
from qat.lang import qrout, H, CNOT, X
# Define the circuit
@qrout
def bell_pair():
H(0)
CNOT(0, 1)
bell_pair.display()
Alternatively you can create a program in a "Sequential" way:
from qat.lang import Program, H, CNOT
bell_pair = Program() # Initialize quantum program
# Number of qbits
nbqbits = 2
# Allocate some qbits
qbits = bell_pair.qalloc(nbqbits)
# Apply some quantum Gates
# Available gates are:
# 'CCNOT', 'CNOT', 'CSIGN', 'H', 'I', 'ISWAP', 'PH', 'RX',
# 'RY', 'RZ', 'S', 'SQRTSWAP', 'SWAP', 'T', 'X', 'Y', 'Z'
H(qbits[0])
CNOT(qbits[0], qbits[1])
# Export this program into a quantum circuit
circuit = bell_pair.to_circ()
# Export this circuit into a job
job = circuit.to_job(nbshots=512)
# Convert the circuit into a myqlm Job
job = bell_pair.to_job(nbshots=512)
# Instantiate the qpu
linalgqpu = LinAlg()
# Submit the job to the QPU
result = linalgqpu.submit(job)
# Visualise the result
for sample in result:
print("State %s probability %s" % (sample.state, sample.probability))
Submitted a new batch: SJob25927 State |00> probability 0.505859375 State |11> probability 0.494140625
# if on a jupyter notebook run `result.display()` to get a better visualisation
result.display()
Result(raw_data=[Sample(_state=b'\x00', probability=0.50390625, _amplitude=None, intermediate_measurements=None, err=0.022118022740803385, qregs=[DefaultRegister(length=2, start=0, msb=None, _subtype_metadata=None, key=None)]), Sample(_state=b'\x03', probability=0.49609375, _amplitude=None, intermediate_measurements=None, err=0.022118022740803385, qregs=[DefaultRegister(length=2, start=0, msb=None, _subtype_metadata=None, key=None)])], _value=None, error=None, value_data=None, error_data=None, meta_data={'nbshots': '512', 'single_job': 'True'}, in_memory=True, data=None, qregs=[DefaultRegister(length=2, start=0, msb=None, _subtype_metadata=None, key=None)], _parameter_map=None, _values=None, values_data=None, need_flip=False, nbqbits=None, lsb_first=False, has_statevector=False, statevector=None)How to choose your QPU backend¶
Now that we know how a typical quantum job looks like, we can now explore how to submit them to the different quantum processing backends. In the Qaptiva framework, these backends are called "QPUs" (Quantum Processing Units). They represent the quantum hardware or simulators that will execute your quantum job. You can choose from a variety of real and simulated QPU backends to run your quantum job.
One of the major benefits of Qaptiva is the possibility to run your quantum jobs on either a QPU emulator or on a real QPU in the exact same manner. From a programming point of view there is no difference between "real" and "emulated" QPUs. Both are representred by Python objects of type "QPUHandler". These can be imported as Python modules from qlmaas.qpus.
As an illustrative example let us consider the QPUs developed within the QSolid project. Within QSolidQPU we try to give intuitive names to these objects. For example with...:
from qlmaas.qpus import QSolidQPU10
... you get access to the 10 qubit quantum processor developed within the QSolid project. Likewise in the near future EmuQSolidQPU30 will let you simulate the QSolid 30 qubit demonstrator chip still to be built.
See a list of available QPUs in the main JUNIQ documentation.
Using plugins:¶
Within the Qaptiva framework, plugins are one of the most important means to modify and manipulate quantum circuits or jobs. Besides a large amount of predefined plugins for different purposes it is also possible to write your own plugins.
A transpiler plugin¶
Most quantum circuits will not run on a chosen QPU out-of-the-box, but have to be compiled (more exact: "transpiled") to match both the gateset and the connectivity provided by the QPU. As circuit transpilation is not trivial, we provide ready-to-use transpiler plugins for the different QPUs. These plugins follow the same name scheme as the QPUs. For example you can use QSolidTranspiler to transpile your circuit to the constraints of the QSolid demonstrator chip. Note that not all QPUS have a transpiler plugin available yet, but we are working on it.
These QSolid transpiler will keep evolving during the runtime of the project and you will most likely be able to choose among a variety of different versions by the end of QSolid.
Other plugins¶
For example, plugins are used for compilers/transpilers and optimizers like NISQCompiler or ScipyMinimizePlugin. Within the QSolid project we are currently developing transpiler plugins for the QSolid QPU to be built. These transpilers and emulators highly depend on the available information of the QPU (native gateset, topology, noise data, etc.) and will evolve accordingly.
How to use stack objects¶
A sorted list of plugins in combination with a QPU backend are called a "stack". When using local (myQLM) and remote (QaptivaAccess) plugins, it is important to put the local plugins before the remote plugins. Please note that all plugins are applied to the input of a job and to the result of a job (in reverse order)
Users who are mainly interested in their algorithms might not care about the details of the compiling and the resulting circuits. For this reason we provide stack objects that can be used just as QPU objects, but have an internally integrated compiler. So the user can send any arbitrary quantum circuit directly to a corresponding stack. For example in the QSolid stacks we follow the same name scheme as for QPUs, e.g. QSolidStack10 will send an automatically compiled circuit to the real QSolid 10 qubit chip and return the result to the user.
A job can be submitted directly to a stack. The syntax for combining plugins and creating a stack is rather simple:
from qat.plugins import local_plugin1, local_plugin2
from qlmaas.plugins import remote_plugin3, remote_plugin4
from qlmaas.qpus import my_remote_qpu
stack = (
local_plugin1
| local_plugin2
| remote_plugin3
| remote_plugin4
| my_remote_qpu()
)
stack.submit(job)
List available Qaptiva QPUs and plugins¶
The myQLM client provides the command qlmaas_prompt.py to query the Qaptiva server. This shows you all QPU backends and plugins currently available on your system.
⚠ Click "Details" to see example usage:
$ module load myqlm
$ qlmaas_prompt.py
> help
List of command:
ls List all jobs
cancel Cancel one or several jobs
delete Delete one or several jobs
dl Try and retrieve a job result
plugins List all possible plugins
qpus List all possible qpus
generators List all possible generators
applications List all possible applications in a python module
module List all importable items present in a module
config Display or update the current server-side configuration file
manage Change password, certificate or DN used to login
quit Exits the session
> qpus
QutipQPU (qat.qpus)
AnalogQPU (qat.qpus)
SQAQPU (qat.qpus)
Bdd (qat.qpus)
RemoteQPU (qat.qpus)
Feynman (qat.qpus)
ClassicalQPU (qat.qpus)
LinAlg (qat.qpus)
MPSLegacy (qat.qpus)
MPS (qat.qpus)
MPO (qat.qpus)
MPSTraj (qat.qpus)
NoisyQProc (qat.qpus)
UploadedQPU (qat.qpus)
QPEG (qat.qpus)
Stabs (qat.qpus)
EmuEleqtronQPU (qat.qpus)
JadeQPU (qat.qpus)
PasqalQPU1 (qat.qpus)
Jiqcer5QPU (qat.qpus)
EmuQSolidQPU10 (qat.qpus)
EmuQSolidStack10 (qat.qpus)
QSolidQPU10 (qat.qpus)
QSolidStack10 (qat.qpus)
> plugins
AdaptVQEPlugin (qat.plugins)
CausalConesSplitter (qat.plugins)
CostFunctionPlugin (qat.plugins)
Fragmenter (qat.plugins)
QEMPlugin (qat.plugins)
Graphopt (qat.plugins)
GradientDescentOptimizer (qat.plugins)
Nnizer (qat.plugins)
PatternManager (qat.plugins)
GateRewriter (qat.plugins)
KAKCompression (qat.plugins)
LazySynthesis (qat.plugins)
InitialMapping (qat.plugins)
NISQCompiler (qat.plugins)
CircuitInliner (qat.plugins)
MultipleLaunchesAnalyzer (qat.plugins)
BaseChanger (qat.plugins)
ObservableSplitter (qat.plugins)
SeqOptim (qat.plugins)
StatPlugin (qat.plugins)
TransformObservable (qat.plugins)
ZeroNoiseExtrapolator (qat.plugins)
Display (qat.plugins)
QuameleonPlugin (qat.plugins)
Remap (qat.plugins)
RemotePlugin (qat.plugins)
EmptyPlugin (qat.plugins)
OffloadedPlugin (qat.plugins)
FusionPlugin (qat.plugins)
PQEOptimizationPlugin (qat.plugins)
SPQEPlugin (qat.plugins)
UploadedPlugin (qat.plugins)
ScipyMinimizePlugin (qat.plugins)
SPSAMinimizePlugin (qat.plugins)
PSOMinimizePlugin (qat.plugins)
QSolidTranspiler (qat.plugins)
> quit
$
Lists of standard Qaptiva QPU backends and plugins can be found here:
https://jurecaqlm-proxy.fz-juelich.de/doc/04_api_reference/module_qat/module_qpus.html
https://jurecaqlm-proxy.fz-juelich.de/doc/04_api_reference/module_qat/module_plugins.html
Sending Qiskit circuits to Qaptiva¶
One can also use the interoperability features of myqlm to create a circuit in Qiskit and then submit it to a Qaptiva backend for execution.
If you use Qiskit 1.0 then you need to follow the instructions for myqlm-interop but for Qiskit 2.0 and above use the native interoperability features of myqlm. See the documentation for more details.
Via myqlm-interop (for Qiskit 1.x)¶
WARNING: MyQLM interoperability modules are experimental and might lead to unexpected results. If any issues arise due to this, please report them at myqlm-interop/issues.
Read more about Qiskit interoperability with myqlm here.
And see an example use below, you need to install myqlm-interop[qiskit_binder] module for it to work.
# Making a simple quantum circuit using qiskit
from qiskit import QuantumCircuit
qc = QuantumCircuit(3)
qc.h(0)
qc.cx(0, 1)
qc.cx(1, 2)
qc.measure_all()
# Convert it to a myqlm Job
from qat.interop.qiskit import qiskit_to_qlm
qlm_circuit = qiskit_to_qlm(qc)
qlm_job = qlm_circuit.to_job(nbshots=1000)
# Submitting a job to Qaptiva for a selected QPU
from qlmaas.qpus import LinAlg # Replace with your desired QPU
linalgqpu = LinAlg()
result = linalgqpu.submit(qlm_job)
result.display()
Submitted a new batch: SJob25928
Result(raw_data=[Sample(_state=b'\x00', probability=0.481, _amplitude=None, intermediate_measurements=[IntermediateMeasurement(cbits=[False], gate_pos=3, probability=None), IntermediateMeasurement(cbits=[False], gate_pos=4, probability=None), IntermediateMeasurement(cbits=[False], gate_pos=5, probability=None)], err=0.015807874268505835, qregs=[DefaultRegister(length=3, start=0, msb=None, _subtype_metadata=None, key=None)]), Sample(_state=b'\x07', probability=0.519, _amplitude=None, intermediate_measurements=[IntermediateMeasurement(cbits=[True], gate_pos=3, probability=None), IntermediateMeasurement(cbits=[True], gate_pos=4, probability=None), IntermediateMeasurement(cbits=[True], gate_pos=5, probability=None)], err=0.015807874268505835, qregs=[DefaultRegister(length=3, start=0, msb=None, _subtype_metadata=None, key=None)])], _value=None, error=None, value_data=None, error_data=None, meta_data={'nbshots': '1000', 'single_job': 'True'}, in_memory=True, data=None, qregs=[DefaultRegister(length=3, start=0, msb=None, _subtype_metadata=None, key=None)], _parameter_map=None, _values=None, values_data=None, need_flip=False, nbqbits=None, lsb_first=False, has_statevector=False, statevector=None)Via native interoperability (for Qiskit 2.x and above)¶
From myqlm==1.12.4 onwards, you can use the native Qiskit 2.0 interoperability with qat-qiskit ensure you have qat-qiskit installed in your environment and see the example circuit below:
from qiskit import ClassicalRegister, QuantumCircuit, QuantumRegister
from qat.qiskit import QaptivaService, Sampler
def bell_pair():
qr = QuantumRegister(2, 'q')
cr = ClassicalRegister(2, 'c')
circuit = QuantumCircuit(qr, cr)
circuit.h(qr[0])
circuit.cx(qr[0], qr[1])
circuit.measure(qr, cr)
return circuit
circuit = bell_pair()
service = QaptivaService()
backend = service.backend("LinAlg")
sampler = Sampler(backend)
job = sampler.run([circuit], shots=10)
print(job.status())
print(job.result())
DeprecationWarning: Treating CircuitInstruction as an iterable is deprecated legacy behavior since Qiskit 1.2, and will be removed in Qiskit 2.0. Instead, use the `operation`, `qubits` and `clbits` named attributes.
Submitted a new batch: SJob94168
JobStatus.RUNNING
PrimitiveResult([SamplerPubResult(data=DataBin(register_0=np.ndarray(<shape=(10,), dtype=<U2>)))], metadata={'single_job': 'False'})
Note that this way you return PrimitiveResult instead of Result object.
from qat.core import Batch
@qrout
def singlet_state():
X(0)
X(1)
H(0)
CNOT(0, 1)
singlet_job = singlet_state.to_job(nbshots=512)
bell_job = bell_pair.to_job(nbshots=512)
# Create a batch
batch = Batch([singlet_job, bell_job])
# submit the batch just as before
batch_result = linalgqpu.submit(batch)
Submitted a new batch: SJob25929
# Display the results
for i, job_result in enumerate(batch_result):
print("Job %s:" % i)
for state in job_result:
print("State %s probability %s" % (state.state, state.probability))
Job 0: State |01> probability 0.5234375 State |10> probability 0.4765625 Job 1: State |00> probability 0.53515625 State |11> probability 0.46484375
!!! note "Metadata structure may change across QPUs"
Each result object contains a meta_data attribute which is a dictionary containing various information about the job execution. The structure of this dictionary can vary across different QPUs as this is not standardised. Different QPU vendors provide different information in different format.
Retrieving past results from Qaptiva¶
If you have submitted a job to Qaptiva and want to retrieve the results later, you can use the job_id that was returned when you submitted the job. You can use this job_id to query the status of the job and retrieve the results once the job is completed. See the following example:
import qlmaas.jobs as jobs
job_id_str = "SJob191637"
old_job = getattr(jobs, job_id_str)
old_job.get_info()
JobInfo(id='SJob191637', status=3, submission_date='2026-03-27 13:16:28.408893', owner='karnad1', type=None, job_file='329131d9-e004-4bb4-aa89-81839f74ea23', result_file='329131d9-e004-4bb4-aa89-81839f74ea23.res', resources=[ResourceModel(qpu='qat.qpus:LinAlg', nbqbits=2, job_count=1, mem_necessary_biggest_job_mb=8388, wished_thread_count=3, devices=[], nb_nodes=1, node_type=0, max_time_minutes=None)], session_id=None, message=None, queue_pos=None, starting_date='2026-03-27 13:16:32.438094', ending_date='2026-03-27 13:16:34.873608', meta_data={'scheduler': 'Slurm Qaptiva Access Scheduler', 'join_type': 'websocket', 'join_handler': 'watcher'})
old_job.get_status()
'done'
old_result = old_job.join()
old_result.plot()
Mapping logical qubits to physical qubits¶
Often times you might want to define a quntum circuit in which you might want to map logical qubits to completely different physical qubits on the QPU. For eg. defining a bell pair between qubits 1 and 4 on the QPU, while in your circuit you are using qubits 0 and 1. This can be achieved by using the MappingPlugin plugin.
As an example, we will define a GHZ-3 circuit as follows:
from qat.lang import Program, H, CNOT
prog = Program()
qubits = prog.qalloc(3)
prog.apply(H, qubits[0])
prog.apply(CNOT, qubits[0], qubits[1])
prog.apply(CNOT, qubits[1], qubits[2])
job = prog.to_circ().to_job()
job.circuit.display()
We want to perform the following mapping from logical to physical qubits: 0 -> 3, 1 -> 4, and 2 -> 5.
mapping = {0: 3, 1: 4, 2: 5}
NOTE: Mapping to a different topology can result in an undesirable rearrangement of qubit indices. For example, a mapping of 0 -> 3, 1 -> 4, and 2 -> 7 would result in 0 -> 3, 1 -> 4, and 2 -> 5 due to the LNN topology. If the user wants to define this mapping explicitly, they would need to choose pbn as the compiler option. Note that pbn might produce circuits with a larger depth than the default method.
The MappingPlugin ca be used as follows (return_circuits is required to display the final circuit):
from qlmaas.plugins import MappingPlugin
from qlmaas.qpus import EmuQSolidStack10
mapper = MappingPlugin(mapping=mapping)
mapped_qpu = mapper | EmuQSolidStack10(return_circuits=True)
Alternatively, the mapping dictionary can be directly passed to the stack:
mapped_qpu = EmuQSolidStack10(mapping=mapping, return_circuits=True)
# If the user wants explicit mappings (e.g.: 0 -> 3, 1 -> 4, 2 -> 7)
# mapped_qpu = EmuQSolidStack10(mapping=mapping, compiler_options={"method": "pbn"})
Similarly to the transpiler, we can examine the remapped circuit (before transpilation):
mapped_job = job.compile(mapper)
mapped_circuit = mapped_job.circuit
mapped_circuit.display()
TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html
Submitted a new batch: SJob208258
Finally, we define our job and submit it to the QPU:
mapped_qpu_result = mapped_qpu.submit(job)
for sample in mapped_qpu_result:
print("State %s, probability %s" % (sample.state, sample.probability))
Submitted a new batch: SJob208262 State |000>, probability 0.4999999999999999 State |111>, probability 0.4999999999999999
The final circuit (mapped and transpiled) can be displayed by defining the following function:
from qat.core import Result, Circuit
import os
import base64
def extract_circuit_from_result(res: Result) -> Circuit:
if not "circuit_binary" in res.meta_data:
return Circuit()
c = res.meta_data["circuit_binary"]
decoded_bytes = base64.b64decode(c.encode("ascii"))
with open("received_circuit.circ", "wb") as f:
f.write(decoded_bytes)
circ = Circuit.load("received_circuit.circ")
os.remove("received_circuit.circ")
return circ
mapped_transpiled_circuit = extract_circuit_from_result(mapped_qpu_result)
mapped_transpiled_circuit.display()
Further reading¶
More details on connecting to Qaptiva can be seen in the following resources:
- myQLM Documentation
- Circuit manipulation in myqlm
- myQLM Cheat Sheet
- Main myqlm documentation
- Internal Qaptiva documentation
- Internal Qaptiva Access documentation
- Interoperability Tutorials
Specific examples of using Qaptiva to connect to various quantum devices can be found in the following notebooks: