5

The semi-classical quantum Fourier transform works by repeatedly preparing a control qubit in a state $R_Z(\phi)|+\rangle$ (where $\phi$ is adapted depending on previous measurements), performing $U^m$ controlled by that qubit to get a phase kickback of $\theta \cdot m$ (where $m$ is a high power of $U$ to scale the kickback, and $\theta$ is the unknown eigenvalue), and then measuring the control qubit in the $X$ basis.

Each step has two inputs (the phase offset $\phi$ and the phase multiplier $m$) and produces one output (the measurement bit). In the usual QFT, each $m$ will be a different power of two and $\phi$ is created by treating the prior measurements as the bits past the decimal point of a binary number.

Suppose that instead of picking $\phi$ and $m$ in a structured way, they were simply chosen at random for each step. In the small numerical tests I've done (code at bottom of question) this still converges exponentially quickly towards $\theta$. If you feed the randomly chosen parameters and outputs from each step into a naive Bayesian inference, it quickly picks out the true $\theta$ from the other hypothesis angles:

enter image description here

enter image description here

So great, it works... except naive Bayesian inference requires space proportional to the number of candidate hypotheses. In real algorithms that could easily be $2^{1000}$ different hypothesis angles. The inference needs to be done in a more clever way for a classical control system to actually recover in the angle in practice, if the parameters are being chosen randomly.

Is there a way to do this angle inference problem more efficiently?

Desired Input: a list of phase offsets $\phi_k$, phase multipliers $m_k$, and measurement result bits $b_k$.

Desired Output: the maximum likelihood $\theta$ from $[0, 2\pi)$.

An example application would be making the semi-classical QFT compatible with the Zekendorf representation from https://arxiv.org/abs/2310.00899, where the multipliers are Fibonacci numbers instead of powers of 2.


import fractions
import math
import random

import numpy as np from matplotlib import pyplot as plt

def probability_of_1_from_phase_kickback( *, phase_offset: fractions.Fraction, eigenvalue_of_state_for_operation: fractions.Fraction, power_of_operation: int, ) -> float: """Probability of the following circuit returning 1 instead of 0:

       |0> ---H---Rz(phase_offset)----@---H---M=== result
                                      |
     |psi> --------------------------U**m--------- |psi>

Args:
    phase_offset: Amount of additional rotation independent of the operation or eigenvalue.
        Specified in fractions of a turn (phase_offset=0.5 is a rotation of pi radians).
    eigenvalue_of_state_for_operation: The amount of phase kickback expected from applying
        the operation U to the test state |psi>.
        Specified in fractions of a turn (eigenvalue_of_state_for_operation=0.5 is a rotation
        of pi radians).
    power_of_operation: How many times U is applied to |psi>, to accumulate phase kickback.
        (In practice it's assumed there must be some efficient way to synthesize U**m, rather
        than literally applying U a total of m times.)
"""
angle = phase_offset + eigenvalue_of_state_for_operation * power_of_operation
rads = float(angle) * 2 * math.pi
return np.cos(rads / 2)**2


def main(): n = 360*10 hypothesis_angles = [fractions.Fraction(k, n) for k in range(n)] true_angle = fractions.Fraction(75, 360) posterior = np.ones(shape=len(hypothesis_angles), dtype=np.float64) posterior /= sum(posterior)

fig: plt.Figure
ax: plt.Axes
fig, ax = plt.subplots()
ax.vlines(true_angle * 360, 10**-16, 1 - 10**-16, linestyle='--', color='red', label='true angle')
steps = 80
for step in range(steps):
    test_multiplier = random.randrange(1, n)
    test_angle = fractions.Fraction(random.randrange(1, n), n)
    p_actual = probability_of_1_from_phase_kickback(
        phase_offset=test_angle,
        eigenvalue_of_state_for_operation=true_angle,
        power_of_operation=test_multiplier,
    )
    measurement_result = random.random() < p_actual
    for k in range(len(hypothesis_angles)):
        p_hypothesis = probability_of_1_from_phase_kickback(
            phase_offset=test_angle,
            eigenvalue_of_state_for_operation=hypothesis_angles[k],
            power_of_operation=test_multiplier,
        )
        if not measurement_result:
            p_hypothesis = 1 - p_hypothesis
        posterior[k] *= p_hypothesis
    posterior /= sum(posterior)
    ax.scatter(
        [float(e)*360 for e in hypothesis_angles],
        posterior,
        label=f'step={step+1}' if step == 0 or step == steps//2 or step == steps -1 else None,
        marker='.',
        color=(
            1 - step / steps,
            1 - step / steps,
            1),
        s=80*(step/steps)**2,
    )

ax.set_ylabel("Posterior Probability (log-odds scale)")
ax.set_xlabel("Hypothesis Angle (degrees)")
def probability_to_log_odds(a: float | np.ndarray) -> float | np.ndarray:
    mask = (0 < a) * (a < 1)
    out = np.empty_like(a)
    e = a[mask]
    out[mask] = np.log(e / (1 - e))
    out[~(0 < a)] = -np.inf
    out[~(a < 1)] = np.inf
    return out
def log_odds_to_probability(a: float | np.ndarray) -> float | np.ndarray:
    mask = (-np.inf < a) * (a < np.inf)
    out = np.empty_like(a)
    e = np.exp(a[mask])
    out[mask] = e / (1 + e)
    out[~(-np.inf < a)] = 0
    out[~(a < np.inf)] = 1
    return out
ax.set_yscale("function", functions=[
    probability_to_log_odds,
    log_odds_to_probability,
])
m = 16
ax.set_ylim(10 ** -m / (10 ** -m + 1), 10 ** m / (10 ** m + 1))
ax.set_yticks([10 ** k / (10 ** k + 1) for k in range(-m, m+1)], labels=['1:1' if k == 0 else f'<span class="math-container">$1 : 10^{{{-k}}}$</span>' for k in range(-m, m+1)])
ax.legend()
plt.show()


if name == 'main': main()

Craig Gidney
  • 47,099
  • 1
  • 44
  • 119

0 Answers0