Projector blocks
This section introduces the ProjectorBlock as an implementation for the quantum mechanical projection operation onto the subspace spanned by : . 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_tensorfrom 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, .
In fact, projectors can be used to compose any arbitrary operator. For example, the CNOT can be defined as and we can compare its matrix representation with the native one in Qadence:
from qadence.blocks import block_to_tensorfrom 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 . Indeed, it can be shown that their matricial representations are again identical:
from qadence.blocks import block_to_tensorfrom 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>)To examplify this point, let's run some non-unitary computation involving projectors.
from qadence import chain, runfrom 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 unnormalizedprojected_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]])