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)
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.