Skip to content

Using QSVT for Fixed-point Amplitude Amplification

View on GitHub Experiment in the IDE

This demo shows how to use the QSVT framework for search problems; specifically, implementing fixed-point amplitude amplification (FPAA). With FPAA, we do not know in advance the concentration of solutions for the search problem, but we want to sample a solution with high probability. In contrast, for the original Grover search algorithm, too many iterations might 'overshoot' the mark.

The demo is based on the paper Grand unification of quantum algorithms.

Given \(|s\rangle\) the initial state and \(|t\rangle\) the 'good' states, we get an effective block encoding of a one-dimensional matrix \(A=|t\rangle\langle s|\).

Given that \(a = \langle s|t\rangle\gt0\), we want to amplify \(a\). The signal operator \(U\) here is \(I\) (and also \(\dagger{U}\)). Now we implement two projector-rotations: one in the '\(|s\rangle\)' space and one in the '\(|t\rangle\)' space; each one around the given state, giving phase to the specific state.

Defining the QSVT Circuit for the Problem

We start with the general QSVT framework definition. It accepts a unitary that block-encodes a matrix together with projector-controlled phase functions, which rotate the state around each of the subspaces where the matrix is encoded.

It applies the qsvt_step multiple times, iterating over the rotation angles provided that encode the polynomial transformation.

Note that the last step is quite tricky and depends on the specific transformation to perform. The code here is suitable for the FP Amplitude Amplification case. In addition, we wrap the auxilliary qubit with \(H\) gates.

from classiq import *
from classiq.qmod.symbolic import floor, logical_and


@qfunc
def my_qsvt_step(
    phase1: CReal,
    phase2: CReal,
    proj_ctrl_phase_1: QCallable[CReal, QArray[QBit], QBit],
    proj_ctrl_phase_2: QCallable[CReal, QArray[QBit], QBit],
    u: QCallable[QArray[QBit]],
    state: QArray[QBit],
    aux: QBit,
):
    u(state)
    proj_ctrl_phase_2(phase1, state, aux)
    invert(lambda: u(state))
    proj_ctrl_phase_1(phase2, state, aux)


@qfunc
def my_qsvt(
    phase_seq: CArray[CReal],
    proj_ctrl_phase_1: QCallable[CReal, QArray[QBit], QBit],
    proj_ctrl_phase_2: QCallable[CReal, QArray[QBit], QBit],
    u: QCallable[QArray[QBit]],
    state: QArray[QBit],
    aux: QBit,
) -> None:
    H(aux)

    proj_ctrl_phase_1(phase_seq[0], state, aux)
    repeat(
        count=floor((phase_seq.len - 1) / 2),
        iteration=lambda index: my_qsvt_step(
            phase_seq[2 * index + 1],
            phase_seq[2 * index + 2],
            proj_ctrl_phase_1,
            proj_ctrl_phase_2,
            u,
            state,
            aux,
        ),
    )

    if_(
        condition=phase_seq.len % 2 == 1,
        then=lambda: IDENTITY(state),
        else_=lambda: (
            u(state),
            proj_ctrl_phase_2(
                phase_seq[phase_seq.len - 1],
                state,
                aux,
            ),
        ),
    )

    H(aux)

Initial State z-rotation: Rotation Around \(|s\rangle\)

def initial_state_rot(phase: CReal, state: QArray[QBit], aux: QBit):
    hadamard_transform(state)
    apply_to_all(X, state)
    control(ctrl=state, stmt_block=lambda: X(aux))
    RZ(phase, aux)
    control(ctrl=state, stmt_block=lambda: X(aux))
    apply_to_all(X, state)
    hadamard_transform(state)

Good States z-rotation: Rotation Around \(|t\rangle\)

def target_state_rot(
    phase: CReal,
    arith_oracle: QCallable[QArray[QBit], QBit],
    state: QArray[QBit],
    aux: QBit,
):
    arith_oracle(state, aux)
    RZ(phase, aux)
    arith_oracle(state, aux)

Defining the Arithmetic Oracle

We implement the following equation: (a + b) == 3 and (c - a) == 2 with a, b, c in sizes 2, 1, 3:

class OracleVars(QStruct):
    a: QNum[2, False, 0]
    b: QNum[1, False, 0]
    c: QNum[3, False, 0]


REG_SIZE = 6


@qfunc
def arith_equation(state: OracleVars, res: QBit):
    res ^= logical_and((state.a + state.b) == 3, (state.c - state.a) == 2)

Wrapping Everything for the FPAA Case

In the FPAA case, the provided unitary is just the Identity matrix! In addition, we provide both projector-controlled phase functions for the initial and target state rotations:

@qfunc
def qsvt_fpaa(
    phase_seq: CArray[CReal],
    arith_oracle: QCallable[QArray[QBit], QBit],
    state: QArray[QBit],
    aux: Output[QBit],
) -> None:
    allocate(1, aux)

    my_qsvt(
        phase_seq,
        lambda phase, state, aux: initial_state_rot(phase, state, aux),
        lambda phase, state, aux: target_state_rot(phase, arith_oracle, state, aux),
        lambda state: IDENTITY(target=state),
        state,
        aux,
    )

