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 with method = 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 the workspace 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 out qrisp.t_depth_indicator. 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 the compilation_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 is True.

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 is True.

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 either jones or gidney will be compiled to use a mid-circuit measurement. If set to False, a functionally equivalent (but less efficient version) will be used without a mid-circuit measurement. For more information see qrisp.mcx() The default is False.

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 the mcx-gate. This way qv has to be allocated before the mcx 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 efficient mcx 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 the depth 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) and T-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.