Projector blocks

This section introduces the ProjectorBlock as an implementation for the quantum mechanical projection operation onto the subspace spanned by \(|a\rangle\): \(\mathbb{\hat{P}}=|a\rangle \langle a|\). It evaluates the outer product for bras and kets expressed as bitstrings for a given qubit support. They have to possess matching lengths.

from qadence.blocks import block_to_tensor
from qadence.operations import Projector  # Projector as an operation.

# Define a projector for |1> onto the qubit labelled 0.
projector_block = Projector(ket="1", bra="1", qubit_support=0)

# As any block, the matrix representation can be retrieved.
projector_matrix = block_to_tensor(projector_block)
projector matrix = tensor([[[0.+0.j, 0.+0.j],
         [0.+0.j, 1.+0.j]]])

Other standard operations are expressed as projectors in Qadence. For instance, the number operator is the projector onto the 1-subspace, \(N=|1\rangle\langle 1|\).

In fact, projectors can be used to compose any arbitrary operator. For example, the CNOT can be defined as \(\textrm{CNOT}(i,j)=|0\rangle\langle 0|_i\otimes \mathbb{I}_j+|1\rangle\langle 1|_i\otimes X_j\) and we can compare its matrix representation with the native one in Qadence:

from qadence.blocks import block_to_tensor
from qadence import kron, I, X, CNOT

# Define a projector for |0> onto the qubit labelled 0.
projector0 = Projector(ket="0", bra="0", qubit_support=0)

# Define a projector for |1> onto the qubit labelled 0.
projector1 = Projector(ket="1", bra="1", qubit_support=0)

# Construct the projector controlled CNOT.
projector_cnot = kron(projector0, I(1)) + kron(projector1, X(1))

# Get the underlying unitary.
projector_cnot_matrix = block_to_tensor(projector_cnot)

# Qadence CNOT unitary.
qadence_cnot_matrix = block_to_tensor(CNOT(0,1))
projector cnot matrix = tensor([[[1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j],
         [0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j],
         [0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j],
         [0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j]]])
qadence cnot matrix = tensor([[[1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j],
         [0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j],
         [0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j],
         [0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j]]], grad_fn=<AddBackward0>)

Another example is the canonical SWAP unitary that can be defined as \(SWAP=|00\rangle\langle 00|+|01\rangle\langle 10|+|10\rangle\langle 01|+|11\rangle\langle 11|\). Indeed, it can be shown that their matricial representations are again identical:

from qadence.blocks import block_to_tensor
from qadence import SWAP

# Define all projectors.
projector00 = Projector(ket="00", bra="00", qubit_support=(0, 1))
projector01 = Projector(ket="01", bra="10", qubit_support=(0, 1))
projector10 = Projector(ket="10", bra="01", qubit_support=(0, 1))
projector11 = Projector(ket="11", bra="11", qubit_support=(0, 1))

# Construct the SWAP gate.
projector_swap = projector00 + projector10 + projector01 + projector11

# Get the underlying unitary.
projector_swap_matrix = block_to_tensor(projector_swap)

# Qadence SWAP unitary.
qadence_swap_matrix = block_to_tensor(SWAP(0,1))
projector swap matrix = tensor([[[1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j],
         [0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j],
         [0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j],
         [0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j]]])
qadence swap matrix = tensor([[[1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j],
         [0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j],
         [0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j],
         [0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j]]], grad_fn=<UnsafeViewBackward0>)

Warning

Projectors are non-unitary operators, only supported by the PyQTorch backend.

To examplify this point, let's run some non-unitary computation involving projectors.

from qadence import chain, run
from qadence.operations import H, CNOT

# Define a projector for |1> onto the qubit labelled 1.
projector_block = Projector(ket="1", bra="1", qubit_support=1)

# Some non-unitary computation.
non_unitary_block = chain(H(0), CNOT(0,1), projector_block)

# Projected wavefunction becomes unnormalized
projected_wf = run(non_unitary_block)  # Run on PyQTorch.
projected_wf = tensor([[0.0000+0.j, 0.0000+0.j, 0.0000+0.j, 0.7071+0.j]])