Majorana Fermion
Majorana Fermion Operator¶
$$ \newcommand{\ket}[1]{|#1\rangle} \newcommand{\bra}[1]{\langle#1|} \newcommand{\norm}[1]{\left\lVert#1\right\rVert} $$
Since the name of this Qubrick is not descriptive at all, we're going to go through a small tutorial to describe where it comes from and what it does.
Let's get started!
Where it comes from¶
To give credit where credit's due, this operation comes from the paper "Encoding Electronic Spectra in Quantum Circuits with Linear T Complexity" ⧉. It is shown in different forms throughout, but our implementation is designed to replicate the circuit diagram in Figure 9.
What it does¶
Although it may not be obvious from the circuit diagram, the action of this Qubrick is actually relatively straightforward.
Given a target system ($\ket{\psi}$) and an "index register" ($\ket{l}$), this circuit applies the operator $Z_0Z_1...Z_{l-1}Y_{l}$ onto the target system:
$$ \ket{\psi} \ket{l} \rightarrow Z_0Z_1...Z_{l-1}Y_{l}\ket{\psi} \ket{l} $$
In words: for each qubit in $\ket{\psi}$ at an index lower than $\ket{l}$ apply $Z$, for the qubit in $\ket{\psi}$ at index $\ket{l}$ apply $Y$, and leave the remaining qubits alone. This is why the authors of the paper call this Qubrick a "ranged" operation since it applies the $Z$ operator onto every qubit in $\ket{\psi}$ up to the qubit at the index labeled by $l$.
But keep in mind, this is typically done when $\ket{l}$ is in superposition, so all these operations are happing on different branches corresponding to the computational basis state of $\ket{l}$ on that branch! As such, the size of the index register ($\ket{l}$) should typically consist of at most $\log_2(\texttt{len}(\ket{\psi}))$ qubits so that any state of $\ket{l}$ corresponds to an index within the size of $\ket{\psi}$, otherwise the action on $\ket{\psi}$ is not defined.
Let's see it in action!
%load_ext autoreload
%autoreload 2
from psiqworkbench import QPU, Qubits
from workbench_algorithms import MajoranaFermionOperator, ZeroAncMultiplexor
import numpy as np
# The MajoranaFermionOperator Qubrick will work with any multiplexor, so here we'll use the ZeroAncMultiplexor
# since it's easiest to understand visually, but don't hesitate to try other multiplexors!
size_of_psi = 4
index_register_value = 2
qc = QPU()
number_of_index_qubits = int(np.ceil(np.log2(size_of_psi)))
# The MajoranaFermionOperator uses a temporary ancilla qubit called the `accumulator`
# to keep track of which index we are at, hence the additional +1 in the next line
total_number_of_qubits = size_of_psi + number_of_index_qubits + 1
qc.reset(total_number_of_qubits)
index = Qubits(number_of_index_qubits, "index", qc)
psi = Qubits(size_of_psi, "psi", qc)
# Here we can use any function with the same signature, but typically this is a qpu operation like qc.had or
# qc.x. This is what determines which operator is applied onto the l'th qubit of psi. In the above example,
# this is the Y operator, so that's what we'll use here.
operator = qc.y
majorana_select = MajoranaFermionOperator(operator, ZeroAncMultiplexor)
index.write(index_register_value)
# Run majorana operator
majorana_select.compute(index, psi)
qc.draw()
qc.print_state_vector()
|index|psi|?> |2|4|.> 0.000000+1.000000j
'|index|psi|?>\n|2|4|.> 0.000000+1.000000j\n'
In the above example, we set the index_register_value to be 2 so that the state of $\ket{l}$ is $\ket{2}$. Therefore, we applied the $Z_0Z_1Y_2$ operator onto $\ket{\psi}$, flipping the state of the 0x4 qubit and applying a phase of $+i$.
What do you think would happen if we set index_register_value to be a different computational basis state? What would happen if we put it into a superposition state? Give it a try!
We can also change the operation that we apply on the $l^{th}$ qubit by changing the operator attribute to be any function that has the signature: operator(target_qubits, control_conditions)
from functools import partial
qc = QPU()
qc.reset(total_number_of_qubits)
index = Qubits(number_of_index_qubits, "index", qc)
psi = Qubits(size_of_psi, "psi", qc)
operator = partial(qc.ry, 90)
majorana_select = MajoranaFermionOperator(operator, ZeroAncMultiplexor)
index.write(index_register_value)
# Run majorana operator
majorana_select.compute(index, psi)
qc.draw()
qc.print_state_vector()
|index|psi|?> |2|0|.> 0.707107+0.000000j |2|4|.> 0.707107+0.000000j
'|index|psi|?>\n|2|0|.> 0.707107+0.000000j\n|2|4|.> 0.707107+0.000000j\n'
Multiplexors¶
We can also use different multiplexors to implement the coherent for-loop over the computational basis states of index. In the previous example, we used ZeroAncMultiplexor which operates similarly to a classical for-loop where it performs the conditions of the for-loop serially by acting on $\ket{\psi}$ with explicit conditioning the index register being in the appropriate computational basis state. However, although this multiplexor is the most efficient for simulators since it uses no additional ancillae, we can use other multiplexors that use ancillae, but reduce the total number of operations.
Let's see an example of using one of the multiplexors introduced in the same paper "Encoding Electronic Spectra in Quantum Circuits with Linear T Complexity" ⧉: the BinaryTreeMultiplexor.
from workbench_algorithms import BinaryTreeMultiplexor
import numpy as np
size_of_psi = 4
index_register_value = 2
qc = QPU()
number_of_index_qubits = int(np.ceil(np.log2(size_of_psi)))
total_number_of_qubits = size_of_psi + number_of_index_qubits + 3
qc.reset(total_number_of_qubits)
index = Qubits(number_of_index_qubits, "index", qc)
psi = Qubits(size_of_psi, "psi", qc)
operator = qc.y
majorana_select = MajoranaFermionOperator(operator, BinaryTreeMultiplexor)
index.write(index_register_value)
# Run majorana operator
majorana_select.compute(index, psi)
qc.draw()
qc.print_state_vector()
|index|psi|?> |2|4|.> 0.000000+1.000000j
'|index|psi|?>\n|2|4|.> 0.000000+1.000000j\n'
And you can see we get the same result as we did previously! And although it may not be obvious, we are performing the same logic, but using fewer costly operations (Toffoli gates) than we did when using the ZeroAncMultiplexor - that's a big win!
For more information on multiplexing and the different multiplexors implemented in workbench_algorithms, check out the tutorial titled "Simple Multiplexing Examples".