Hamming weight phasing¶
In this notebook, we'll go over the circuit for a rotation synthesis technique called "Hamming weight phasing." This method is another flavor of synthesizing rotations via adding into a phase gradient catalyst state. Instead of synthesizing a single rotation (of type phase or rz), one can implement a set or "tower" of these rotations of the same angle on parallel qubits! The core idea remains the same: we're making use of a phase kickback from adding into a phase gradient state.
Let us look at the circuit we'd like to implement:
%load_ext autoreload
%autoreload 2
import numpy as np
from psiqworkbench import QPU, Qubits
from psiqworkbench.utils.rotation_utils import handle_rotation
from workbench_algorithms import HammingWeightPhasing, ComputeHammingWeightNaive, ComputeHammingWeightGroupOfThrees
# Angle of the set of rotations we'd like to implement
true_angle = 167.872
bits_of_precision = 6
# In practice, we'll support a finite bit precision
angle = handle_rotation(angle=true_angle, bits_of_precision=bits_of_precision)
print(f'Truncated angle: {angle}')
# Number of target qubits in the rotation tower
number_of_target_qubits = 5
Truncated angle: 168.75
# Visualize the tower of rotations
qc = QPU()
qc.reset(number_of_target_qubits)
tgts = Qubits(number_of_target_qubits, 'Targets', qc)
tgts.phase(angle)
qc.draw()
Instead of synthesizing each phase rotation (in the "rotation tower") one by one, we can do it in one-go (i.e. one addition)!
This is achieved by first computing the Hamming weight in another log-sized register, then applying a "Phasing circuit" to the log-sized register, followed by un-computing the Hamming weight.
The "Phasing circuit" consists of a cascade of half-adders before applying a final rotation. This circuit applies rotations of angle*(2**i) for the i-th qubit of the target_reg where i runs from 0 to len(target_reg) - 1. You can see this more easily by setting use_catalyst_state to False!
# Set this to False to see the tower of rotations with exponentially increasing angles
use_catalyst_state = True
total_number_of_qubits = number_of_target_qubits + int(np.ceil(np.log2(number_of_target_qubits)))
size_of_catalyst_state = max(1, int(np.ceil(np.log2(number_of_target_qubits))) + 1)
total_number_of_qubits += (2*size_of_catalyst_state) - 1 + 1
qc = QPU()
qc.reset(total_number_of_qubits + 1)
qc.set_random()
ctrl = Qubits(1, "ctrl", qc)
target = Qubits(number_of_target_qubits, "targets", qc)
reset_other_qubits = Qubits(total_number_of_qubits - number_of_target_qubits, "reset", qc)
reset_other_qubits.write(0)
reset_other_qubits.release()
init = qc.pull_state()
cat_angle = (1 << (size_of_catalyst_state - 1)) * angle
with qc.fetch_rotation_catalyst_state(cat_angle, size_of_catalyst_state) as catalyst_state_reg:
hwp = HammingWeightPhasing(angle,
hamming_weight_qubrick=ComputeHammingWeightGroupOfThrees(),
use_catalyst_state=use_catalyst_state,
catalyst_state_reg=catalyst_state_reg,
)
hwp.compute(target, ctrl=ctrl)
qc.release_all_rotation_catalyst_qubits()
final_state = qc.pull_state()
qc.draw()
Note that the rotation gates in the catalyst and the single rotation at the bottom of the phasing ancillae (bottom of the ladder in the Phasing circuit) need to be synthesized. However, the catalyst can be re-used, and synthesizing a single rotation and using an adder can often by cheaper than synthesizing some $k$ rotations (on the targets register) individually. This can especially be the case if we apply these towers of rotations over and over again (e.g. when you're using Trotterization to simulate the Fermi-Hubbard model).
Going back to our demo, in order to check our output state, let's construct the reference state:
# Reference state
qc = QPU()
qc.reset(total_number_of_qubits + 1)
ctrl = Qubits(1, "ctrl", qc)
target = Qubits(number_of_target_qubits, "target", qc)
reset_other_qubits = Qubits(total_number_of_qubits - number_of_target_qubits, "reset", qc)
reset_other_qubits.write(0)
reset_other_qubits.release()
# Insert same initial random state
qc.push_state(init)
# Apply phase gates to the target register
# This creates the tower of rotations
target.phase(angle)
expected_state = qc.pull_state()
# Check that the two states are the same!
assert np.allclose(final_state, expected_state)