Getting the Phase Sequence for the Sign Function

We use the pyqsp package to receive the phases of the rotation sequence and get the coefficient of the sign function directly, based on the ERFC approximation:

# The following code assumes pyqsp version 0.1.6
# !pip install pyqsp==0.1.6
import pyqsp

DEGREE = 25
X_BASIS = True

pg = pyqsp.poly.PolySign()
pcoefs, scale = pg.generate(
    degree=DEGREE, delta=10, ensure_bounded=True, return_scale=True
)
[pyqsp.poly.PolySign] degree=25, delta=10
[PolyTaylorSeries] max [0.89999962] is at [1.]: normalizing
[PolyTaylorSeries] average error = 0.09627807622454267 in the domain [-1, 1] using degree 25
import numpy as np
from pyqsp.angle_sequence import Polynomial, QuantumSignalProcessingPhases

poly = Polynomial(pcoefs)
measurement = "x"
ang_seq = QuantumSignalProcessingPhases(
    poly, signal_operator="Wx", method="laurent", measurement=measurement
)
pyqsp.response.PlotQSPResponse(ang_seq, signal_operator="Wx", measurement=measurement)

png

As \(R(a)=-i*e^{i\frac{\pi}{4}Z}W(a)e^{i\frac{\pi}{4}Z}\) and we have an odd number of rotations, we get an \(i\) phase for our polynomial, so we get \(Im(P(a))\) instead of the real part. So we get the result in the \(|1\rangle\) state in the ancilla. However, we can fix it by adding an \(\pi/2\) phase to the last or first rotation.

#### change the R(x) to W(x), as the phases are in the W(x) conventions
phases = np.array(ang_seq)
phases[1:-1] = phases[1:-1] - np.pi / 2
phases[0] = phases[0] - np.pi / 4
phases[-1] = phases[-1] + (2 * (len(phases) - 1) - 1) * np.pi / 4

phases = (
    -2 * phases
)  # verify conventions. minus is due to exp(-i*phi*z) in qsvt in comparison to qsp

Creating the Full QSVT Model

@qfunc
def main(state: Output[OracleVars], aux: Output[QBit]):
    allocate(state.size, state)
    hadamard_transform(state)

    qsvt_fpaa(
        phase_seq=list(phases),
        arith_oracle=arith_equation,
        state=state,
        aux=aux,
    )

Synthesizing and Executing on a Simulator

We use the Classiq synthesis engine to translate the model to a quantum circuit, and execute on the Classiq simulator:

from classiq.execution import (
    ClassiqBackendPreferences,
    ClassiqSimulatorBackendNames,
    ExecutionPreferences,
)

constraints = Constraints(max_width=12)

NUM_SHOTS = 1000
# we will want to execute this qmod on a state-vector simulator:
execution_preferences = ExecutionPreferences(
    num_shots=NUM_SHOTS,
    backend_preferences=ClassiqBackendPreferences(
        backend_name=ClassiqSimulatorBackendNames.SIMULATOR
    ),
)

qmod = create_model(
    main,
    constraints,
    execution_preferences,
    out_file="qsvt_fixed_point_amplitude_amplification",
)
qprog = synthesize(qmod)
show(qprog)

Execute the circuit:

result = execute(qprog).result_value()
def equation(a, b, c):
    return ((a + b) == 3) and ((c - a) == 2)


measured_good_shots = 0

for r in result.parsed_counts:
    a, b, c, aux = (
        r.state["state"]["a"],
        r.state["state"]["b"],
        r.state["state"]["c"],
        r.state["aux"],
    )
    if equation(a, b, c) and (aux == 0):
        print(
            f"a: {a}, b: {b}, c: {c}, aux: {aux}, equation_result: {equation(a, b, c)}, counts={r.shots}"
        )
        measured_good_shots += r.shots

print("Measured good shots:", measured_good_shots)
a: 2, b: 1, c: 4, aux: 0.0, equation_result: True, counts=419
a: 3, b: 0, c: 5, aux: 0.0, equation_result: True, counts=394
Measured good shots: 813

What do we expect?

We need to substitute the amplitude of \(|s\rangle\langle t|\) in \(P(x)\):

p_good_shot = poly(np.sqrt(2 / 2**6)) ** 2
print("Expected good shots:", NUM_SHOTS * p_good_shot)
Expected good shots: 832.9890148378344

Indeed, we received the expected result according to the polynomial we created with the QSVT sequence:

import scipy

assert np.isclose(
    measured_good_shots,
    NUM_SHOTS * p_good_shot,
    atol=5 * scipy.stats.binom.std(NUM_SHOTS, p_good_shot),
)

References

[1]: Martyn JM, Rossi ZM, Tan AK, Chuang IL. Grand unification of quantum algorithms. PRX Quantum. 2021 Dec 3;2(4):040203.