How to modify the transpiler?#
A Qibolab platform can execute pulse sequences.
As shown in Circuit execution, Qibo circuits can be executed by invoking the qibolab.backends.QibolabBackend
, which is the object integrating Qibolab to Qibo.
When a Qibo circuit is executed, the backend will automatically transpile and compile it to a pulse sequence, which will be sent to the platform for execution.
The default transpilers and compiler outlined in the Transpiler and Compiler section will be used in this process.
In this tutorial we will demonstrate how the user can modify this process for custom applications.
The transpiler
and compiler
objects used when executing a circuit are attributes of qibolab.backends.QibolabBackend
.
Creating an instance of the backend provides access to these objects:
from qibolab.backends import QibolabBackend
backend = QibolabBackend(platform="my_platform")
print(backend.transpiler)
print(backend.compiler)
The transpiler is responsible for transforming the circuit to respect the chip connectivity and native gates, while the compiler transforms the circuit to the equivalent pulse sequence. The user can modify these attributes before executing a circuit. For example:
from qibo import gates
from qibo.models import Circuit
from qibolab.backends import QibolabBackend
# define circuit
circuit = Circuit(1)
circuit.add(gates.U3(0, 0.1, 0.2, 0.3))
circuit.add(gates.M(0))
backend = QibolabBackend(platform="my_platform")
# disable the transpiler
backend.transpiler = None
# execute circuit
result = backend.execute_circuit(circuit, nshots=1000)
completely disables the transpilation steps. In this case the circuit will be sent directly to the compiler, to be compiled to a pulse sequence. Instead of completely disabling, custom transpilation steps can be given:
from qibolab.backends import QibolabBackend
from qibolab.native import NativeType
from qibolab.transpilers.gate_decompositions import NativeGates
backend = QibolabBackend(platform="my_platform")
backend.transpiler = NativeGates(two_qubit_natives=NativeType.CZ)
Now circuits will only be transpiled to native gates, without any connectivity matching steps.
The qibolab.transpilers.gate_decompositions.NativeGates
transpiler used in this example assumes Z, RZ, GPI2 or U3 as the single-qubit native gates, and supports CZ and iSWAP as two-qubit natives.
In this case we restricted the two-qubit gate set to CZ only.
If the circuit to be executed contains gates that are not included in this gate set, they will be transformed to multiple gates from the gate set.
Arbitrary single-qubit gates are typically transformed to U3.
Arbitrary two-qubit gates are transformed to two or three CZ gates following their universal CNOT decomposition.
The decomposition of some common gates such as the SWAP and CNOT is hard-coded for efficiency.
Multiple transpilation steps can be implemented using the qibolab.transpilers.pipeline.Pipeline
:
from qibolab.native import NativeType
from qibolab.transpilers.pipeline import Pipeline
from qibolab.transpilers.star_connectivity import StarConnectivity
from qibolab.transpilers.gate_decompositions import NativeGates
backend = QibolabBackend(platform="my_platform")
backend.transpiler = Pipeline(
[
StarConnectivity(middle_qubit=2),
NativeGates(two_qubit_natives=NativeType.CZ),
]
)
In this case circuits will first be transpiled to respect the 5-qubit star connectivity, with qubit 2 as the middle qubit. This will potentially add some SWAP gates. Then all gates will be converted to native.
The compiler can be modified similarly, by adding new compilation rules or modifying existing ones. As explained in Transpiler and Compiler section, a rule is a function that accepts a Qibo gate and a Qibolab platform and returns the corresponding pulse sequence implementing this gate.
The following example shows how to modify the transpiler and compiler in order to execute a circuit containing a Pauli X gate using a single pi-pulse:
from qibo import gates
from qibo.models import Circuit
from qibolab.backends import QibolabBackend
# define the circuit
circuit = Circuit(1)
circuit.add(gates.X(0))
circuit.add(gates.M(0))
# define a compiler rule that translates X to the pi-pulse
def x_rule(gate, platform):
"""X gate applied with a single pi-pulse."""
qubit = gate.target_qubits[0]
sequence = PulseSequence()
sequence.add(platform.create_RX_pulse(qubit, start=0))
return sequence, {}
# the empty dictionary is needed because the X gate does not require any virtual Z-phases
backend = QibolabBackend(platform="my_platform")
# disable the transpiler (the default transpiler will attempt to convert X to U3)
backend.transpiler = None
# register the new X rule in the compiler
backend.compiler[gates.X] = x_rule
# execute the circuit
result = backend.execute_circuit(circuit, nshots=1000)
Here we completely disabled the transpiler to avoid transforming the X gate to a different gate and we added a rule that instructs the compiler how to transform the X gate.
The default set of compiler rules is defined in qibolab.compilers.default
.
Note
If the compiler receives a circuit that contains a gate for which it has no rule, an error will be raised. This means that the native gate set that the transpiler uses, should be compatible with the available compiler rules. If the transpiler is disabled, a rule should be available for all gates in the original circuit.
In the above examples we executed circuits using the backend backend.execute_circuit
method,
unlike the previous example (Circuit execution) where circuits were executed directly using circuit(nshots=1000)
.
It is possible to perform transpiler and compiler manipulation in both approaches.
When using circuit(nshots=1000)
, Qibo is automatically initializing a GlobalBackend()
singleton that is used to execute the circuit.
Therefore the previous manipulations can be done as follows:
import qibo
from qibo import gates
from qibo.models import Circuit
from qibo.backends import GlobalBackend
# define circuit
circuit = Circuit(1)
circuit.add(gates.U3(0, 0.1, 0.2, 0.3))
circuit.add(gates.M(0))
# set backend to qibolab
qibo.set_backend("qibolab", platform="my_platform")
# disable the transpiler
GlobalBackend().transpiler = None
# execute circuit
result = circuit(nshots=1000)