Sending Jobs to QSolid¶
This notebook demonstrates how to send jobs explicitly to the QSolid demonstrator QPU, as well as how to use the QSolid-specific stack objects (QSolidQPU10, QSolidTranspiler, QSolidStack10, and EmuQSolidQPU10).
QSolid: How to access the QPU - available QPU Backends and Plugin¶
We provide to the user:
- Python Qaptiva
QPUclasses for emulated and real QSolid QPUs (need QPU specific circuits as input) - Python Qaptiva
Transpilerplugin (more details on plugins see section Using plugins ) for compilation of arbitrary circuits to the hardware specifications of the QSolid demonstrator QPUs - Python Qaptiva
Stackclasses as combination ofTranspilerandQPU. Can be uses in the same way asQPUobjects, but with arbitrary circuits.
In more detail:
- The real QSolid demonstrator can be accessed by initializing a corresponding Qaptiva QPU object:
QSolidQPU10- direct access to the QPU, no transpilation, circuit has to match the native gateset and topology of the hardware- By adding the
QSolidTranspilerto the QPU stack, almost any circuit can be adapted to the QPU:- e.g.
stack = QSolidTranspiler() | QSolidQPU10()
- e.g.
- Or simply use the ready-to-use QSolidStack object:
QSolidStack10- full stack combining the transpiler and the QPU
- An emulated QPU which corresponds to the actual QSolid hardware configuration is available as:
EmuQSolidQPU10- 10 qubits, linear nearest neighbour (LNN) topology- A ready-to-use stack which combines the
QSolidTranspilerand theEmuQSolidQPU10:EmuQSolidStack10- 10 qubits, linear nearest neighbour (LNN) topology
- The emulated QSolid QPUs are noisy by default but can be configured to be noiseless by giving the parameter
sim_method=noiseless - The underlying noise model uses the actual measured noise parameters
- Crosstalk noise has shown to have a major impact on the results, so for any noisy simulation crosstalk noise is switched on by default
- An emulated QPU which corresponds to the planned QSolid hardware:
EmuQSolidQPU30:- 30 qubits, linear nearest neighbour (LNN) topology
- Not yet available as real hardware, expected by the end of 2026
- As best guess for noise parameters, an average of the current 10 qubit chip calibration data is being used
The QSolid transpiler is based on the NISQCompiler, which is part of Qaptiva and supports two different methods for topology adaptation:
Nnizer- which just adds SWAP gates to adapt the input circuit to the topology of the QPULazySynthesis- which optimizes the generated circuit and may move some of its parts to classical post-processing
The QSolid transpiler uses Nnizer by default. You can use the parameter compiler="LazySynthesis" of QSolidTranspiler or one of the QSolid stacks to enable LazySynthesis.
NOTE: If you are running a circuit compiled with LazySynthesis on a QPU (or another emulator) without going through the full QSolid Qaptiva software stack you will be missing the required postprocessing. Please ask the QSolid Eviden team for advice.
Example – Run simple Bell pair on real QPU¶
Here: Using the convenience object QSolidStack10, so you don't need to bother about explicit transpilation of your job.
import sys
print("Running...", file=sys.stderr)
from qlmaas.qpus import QSolidStack10
from qat.lang.AQASM import Program, H, CNOT
prog = Program()
qubits = prog.qalloc(2)
prog.apply(H, qubits[0])
prog.apply(CNOT, qubits[0], qubits[1])
circ = prog.to_circ()
circ.display()
job = circ.to_job()
qpu = QSolidStack10()
result = qpu.submit(job)
for sample in result:
print(f" State {sample.state}: probability = {sample.probability}")
print("Done.", file=sys.stderr)
Example – Transpile only and display transpiled circuit¶
Like this you can see what the resulting circuit on the QPU would look like without actually running it on any QPU backend.
from qlmaas.qpus import EmuQSolidQPU10
from qlmaas.plugins import QSolidTranspiler
from qat.lang.AQASM import Program, H, CNOT
# Define circuit:
prog = Program()
qubits = prog.qalloc(2)
prog.apply(H, qubits[0])
prog.apply(CNOT, qubits[0], qubits[1])
circ = prog.to_circ()
circ.display()
# Create job object:
job = circ.to_job()
# Get QPU specifications:
qpu = EmuQSolidQPU10()
hwspecs = qpu.get_specs()
# Display transpiled circuit (+ "transpilation job"):
target_circ = QSolidTranspiler().compile(job, hwspecs).circuit
target_circ.display()
Example – variational algorithm:¶
from qat.core import Observable as Obs
from qat.lang.AQASM import CNOT, RY, Program
from qlmaas.plugins import ScipyMinimizePlugin
from qlmaas.qpus import QSolidStack10, LinAlg
# we instantiate the Hamiltonian we want to approximate the ground state energy of
hamiltonian = (
Obs.sigma_z(0, 2) * Obs.sigma_z(1, 2)
+ Obs.sigma_x(0, 2) * Obs.sigma_x(1, 2)
+ Obs.sigma_y(0, 2) * Obs.sigma_y(1, 2)
)
# we construct the variational circuit (ansatz)
prog = Program()
reg = prog.qalloc(2)
thetas = [prog.new_var(float, "\\theta_%s" % i) for i in range(2)]
RY(thetas[0])(reg[0])
RY(thetas[1])(reg[1])
CNOT(reg[0], reg[1])
circ = prog.to_circ()
# construct a (variational) job with the variational circuit and the observable
job = circ.to_job(observable=hamiltonian)
# we now build a stack that can handle variational jobs
optimizer_scipy = ScipyMinimizePlugin(
method="COBYLA",
tol=1e-6,
options={"maxiter": 200},
x0=[1, 1]
)
# Choose your QPU backend:
stack = optimizer_scipy | QSolidStack10()
# we submit the job and print the optimized variational energy (the exact GS energy is -3)
result = stack.submit(job)
# the output of the optimizer can be found here
print(result.meta_data["optimizer_data"])
print(f"Minimum VQE energy = {result.value}\n")
Noisy simulation - "QSolid Gate-based Digital Twin"¶
As for any other real Quantum hardware, the results of the QSolid demonstrator chip are not ideal results, but influenced by quantum noise. Within the QSolid project we have developped a QSolid specific noise model, which takes many different source of noise into account. These are not only T1/T2 times, gate and readout fidelities, etc., but also the influence of microwave crosstalk and flux crosstalk between the individual qubits.
The underlying noise model uses the actual measured noise parameters of the demonstrator.
As crosstalk noise has shown to have a major impact on the results, for any noisy simulation crosstalk noise is switched on by default (not for noiseless simulations of course).
Imports and example¶
At first, we have to do some basic and Qaptiva-specific imports again:
from qat.lang.AQASM import *
from qlmaas.qpus import EmuQSolidQPU10, EmuQSolidStack10, EmuQSolidQPU30, EmuQSolidStack30, QSolidQPU10, QSolidStack10
from qlmaas.plugins import QSolidTranspiler
After that, we will create a simple quantum program returning a Bell state and use QSolid's 10-qubit QPU, where we can print the state probabilities and the corresponding quantum circuit. Since we will use a noisy QPU, we should expect slightly different state probabilities when compared to the ideal QPU.
quantum_program = Program()
qubits = quantum_program.qalloc(2)
quantum_program.apply(H, qubits[0])
quantum_program.apply(CNOT, qubits[0], qubits[1])
original_circuit = quantum_program.to_circ()
original_job = original_circuit.to_job()
original_circuit.display()
Emulation of noiseless QSolid QPU (Default for EmuQSolidQPU10 and EmuQSolidQPU30)¶
We initialize a noiseless QPU that is used to compare the results with the QSolid QPU.
noiseless_qpu = EmuQSolidQPU10()
# noiseless_qpu = EmuQSolidQPU30()
If you are interested in the specific layout of the QPU, you can use get_specs(), e.g. for various hardware parameters, such as the gateset, topology and number of qubits.
⚠ The resulting hardware_specs object is required as input parameter for the QSolidTranspiler plugin, if you decide to explicitely do a manual transpilation of your circuit:
hardware_specs = noiseless_qpu.get_specs()
nbqbits = hardware_specs.nbqbits
gateset = hardware_specs.gateset
topology = hardware_specs.topology
graph = hardware_specs.as_graph()
As a first step, we call the QSolid transpiler and compile the input circuit into a circuit that is compatible with the QSolid QPU (remember: not necessary when using the Stack objects).
compiler = QSolidTranspiler()
compiled_circuit = compiler.compile(original_job, hardware_specs).circuit
compiled_job = compiled_circuit.to_job()
compiled_circuit.display()
Since we now have a circuit that has been adapted to the QPU specs, it is sufficient to just call the QPU, since the transpilation step is not required anymore.
We obtain the results with the submit_job method:
noiseless_result = noiseless_qpu.submit(compiled_job)
for sample in noiseless_result:
print("State %s, probability %s" % (sample.state, sample.probability))
Available parameters for the EmuQSolidQPU10/EmuQSolidStack10/EmuQSolidQPU30/EmuQSolidStack30¶
Described in more detail further below, these are the available parameters to both the QSolid QPU and the Stack object:
include_crosstalk (bool)
Default: True for noisy simulations
False for noiseless simulations
sim_method (str)
Default: "noiseless"
Possible values are:
"noiseless" (noiseless LinAlg state vector simulator)
"deterministic" (deterministic NoisyQProc simulator)
"stochastic" (stochastic NoisyQProc simulator)
"MPO" (Matrix Product Operator simulator)
Per default, the noiseless QPU does of course not include the simulation of crosstalk noise. However, it can be included using include_crosstalk, e.g. if you want to examine its specific influence:
noiseless_crosstalk_stack = EmuQSolidStack10(
sim_method="noiseless",
include_crosstalk=True
)
noiseless_crosstalk_result = noiseless_crosstalk_stack.submit(original_job)
for sample in noiseless_crosstalk_result:
print("State %s, probability %s" % (sample.state, sample.probability))
Noisy emulators¶
To get a realistic emulation of the QPU, the user will have the choice between three methods: deterministic, stochastic and MPO (matrix product operator). As default, the average noise data of the 10-qubit emulator is used for both EmuQSolidQPU30 and EmuQSolidStack30. Furthermore, crosstalk is already included when selecting one of these three simulation methods, although all simulation methods can be used explicitely without crosstalk by setting include_crosstalk=False when initializing the QPU object.
For crosstalk, a plugin has been implemented that models quantum circuits using experimentally provided flux and microwave crosstalk matrices. It injects virtual and physical single-qubit rotations and ZZ interactions to represent leakage from flux-controlled and microwave-driven gates, including two-qubit gates such as iSWAP. When accumulated leakage signifcantly alters a gate's rotation axis, the plugin performs an exact SU(2) decomposition (here, RZ-RY-RZ) to preserve unitary fidelity.
For small circuit sizes with up to 10 qubits, it is recommended to use EmuQSolidQPU10 or EmuQSolidStack10 in deterministic mode due to its full density matrix description if the qubits' state. Going to larger qubits numbers, the runtime scales exponentially and it is recommended to either use the stochastic or MPO method (see also here).
Using the emulator in stochastic mode avoids the overhead of storing the entire density matrix but comes at the cost of stochastic sampling of the final result, with intrinsic statistical uncertainty.
For simplicity we use either the Stack or the QPU objects in the following examples, but of course depending on your circuit ("original" or "pre-compiled"), you just exchange the corresponding class names.
Deterministic QPU/Stack (with crosstalk)¶
After defining the stack, we can extract informations about the QPU such as the number of qubits, gateset and topology with the get_specs method.
deterministic_stack = EmuQSolidStack10(sim_method='deterministic')
deterministic_result = deterministic_stack.submit(original_job)
for sample in deterministic_result:
print("State %s, probability %s" % (sample.state, sample.probability))
When simulating circuits with a large number of qubits and circuit depth, the deterministic approach might reach its limits soon in terms of memory and runtime. Therefore, the user has the option to choose a stochastic approach or to use a matrix-product based (MPO) QPU.
We will start with the stochastic simulation method, with n_samples being the relevant parameter of choice:
Stochastic QPU¶
stochastic_qpu = EmuQSolidQPU10(
sim_method="stochastic",
n_samples=10000
)
stochastic_stack = QSolidTranspiler() | stochastic_qpu
stochastic_result = stochastic_stack.submit(original_job)
for sample in stochastic_result:
print("State %s, probability %s" % (sample.state, sample.probability))
MPO QPU/Stack¶
The third simulation method is MPO, where the runtime is defined by the bond_dimension, which is the amount of entanglement that a quantum state can encapsulate. The MPO representation compactly approximates large quantum states by truncating bond dimensions, thus ensuring faster runtimes when entanglement is small. However, large entanglement increases truncation errors, especially if the bond dimension is set too small. This reduces the method's effectiveness and may produce incorrect results. To overcome this, one would need to increase the bond dimension, which would lead to more precise results but also increase the runtime.
mpo_qpu = EmuQSolidQPU10(
sim_method="MPO",
bond_dimension=4
)
mpo_stack = QSolidTranspiler() | mpo_qpu
mpo_stack_result = mpo_stack.submit(original_job)
for sample in mpo_stack_result:
print("State %s, probability %s" % (sample.state, sample.probability))
Using non-default parameters for the Nnizer and LazySynthesis methods¶
For the compilation of the cicuit, it is possible to use different input parameters for the Nnizer or LazySynthesis method (for more information about the input parameters, have a look at the Qaptiva documentation). We will start with the Nnizer:
qpu = EmuQSolidQPU10(sim_method="deterministic")
transpiler = QSolidTranspiler(compiler_options={"method": "sabre"})
stack = transpiler | qpu
result = stack.submit(original_job)
for sample in result:
print("State %s, probability %s" % (sample.state, sample.probability))
For LazySynthesis we will choose a different search depth, which is a crucial parameter for this compilation method:
qpu = EmuQSolid10(sim_method="deterministic")
transpiler = QSolidTranspiler(
compiler="LazySynthesis",
compiler_options={"depth": 3}
)
stack = transpiler | qpu
result = stack.submit(original_job)
for sample in compiled_result_lazy_synthesis_params:
print("State %s, probability %s" % (sample.state, sample.probability))
How to choose calibration of different calibration cycle/point in time¶
With continuous operation of the QSolid demonstrators, the QPUs need to be recalibrated from time to time, which can significantly change some noise parameters. The default noise data will always be the latest available data. But when reproduceability of results is required, e.g. for comparison studies of different algorithms, it is vital to have stable noise parameters. All noise data will be made available tagged with the timestamp when the data was delivered. The interface of listing the available data will still be subject to improvement.
Reminder: List general hardware specifications like topology and gateset:
from qlmaas.qpus import EmuQSolidQPU10, EmuQSolidStack10
EmuQSolidQPU10(sim_method='noiseless').get_specs()
List availale noise data sets:
This lists both basic noise data sets as well as crosstalk noise data (flux and microwave).
EmuQSolidQPU10(sim_method='deterministic').get_specs().meta_data['Available noise data']
Choose specific sets of noise data:
stack = EmuQSolidStack10(
sim_method="deterministic",
noise_data="2026-02-18",
flux_crosstalk="2026-01-15",
microwave_crosstalk="2026-01-15"
)
result = stack.submit(original_job)
for sample in result:
print("State %s, probability %s" % (sample.state, sample.probability))
How to define your own noise data for emulation¶
For studying the influence of certain noise sources, fitting of noise parameters or estimations of maximum acceptable noise levels for certain algorithms/circuits, it is necessary to define your own noise parameters. This can be done by simply defining a corresponding noise data dictionary containing all the following keys with sub dictionaries and entries for each qubit or qubit-connection respectively:
noise_data = {
"t1_time": {
"0": 12000.0,
"1": 13000.0,
"2": 14000.0,
"3": 15000.0,
"4": 16000.0,
"5": 17000.0,
"6": 18000.0,
"7": 19000.0
},
"t2_time": { "0": 15000.0, ... },
"t2_Ramsey": { "0": 12000.0, ... },
"rx_duration": { "0": 40.0, ... },
"ry_duration": { "0": 40.0, ... },
"rz_duration": { "0": 40.0, ... },
"iswap_duration": { "(0,1)": 50.0, ... },
"fidelity_1qb_gate": { "0": 99.9, ... },
"fidelity_2qb_gate": { "(0,1)": 99.8, ... },
"readout_error_state_0": { "0": 5.0, ... },
"readout_error_state_1": { "0": 25.0, ... },
"initialization_error": { "0": 2.5, ... },
}
This dictionary can then be handed over to the emulated QSolid QPU object as parameter:
deterministic_own_data_stack = EmuQSolidStack10(sim_method='deterministic', noise_data=noise_data)
deterministic_own_data_stack_result = deterministic_own_data_stack.submit(original_job)
for sample in deterministic_own_data_stack_result:
print("State %s, probability %s" % (sample.state, sample.probability))
Currently this noise data dictionary has to be complete with all keys and values for all qubits. In future versions of the emulated QSolid QPUs it is planned to feature exchanging subsets of the real noise data as well.
How to map logical to phyisical qubits¶
See the section Mapping logical to physical qubits for more details.
The QSolid project (Quantum Computer in the Solid State) (www.q-solid.de) acknowledges the support of the Federal Ministry of Research, Technology and Space (BMFTR) within the framework programme “Quantum technologies – from basic research to market”.