Canonical phase estimation

Download Notebook - canonical_phase_estimation.ipynb

  • ‘Canonical’ phase estimation reads out the bitstring representation of eigenphases of a unitary matrix by repeated application of the controlled unitary onto a phase register.

  • It relies on the use of the inverse Quantum Fourier Transform to transform a cosine wave in the time-domain into a delta function in the frequency (phase) domain.

  • To prepare the signal, binary powers (U^2^1, U^2^2, etc) of the unitary are used to express a phase kickback, which is entangled with the system register.

  • The controlled unitary here is defined with a guppy function, with the system and ancilla registers and unitary power as its arguments.

from guppylang.decorator import guppy
from guppylang.std.builtins import array, output, nat
from guppylang.std.quantum import (
    measure_array,
    qubit,
    discard_array,
    ry,
    crz,
    collect_measurements
)
from guppylang.std.builtins import comptime
from guppylang.std.angles import angle, pi
from guppyalgos.utils import qarray

from guppyalgos.algorithms.phase_estimation import qpe

from guppyalgos.primitives.state_preparation import uniform_state

from guppyalgos.utils import binary_fraction

Here we prepare a toy system with Rz as the Hamiltonian. The powers of the controlled-Rz encode phi onto the phase register. To mirror the more general UnitaryRegs pattern, we wrap the system register in a small RzUnitaryRegs struct and let the oracle access unitary_regs.system. Linear combinations of the \(\ket{0}\) and \(\ket{1}\) eigenstates of this unitary can be prepared on the system register with an Ry gate. Their eigenphases are -phi and phi respectively.

n_ancilla = 4
n_system = 1
phi = 0.75


@guppy.struct
class RzUnitaryRegs[n_q_s: nat]:
    """Wrap the system register for the unitary application.

    Using a struct here keeps the QPE interface compatible with more complex
    unitaries that may need multiple registers, not just a single system array.

    """
    system: array[qubit, n_q_s]

@guppy
def power_oracle[n_q_s: nat](ctrl: qubit, unitary_regs: RzUnitaryRegs[n_q_s], power: int) -> None:
    """Use CRz gate to implement controlled e^-iπHt/2 for H = 0.5 * Z.

    Args:
        ctrl: Control qubit.
        unitary_regs: Registers acted on by the powered unitary.
        power: Exponent power.

    Returns:
        None

    Note: This oracle is parametrerized by the power.

    """
    crz(ctrl, unitary_regs.system[0], -2 * pi * phi * power)


@guppy
def state_preparation[n_q_s: nat](system_register: array[qubit, n_q_s]) -> None:
    """Uses Ry to create arbitrary linear combination of the simple oracle eigenstates."""
    # a value of 1 yields the |0> state with eigenphase -phi
    ry(system_register[0], angle(0))


# prebuild guppy functions for generic arrays
ancilla_prep_function = uniform_state(2**n_ancilla)

@guppy
def abstract_canonical_qpe() -> None:
    """Abstract canonical QPE algorithm using functions for state prep and oracle."""
    # The unitary application can use a richer regs object than a bare qubit array.
    unitary_regs = RzUnitaryRegs(qarray(n_system))
    phase_reg = qarray(n_ancilla)
    state_preparation(unitary_regs.system)
    ancilla_prep_function(phase_reg)
    qpe(phase_reg, unitary_regs, power_oracle)

    output("qpe_bitstring", collect_measurements(measure_array(phase_reg)))
    discard_array(unitary_regs.system)


#abstract_canonical_qpe.compile_function()
n_shots = 500

sim_result = (
    abstract_canonical_qpe.emulator(n_qubits=n_ancilla + 1)
    .with_seed(5)
    .with_shots(n_shots)
    .run()
)
result_counter = sim_result.register_counts()["qpe_bitstring"]
print(result_counter)

print("Target phase:", phi)
for key, value in result_counter.items():
    print(
        f"Measured bitstring: {key}, counts: {value},\
              phase: {binary_fraction([int(bit) for bit in key])},\
                  empirical probability: {value / n_shots}"
    )
Counter({'0110': 500})
Target phase: 0.75
Measured bitstring: 0110, counts: 500,              phase: 0.75,                  empirical probability: 1.0
# If the unitary only acts on a single qubit array, you can still pass that
# array directly into qpe without defining a new regs struct.
# however, due to typing, we do have to redefine the unitary

@guppy
def power_oracle_array[n_q_s: nat](ctrl: qubit, system_register: array[qubit, n_q_s], power: int) -> None:
    """Bare-array version of the same controlled Rz oracle."""
    crz(ctrl, system_register[0], -2 * pi * phi * power)


@guppy
def abstract_canonical_qpe_single_array() -> None:
    """Canonical QPE also works when the unitary registers are just an array."""
    system_register = qarray(n_system)
    phase_reg = qarray(n_ancilla)
    state_preparation(system_register)
    ancilla_prep_function(phase_reg)
    qpe(phase_reg, system_register, power_oracle_array)

    output("qpe_bitstring_single_array", collect_measurements(measure_array(phase_reg)))
    discard_array(system_register)


n_shots = 500

sim_result = (
    abstract_canonical_qpe_single_array.emulator(n_qubits=n_ancilla + 1)
    .with_seed(5)
    .with_shots(n_shots)
    .run()
)
result_counter = sim_result.register_counts()["qpe_bitstring_single_array"]
print(result_counter)

print("Target phase:", phi)
for key, value in result_counter.items():
    print(
        f"Measured bitstring: {key}, counts: {value},\
              phase: {binary_fraction([int(bit) for bit in key])},\
                  empirical probability: {value / n_shots}"
    )
Counter({'0110': 500})
Target phase: 0.75
Measured bitstring: 0110, counts: 500,              phase: 0.75,                  empirical probability: 1.0