Parity with measurement and feed-forward

Download Notebook - parity_laqcc.ipynb

  • parity_laqcc combines several input qubits into one parity target. It XORs the input parity into the target:

\[ |x_0,\ldots,x_{n-1}\rangle|t\rangle \longmapsto |x_0,\ldots,x_{n-1}\rangle \left|t\oplus\bigoplus_{j=0}^{n-1}x_j\right\rangle. \]
  • An odd number of input ones flips the target; an even number leaves it unchanged. The target can start in either state.

  • The operation also works coherently on superpositions. Internal ancilla measurements drive corrections rather than reading out the input parity.

  • Run from a source checkout with development dependencies installed. The repository default is little endian.

Hide code cell source

import itertools
import numpy as np
import pandas as pd
from guppylang import guppy
from guppylang.std.builtins import array, output
from guppylang.std.quantum import (
    collect_measurements, measure, measure_array, qubit, x, h, discard_array,
)
from guppylang.std.debug import state_output
from selene_sim import QuantumReplay, Quest
from guppyalgos.primitives.subroutines.parity import (
    parity_laqcc, parity_laqcc_total_qubits, parity_sequential,
)
from guppyalgos.utils import qarray, transversal
from guppyalgos.tests.helpers import assert_allclose_ignorephase

1. Choose the implementation

  • parity_sequential uses controlled gates without extra qubits.

  • For four or more inputs, parity_laqcc uses two ancilla registers and classical feed-forward to arrange the quantum gates in constant-depth layers. This trades extra qubits and measurement feedback for quantum depth; it does not imply constant execution time on every device.

  • With fewer than four inputs, it falls back to the sequential circuit. Both implementations use the same call: parity(target, qreg).

  • Use the built-in helper to size the simulator:

\[ N_{\mathrm{total}}=n+1+2\max(n-3,0). \]
n_inputs = 4
total_qubits = parity_laqcc_total_qubits(n_inputs)
print(f"{n_inputs} inputs + 1 target + {total_qubits - n_inputs - 1} ancillas = {total_qubits} qubits")
4 inputs + 1 target + 2 ancillas = 7 qubits

2. Check even and odd parity

  • Prepare a computational-basis input, apply parity, and measure both registers.

  • For \(|1,0,1,1\rangle\), the input parity is \(1\oplus0\oplus1\oplus1=1\). The target flips from \(0\) to \(1\), or from \(1\) to \(0\).

  • Compile once and reuse the emulator for each input. The table also checks that the input bits are preserved.

@guppy
def basis_example(bits: array[bool, 4], target_bit: bool) -> None:
    qreg = qarray(n_inputs)
    target = qubit()
    for i in range(n_inputs):
        if bits[i]:
            x(qreg[i])
    if target_bit:
        x(target)
    parity_laqcc(target, qreg)
    output("inputs", collect_measurements(measure_array(qreg)))
    output("target", measure(target).read())


emulator = basis_example.emulator(total_qubits).with_seed(42)
rows = []
for bits in [[True, False, True, True], [True, False, True, False]]:
    for target_bit in [False, True]:
        shot = emulator.run(bits=bits, target_bit=target_bit).collated_shots()[0]
        expected = target_bit ^ (sum(bits) % 2 == 1)
        assert shot["inputs"][0] == bits
        assert shot["target"][0] == expected
        rows.append({
            "Input bits": ", ".join(str(int(bit)) for bit in bits),
            "Parity": "Odd" if sum(bits) % 2 else "Even",
            "Initial target": int(target_bit), "Final target": int(shot["target"][0]),
        })
print(pd.DataFrame(rows).to_string(index=False, float_format=lambda value: f"{value:.4f}"))
Input bits Parity Initial target Final target
1, 0, 1, 1    Odd              0            1
1, 0, 1, 1    Odd              1            0
1, 0, 1, 0   Even              0            0
1, 0, 1, 0   Even              1            1

3. Check coherence across measurement branches

  • Start with \(|+\rangle^{\otimes4}|0\rangle\). Parity entangles the target with the inputs:

\[ \frac14\sum_{x\in\{0,1\}^4}|x\rangle|0\rangle \longmapsto \frac14\sum_x|x\rangle|\operatorname{parity}(x)\rangle. \]
  • Apply parity_sequential afterward to undo that operation. If the LAQCC corrections preserve coherence, the inputs return to \(|+\rangle^{\otimes4}\) and the target returns to \(|0\rangle\).

  • Force each of the four internal measurement branches with QuantumReplay. Statevector checks verify relative phases as well as probabilities; a basis-state truth table alone would miss dephasing.

@guppy
def coherence_example() -> None:
    qreg = qarray(n_inputs)
    target = qubit()
    transversal(h, qreg)
    parity_laqcc(target, qreg)
    parity_sequential(target, qreg)
    state_output("inputs", qreg)
    output("target", measure(target).read())
    discard_array(qreg)


branches = [list(bits) for bits in itertools.product([False, True], repeat=2)]
replay = QuantumReplay(
    simulator=Quest(), measurements=branches, resume_with_measurement=True,
)
result = coherence_example.emulator(total_qubits).with_simulator(replay).with_shots(4).run()
expected_state = np.ones(2**n_inputs) / np.sqrt(2**n_inputs)
rows = []
for branch, shot, measured in zip(branches, result.results, result.collated_shots()):
    actual = Quest.extract_states_dict(shot)["inputs"].get_single_state()
    assert_allclose_ignorephase(actual, expected_state)
    assert not measured["target"][0]
    rows.append({
        "Ancilla outcomes": ", ".join(str(int(bit)) for bit in branch),
        "Restored input fidelity": abs(np.vdot(expected_state, actual))**2,
        "Final target": int(measured["target"][0]),
    })
print(pd.DataFrame(rows).to_string(index=False, float_format=lambda value: f"{value:.8f}"))
Ancilla outcomes Restored input fidelity Final target
            0, 0              1.00000000            0
            0, 1              1.00000000            0
            1, 0              1.00000000            0
            1, 1              1.00000000            0
  • All replayed branches restore the coherent input after uncomputation. The forced outcomes test corrections; they do not estimate branch probabilities.

  • Parity collects information from several inputs into one target. Fanout applies one control to several targets; the two operations serve different roles.