qrisp.QuantumSession.compile#
- QuantumSession.compile(workspace=0, intended_measurements=[], cancel_qfts=True, disable_uncomputation=True, compile_mcm=False, gate_speed=None)[source]#
Method to compile the QuantumSession into a QuantumCircuit. The compiler dynamically allocates the qubits of the QuantumSession on qubits that might have been used by priorly deleted QuantumVariables.
Using the
workspace
keyword, we can grant the compiler a number of extra qubits to use in order to reduce the circuit depth.Furthermore, the compiler recompiles any
mcx
instruction withmethod = auto
using a dynamically generated mcx implementation that makes use of as much of the currently available clean and dirty ancillae. This feature will never allocate additional qubits on its own. If required, it can be supplied with additional space using theworkspace
keyword.Another important feature of this function is gate speed aware compilation. Gate speed here means the amount of time each basis gate requires in a physical execution of the QuantumCircuit. For NISQ era devices, CNOT gates are a bottleneck, whereas FT era devices are expected to be bottlenecked by T-gates. While these are two important examples, more backend specific gate-speed specifications are possible. The Qrisp compiler can leverage several non-trivial commutation relations to reorder circuits such that the run-time is optimal. To tell the compiler, the time that is required for each gate, the
gate_speed
keyword argument exists. This argument should be a function of Operation objects, that returns a float indicating the gate speed. For an example of such a function, check outT-depth
. For further details, check the examples.The .compile method is called by default, when executing the
get_measurement
method of QuantumVariable. This method also allows specification of compilation option through thecompilation_kwargs
argument.- Parameters:
- workspaceint, optional
The amount of workspace qubits to be granted. The default is 0.
- intended_measurementslist[Qubit], optional
A list of Qubits that are supposed to be measured. The compiler will remove any instructions that are not directly neccessary to perform the measurements. Note that the resulting QuantumCircuit contains no measurements, such that the user can still specify a classical bit for the measurement. The default ist [].
- cancel_qftsbool, optional
If set to True, any
QFT
instruction that is executed on a set of qubits that have just been allocated (ie. the \(\ket{0}\) state) will be replaced by a set of H gates. The same goes for QFT instructions executed directly before deallocation. The default isTrue
.- disable_uncomputationbool, optional
Experimental feature the allows fully automized uncomputation. If set to
False
any QuantumVariable that went out of scope will be uncomputed by the compiler. The default isTrue
.- gate_speedfunction, optional
Enables the compiler to create circuits that are aware of differences in gate speed. For NISQ era devices, CNOT gates are a bottleneck, whereas FT era devices are expected to be bottlenecked by
- compile_mcmfunction, optional
If set to
True
, any instance of mcx gates with method eitherjones
orgidney
will be compiled to use a mid-circuit measurement. If set toFalse
, a functionally equivalent (but less efficient version) will be used without a mid-circuit measurement. For more information seeqrisp.mcx()
The default isFalse
.
- Returns:
- QuantumCircuit
The compiled QuantumCircuit.
Examples
Workspace
We calculate a product of 2 QuantumFloats using the
sbp_mult
function which heavily profits from more workspace.>>> from qrisp import QuantumFloat, sbp_mult >>> qf_0 = QuantumFloat(5) >>> qf_0[:] = 3 >>> qf_1 = QuantumFloat(5) >>> qf_1[:] = 5
Calculate product:
>>> qf_res = sbp_mult(qf_0, qf_1) >>> qf_res.qs.num_qubits() 45
Compile circuit with no workspace
>>> qc_0 = qf_res.qs.compile(0) >>> qc_0.num_qubits() 21 >>> qc_0.depth() 497
Compile circuit with 4 workspace qubits
>>> qc_1 = qf_res.qs.compile(4) >>> qc_1.num_qubits() 25 >>> qc_1.depth() 258
mcx recompilation
To demonstrate the recompilation feature, we create two QuantumVariables.
>>> from qrisp import QuantumVariable, mcx, cx >>> ctrl = QuantumVariable(4) >>> target = QuantumVariable(1) >>> mcx(ctrl, target) >>> print(ctrl.qs)
QuantumCircuit: -------------- ctrl.0: ──■── │ ctrl.1: ──■── │ ctrl.2: ──■── │ ctrl.3: ──■── ┌─┴─┐ target.0: ┤ X ├ └───┘ Live QuantumVariables: --------------------- QuantumVariable ctrl QuantumVariable target
We can now call the
.compile
method>>> compiled_qc = ctrl.qs.compile() >>> compiled_qc.depth() 50 >>> print(compiled_qc)
ctrl.0: ──■── │ ctrl.1: ──■── │ ctrl.2: ──■── │ ctrl.3: ──■── ┌─┴─┐ target.0: ┤ X ├ └───┘
We see no change here, because there was no free space to execute a more optimal mcx implementation. We can grant additional space using the
workspace
argument:>>> compiled_qc = ctrl.qs.compile(workspace = 2) >>> compiled_qc.depth() 22 >>> print(compiled_qc)
┌────────┐ ┌────────┐ ctrl.0: ┤0 ├───────────────┤0 ├────────── │ │ │ │ ctrl.1: ┤1 ├───────────────┤1 ├────────── │ │┌────────┐ │ │┌────────┐ ctrl.2: ┤ ├┤0 ├─────┤ ├┤0 ├ │ pt2cx ││ │ │ pt2cx ││ │ ctrl.3: ┤ ├┤1 ├─────┤ ├┤1 ├ │ ││ │┌───┐│ ││ │ target.0: ┤ ├┤ pt2cx ├┤ X ├┤ ├┤ pt2cx ├ │ ││ │└─┬─┘│ ││ │ workspace_0: ┤2 ├┤ ├──■──┤2 ├┤ ├ └────────┘│ │ │ └────────┘│ │ workspace_1: ──────────┤2 ├──■────────────┤2 ├ └────────┘ └────────┘
Granting extra qubits to use this feature is however not usually necessary. The compiler automatically detects and reuses qubit resources available at the corresponding stage of the compilation. To demonstrate this feature, we allocate a third QuantumVariable:
>>> qv = QuantumVariable(2) >>> cx(target[0], qv) >>> print(ctrl.qs.compile())
┌────────┐ ┌────────┐ ctrl.0: ┤0 ├───────────────┤0 ├──────────────────── │ │ │ │ ctrl.1: ┤1 ├───────────────┤1 ├──────────────────── │ │┌────────┐ │ │┌────────┐ ctrl.2: ┤ ├┤0 ├─────┤ ├┤0 ├────────── │ pt2cx ││ │ │ pt2cx ││ │ ctrl.3: ┤ ├┤1 ├─────┤ ├┤1 ├────────── │ ││ │┌───┐│ ││ │ target.0: ┤ ├┤ pt2cx ├┤ X ├┤ ├┤ pt2cx ├──■────■── │ ││ │└─┬─┘│ ││ │┌─┴─┐ │ qv.0: ┤2 ├┤ ├──■──┤2 ├┤ ├┤ X ├──┼── └────────┘│ │ │ └────────┘│ │└───┘┌─┴─┐ qv.1: ──────────┤2 ├──■────────────┤2 ├─────┤ X ├ └────────┘ └────────┘ └───┘
We see how the qubits that will later hold
qv
are used to efficiently compile the mcx gate.In situations of no free clean ancilla qubits, the Qrisp compiler even makes use of dirty ancillae. To demonstrate, we again create three QuantumVariables but this time we execute a
cx
-gate before executing themcx
-gate. This wayqv
has to be allocated before themcx
gate.>>> ctrl = QuantumVariable(4) >>> target = QuantumVariable(1) >>> qv = QuantumVariable(2) >>> cx(target[0], qv) >>> mcx(ctrl, target) >>> print(ctrl.qs.compile())
ctrl.0: ────────────────────────────────────■──────────────────────────» ┌─────────────────┐ │ ┌─────────────────┐ » ctrl.1: ───────────────┤1 ├──┼──┤1 ├─────» │ │ │ │ │ » ctrl.2: ───────────────┤2 ├──┼──┤2 ├─────» │ │ │ │ │ » ctrl.3: ────────────■──┤ ├──┼──┤ ├──■──» ┌─┴─┐│ reduced_maslov │ │ │ reduced_maslov │┌─┴─┐» target.0: ──■────■──┤ X ├┤ ├──┼──┤ ├┤ X ├» ┌─┴─┐ │ └─┬─┘│ │┌─┴─┐│ │└─┬─┘» qv.0: ┤ X ├──┼────┼──┤0 ├┤ X ├┤0 ├──┼──» └───┘┌─┴─┐ │ │ │└───┘│ │ │ » qv.1: ─────┤ X ├──■──┤3 ├─────┤3 ├──■──» └───┘ └─────────────────┘ └─────────────────┘ » « « ctrl.0: ─────────────────────■───────────────────── « ┌─────────────────┐ │ ┌─────────────────┐ « ctrl.1: ┤1 ├──┼──┤1 ├ « │ │ │ │ │ « ctrl.2: ┤2 ├──┼──┤2 ├ « │ │ │ │ │ « ctrl.3: ┤ ├──┼──┤ ├ « │ reduced_maslov │ │ │ reduced_maslov │ «target.0: ┤ ├──┼──┤ ├ « │ │┌─┴─┐│ │ « qv.0: ┤0 ├┤ X ├┤0 ├ « │ │└───┘│ │ « qv.1: ┤3 ├─────┤3 ├ « └─────────────────┘ └─────────────────┘
We see how the qubits of
qv
are utilized as dirty ancilla qubits in order to facilitate a more efficientmcx
implementation compared to no ancillae at all.Gate speed aware compilation
Next to the mentioned features, the
compile
method performs a variety of techniques of reordering the gate sequence (without changing the semantics, of course) to reduce the overall depth. Some of these techniques allow for a consideration of the gate speed, which enables a unique compilation workflow for each backend.The gate speed of the backend can be specified as a function of Operation objects:
def mock_gate_speed_0(op): if op.name == "x": return 1 if op.name == "y": return 10 else: return 0
This function describes a backend where the X-gate requires 1 time unit (for instance nanoseconds), the Y-gate requires 10 time units and every other gate can be executed instantaneusly.
We can now observe how this influences the compilation:
>>> from qrisp import QuantumVariable, x, y, cx >>> qv = QuantumVariable(3) >>> y(qv[0]) >>> x(qv[1]) >>> cx(qv[2], qv[:2]) >>> y(qv[1]) >>> x(qv[0]) >>> print(qv.qs) QuantumCircuit: --------------- ┌───┐┌───┐┌───┐ qv.0: ┤ Y ├┤ X ├┤ X ├───── ├───┤└─┬─┘├───┤┌───┐ qv.1: ┤ X ├──┼──┤ X ├┤ Y ├ └───┘ │ └─┬─┘└───┘ qv.2: ───────■────■─────── Live QuantumVariables: ---------------------- QuantumVariable qv
Because the CNOT gate on
qv.1
has to wait for the other CNOT gate (which takes a lot of time because of the costly y gate), the second y gate can only be executed delayed, making the total runtime of this circuit 20 time units.We can verify this using the
depth_indicator
keyword of thedepth
method:>>> qv.qs.depth(depth_indicator = mock_gate_speed_0) 20
Call the compile method, which automatically fixes the problem
>>> qc_fixed_0 = qv.qs.compile(gate_speed = mock_gate_speed_0) >>> print(qc_fixed_0) ┌───┐ ┌───┐┌───┐ qv.0: ┤ Y ├──────────┤ X ├┤ X ├ ├───┤┌───┐┌───┐└─┬─┘└───┘ qv.1: ┤ X ├┤ X ├┤ Y ├──┼─────── └───┘└─┬─┘└───┘ │ qv.2: ───────■─────────■───────
We see that the order of the CNOT gates has been switched (which doesn’t change the semantics) such that now
qv.1
no longer has to wait for the costly y gate.>>> qc_fixed_0.depth(depth_indicator = mock_gate_speed_0) 11
To see that the compilation function did not do this randomly, we can also create another
gate_speed
function.def mock_gate_speed_1(op): if op.name == "x": return 10 elif op.name == "y": return 1 else: return 0
>>> qc_fixed_1 = qv.qs.compile(gate_speed = mock_gate_speed_1) >>> print(qc_fixed_1) ┌───┐┌───┐┌───┐ qv.0: ┤ Y ├┤ X ├┤ X ├───── ├───┤└─┬─┘├───┤┌───┐ qv.1: ┤ X ├──┼──┤ X ├┤ Y ├ └───┘ │ └─┬─┘└───┘ qv.2: ───────■────■───────
Now the CNOT gate on
qv.0
is executed first, giving again a total depth of 11>>> qc_fixed_1.depth(depth_indicator = mock_gate_speed_1)
Qrisp has the two most important depth indicators in-built:
CNOT-depth
(NISQ) andT-depth
(FT).Fully automized uncomputation
This feature is as of right now experimental. To demonstrate, we create a test function, creating a local QuantumBool
from qrisp import QuantumBool, mcx def triple_AND(a, b, c): local = QuantumBool() result = QuantumBool() mcx([a,b], local) mcx([c, local], result) return result
>>> a = QuantumBool() >>> b = QuantumBool() >>> c = QuantumBool() >>> res = triple_AND(a,b,c) >>> print(res.qs)
QuantumCircuit: -------------- a.0: ──■─────── │ b.0: ──■─────── │ c.0: ──┼────■── ┌─┴─┐ │ local.0: ┤ X ├──■── └───┘┌─┴─┐ result.0: ─────┤ X ├ └───┘ Live QuantumVariables: --------------------- QuantumBool a QuantumBool b QuantumBool c QuantumBool local QuantumBool result
We now compile with the corresponding keyword argument:
>>> print(a.qs.compile(disable_uncomputation = False))
┌────────┐ ┌────────┐ a.0: ┤0 ├─────┤0 ├ │ │ │ │ b.0: ┤1 ├─────┤1 ├ │ │ │ │ c.0: ┤ pt2cx ├──■──┤ pt2cx ├ │ │┌─┴─┐│ │ result.0: ┤ ├┤ X ├┤ ├ │ │└─┬─┘│ │ workspace_0: ┤2 ├──■──┤2 ├ └────────┘ └────────┘
We see that the
local
QuantumBool is no longer allocated but has been uncomputed and it’s qubits are available as workspace.