qp.transforms.cancel_inverses

cancel_inverses(tape, recursive=True)[source]

Quantum function transform to remove any operations that are applied next to their (self-)inverses or adjoint.

Parameters:
  • tape (QNode or QuantumTape or Callable) – A quantum circuit.

  • recursive (bool) –

    Whether or not to recursively cancel inverses after a first pair of mutual inverses has been cancelled. Enabled by default.

    Note

    This argument is not supported within a qjit() workflow.

Returns:

The transformed circuit as described in qp.transform.

Return type:

qnode (QNode) or quantum function (Callable) or tuple[List[QuantumTape], function]

Example

You can apply the cancel inverses transform directly on QNode.

>>> dev = qp.device('default.qubit', wires=3)
@qp.transforms.cancel_inverses
@qp.qnode(device=dev)
def circuit(x, y, z):
    qp.Hadamard(wires=0)
    qp.Hadamard(wires=1)
    qp.Hadamard(wires=0)
    qp.RX(x, wires=2)
    qp.RY(y, wires=1)
    qp.X(1)
    qp.RZ(z, wires=0)
    qp.RX(y, wires=2)
    qp.CNOT(wires=[0, 2])
    qp.X(1)
    return qp.expval(qp.Z(0))
>>> print(circuit(0.1, 0.2, 0.3))
1.0

You can also apply it on quantum functions:

def qfunc(x, y, z):
    qp.Hadamard(wires=0)
    qp.Hadamard(wires=1)
    qp.Hadamard(wires=0)
    qp.RX(x, wires=2)
    qp.RY(y, wires=1)
    qp.X(1)
    qp.RZ(z, wires=0)
    qp.RX(y, wires=2)
    qp.CNOT(wires=[0, 2])
    qp.X(1)
    return qp.expval(qp.Z(0))

The circuit before optimization:

>>> qnode = qp.QNode(qfunc, dev)
>>> print(qp.draw(qnode)(1, 2, 3))
0: ──H─────────H─────────RZ(3.00)─╭●────┤  <Z>
1: ──H─────────RY(2.00)──X────────│───X─┤
2: ──RX(1.00)──RX(2.00)───────────╰X────┤

We can see that there are two adjacent Hadamards on the first qubit that should cancel each other out. Similarly, there are two Pauli-X gates on the second qubit that should cancel. We can obtain a simplified circuit by running the cancel_inverses transform:

>>> optimized_qfunc = qp.transforms.cancel_inverses(qfunc)
>>> optimized_qnode = qp.QNode(optimized_qfunc, dev)
>>> print(qp.draw(optimized_qnode)(1, 2, 3))
0: ──RZ(3.00)───────────╭●─┤  <Z>
1: ──H─────────RY(2.00)─│──┤
2: ──RX(1.00)──RX(2.00)─╰X─┤

There are two key differences to note when using cancel_inverses with qjit:

dev = qp.device("lightning.qubit", wires=1)

@qp.qjit(capture=True)
@qp.transforms.cancel_inverses
@qp.qnode(dev)
def circuit():
    qp.RX(0.1, wires=0)
    qp.Hadamard(wires=0)
    qp.Hadamard(wires=0)
    return qp.expval(qp.PauliZ(0))
>>> print(qp.specs(circuit, level=1)())
Device: lightning.qubit
Device wires: 1
Shots: Shots(total=None)
Level: cancel-inverses

Wire allocations: 1
Total gates: 1
Gate counts:
- RX: 1
Measurements:
- expval(PauliZ): 1
Depth: Not computed

Additionally, the cancel_inverses transform with qjit supports loop-boundary optimization.

For more technical information on how this transform behaves, consult the Catalyst documentation for catalyst.passes.cancel_inverses().