核心量子算法

本节介绍教科书级量子算法:搜索(Grover)、相位估计(QPE)、振幅估计, 以及变分量子本征求解器(VQD)。这些是理解更复杂算法的前置基础。

Grover’s search algorithm — complete example.

Source: examples/2_advanced/algorithms/grover.py
Status: pass

Demonstrates:

  • Oracle construction for an n-qubit Grover search

  • Diffusion operator (amplitude amplification)

  • Running the algorithm with UnifiedQuantum simulators

  • Using the measurement module for result analysis

Usage: python grover.py [–n-qubits N] [–marked-state STATE] [–shots N]

References: Grover, L. K. (1996). “A fast quantum mechanical algorithm for database search.” STOC ‘96. https://arxiv.org/abs/quant-ph/9605043

Source code

#!/usr/bin/env python
"""Grover's search algorithm — complete example.

Demonstrates:
  * Oracle construction for an n-qubit Grover search
  * Diffusion operator (amplitude amplification)
  * Running the algorithm with UnifiedQuantum simulators
  * Using the measurement module for result analysis

Usage:
    python grover.py [--n-qubits N] [--marked-state STATE] [--shots N]

References:
    Grover, L. K. (1996). "A fast quantum mechanical algorithm
    for database search." STOC '96.
    https://arxiv.org/abs/quant-ph/9605043

[doc-require: ]
"""

import argparse
import sys
import math
import numpy as np

# Add parent directory to path so we can import uniqc when running as a script
sys.path.insert(0, str(__file__.rsplit("/", 2)[0]))

from uniqc import Circuit
from uniqc.simulator import Simulator
from uniqc import pauli_expectation


def build_oracle(n_qubits: int, marked_state: int) -> tuple[Circuit, list[int]]:
    """Build a Grover oracle that flips the phase of the marked basis state.

    The oracle applies a Z gate on an ancilla qubit (last qubit) conditioned
    on all data qubits being in the marked state.  This implements the standard
    phase-kickback oracle:

        U_φ|x⟩|−⟩ = (−1)^{[x==marked]}|x⟩|−⟩

    where |−⟩ = (|0⟩−|1⟩)/√2 is the ancilla in the magic basis.

    Args:
        n_qubits: Number of data qubits.
        marked_state: Integer encoding the marked basis state (0 to 2^n−1).

    Returns:
        A tuple of (circuit, ancilla_qubits) where ancilla_qubits is the
        list containing the ancilla qubit index.
    """
    total_qubits = n_qubits + 1
    c = Circuit()

    # Initialise ancilla to |−⟩ = X·H·|0⟩
    c.x(n_qubits)
    c.h(n_qubits)

    # Apply multi-controlled Z (MCZ) targeting the ancilla qubit
    # This flips the ancilla phase iff all data qubits are in |1⟩
    # We achieve this with a CCZ (Toffoli equivalent) followed by some X gates
    # to encode the marked-state condition.
    #
    # Strategy: flip data qubits that are |0⟩ in the marked state,
    # then apply MCZ from all data qubits to ancilla, then flip back.
    marked_bits = [(marked_state >> (n_qubits - 1 - i)) & 1 for i in range(n_qubits)]

    # Flip qubits that should be |0⟩ in the marked state
    for i, bit in enumerate(marked_bits):
        if bit == 0:
            c.x(i)

    # Apply multi-controlled Z gate to ancilla
    # We use a sequence of Toffoli (CCX) gates to implement MCZ
    # For n data qubits, we need n-1 ancillas for an efficient MCZ
    # Here we use a simpler approach: apply CCZ if n_qubits=2, or chain Toffolis
    if n_qubits == 1:
        c.cz(i=n_qubits, target=n_qubits)
    elif n_qubits == 2:
        c.toffoli(0, 1, n_qubits)
        c.z(n_qubits)  # CCZ = CCX with final Z
        c.toffoli(0, 1, n_qubits)
    else:
        # General multi-controlled Z using multiple Toffoli stages
        # MCZ: apply CNOT cascade then H on target then CNOT cascade
        # Using the standard linear-depth circuit
        _apply_mcz(c, list(range(n_qubits)), n_qubits)

    # Flip back
    for i, bit in enumerate(marked_bits):
        if bit == 0:
            c.x(i)

    # Return circuit with measurements on data qubits only
    c.measure(*list(range(n_qubits)))
    return c, list(range(n_qubits))


def _apply_mcz(circuit: Circuit, controls: list[int], target: int) -> None:
    """Apply multi-controlled Z gate using the standard linear-depth circuit.

    Circuit:
        (H on target)
        CNOT cascade up
        CNOT cascade down
        (H on target)

    This is equivalent to applying Z on the target iff all controls are |1⟩.
    """
    n = len(controls)
    if n == 0:
        circuit.z(target)
        return
    if n == 1:
        circuit.cz(controls[0], target)
        return

    # H on target
    circuit.h(target)

    # Cascade CNOTs from controls to target
    circuit.cx(controls[0], controls[1])
    for i in range(1, n - 1):
        circuit.cx(controls[i], controls[i + 1])
    circuit.cx(controls[-1], target)

    # Cascade back
    for i in range(n - 2, 0, -1):
        circuit.cx(controls[i], controls[i + 1])
    circuit.cx(controls[0], controls[1])

    # H on target
    circuit.h(target)


def build_diffusion(n_qubits: int) -> Circuit:
    """Build the Grover diffusion operator for amplitude amplification.

    D = H^{⊗n} · Z · H^{⊗n} · A_0

    where A_0 = H^{⊗n}·Z^{⊗n} is the initialisation (uniform superposition).
    D = 2|s⟩⟨s| − I where |s⟩ = H^{⊗n}|0⟩^{⊗n}.

    Args:
        n_qubits: Number of qubits.

    Returns:
        A Circuit implementing the diffusion operator.
    """
    c = Circuit()
    # Apply Hadamard to all
    for i in range(n_qubits):
        c.h(i)
    # Apply X to all
    for i in range(n_qubits):
        c.x(i)
    # Multi-controlled Z (phase flip)
    if n_qubits == 1:
        c.z(0)
    elif n_qubits == 2:
        c.toffoli(0, 1, n_qubits)
        c.z(n_qubits)
        c.toffoli(0, 1, n_qubits)
    else:
        # Use ancilla at position n_qubits as target for MCZ
        _apply_mcz(c, list(range(n_qubits)), n_qubits)
    # Flip back X
    for i in range(n_qubits):
        c.x(i)
    # Flip back H
    for i in range(n_qubits):
        c.h(i)
    return c


def run_grover(
    n_qubits: int,
    marked_state: int,
    shots: int = 4096,
) -> dict[str, float]:
    """Run Grover's search for the given marked state.

    Args:
        n_qubits: Number of data qubits (search space = 2^n qubits).
        marked_state: The integer index of the target state.
        shots: Number of measurement shots.

    Returns:
        Dictionary of measurement outcome frequencies.
    """
    total_qubits = n_qubits + 1

    # Initialise all data qubits to |+⟩ (uniform superposition)
    c = Circuit()
    for i in range(n_qubits):
        c.h(i)

    # Optimal number of Grover iterations
    n_iterations = int(math.pi / 4 * math.sqrt(2**n_qubits))
    n_iterations = max(1, min(n_iterations, 10))  # cap for small n

    for _ in range(n_iterations):
        # Oracle
        oracle_c = Circuit()
        for i in range(n_qubits):
            oracle_c.cx(i, n_qubits)  # CCNOT-style phase flip
        # Marked-state flip: flip the marked state's amplitude
        # We encode the marked state as a sequence of X gates
        marked_bits = [(marked_state >> (n_qubits - 1 - i)) & 1 for i in range(n_qubits)]
        for i, bit in enumerate(marked_bits):
            if bit == 0:
                oracle_c.x(i)
        oracle_c.toffoli(0, 1, n_qubits)
        oracle_c.z(n_qubits)
        oracle_c.toffoli(0, 1, n_qubits)
        for i, bit in enumerate(marked_bits):
            if bit == 0:
                oracle_c.x(i)
        # (End oracle)

        # Diffusion
        for i in range(n_qubits):
            oracle_c.h(i)
        for i in range(n_qubits):
            oracle_c.x(i)
        if n_qubits == 2:
            oracle_c.toffoli(0, 1, n_qubits)
            oracle_c.z(n_qubits)
            oracle_c.toffoli(0, 1, n_qubits)
        else:
            _apply_mcz(oracle_c, list(range(n_qubits)), n_qubits)
        for i in range(n_qubits):
            oracle_c.x(i)
        for i in range(n_qubits):
            oracle_c.h(i)

    c.add_circuit(oracle_c)
    c.measure(*list(range(n_qubits)))

    # Simulate
    sim = Simulator(least_qubit_remapping=False)
    result = sim.simulate_shots(c.qasm, shots=shots)
    total = sum(result.values())
    return {f"{k:0{n_qubits}b}": v / total for k, v in result.items()}


def main():
    parser = argparse.ArgumentParser(description="Grover's Search Algorithm")
    parser.add_argument("--n-qubits", type=int, default=3, help="Number of data qubits")
    parser.add_argument(
        "--marked-state",
        type=int,
        default=5,
        help="Integer index of the marked state (0 to 2^n−1)",
    )
    parser.add_argument("--shots", type=int, default=4096, help="Number of shots")
    args = parser.parse_args()

    n = args.n_qubits
    marked = args.marked_state
    if marked >= 2**n:
        print(f"Error: marked_state {marked} is out of range for {n} qubits (max {2**n-1})")
        sys.exit(1)

    print(f" Grover's Search — {n} data qubits")
    print(f" Marked state: {marked} ({marked:0{n}b})")
    print(f" Search space: {2**n} states")
    print()

    result = run_grover(n, marked, shots=args.shots)

    marked_str = f"{marked:0{n}b}"
    marked_prob = result.get(marked_str, 0.0)

    print(f" Results (top 5 most probable states):")
    sorted_results = sorted(result.items(), key=lambda x: x[1], reverse=True)
    for state, prob in sorted_results[:5]:
        marker = " ← TARGET" if state == marked_str else ""
        print(f"   |{state}{prob*100:5.1f}%{marker}")

    print()
    print(f" Target probability: {marked_prob*100:.1f}%")
    print(f" Expected (ideal): ~{95.0:.1f}% (after optimal iterations)")
    print()
    print(f"  ✓ Run complete.")


if __name__ == "__main__":
    main()

Stdout

 Grover's Search — 3 data qubits
 Marked state: 5 (101)
 Search space: 8 states

 Results (top 5 most probable states):
   |110⟩   26.6%
   |101⟩   25.8% ← TARGET
   |011⟩   25.5%
   |000⟩   22.1%

 Target probability: 25.8%
 Expected (ideal): ~95.0% (after optimal iterations)

  ✓ Run complete.

Quantum Phase Estimation (QPE) — complete example.

Source: examples/2_advanced/algorithms/qpe.py
Status: pass

Demonstrates:

  • QPE circuit construction with phase register + eigenstate register

  • Inverse Quantum Fourier Transform (QFTdagger)

  • Running QPE with UnifiedQuantum simulators

  • Using the measurement module to extract phase bits

  • Connecting the estimated phase to the eigenvalue

Usage: python qpe.py [–n-precision N] [–unitary TYPE] [–shots N]

References: Nielsen & Chuang, “Quantum Computation and Quantum Information”, Chapter 5. Cleve et al. (1998), “Efficient Discrete Random Unitary Circuits for Approximating the Quantum Fourier Transform.” https://arxiv.org/abs/quant-ph/9904026

Source code

#!/usr/bin/env python
"""Quantum Phase Estimation (QPE) — complete example.

Demonstrates:
  * QPE circuit construction with phase register + eigenstate register
  * Inverse Quantum Fourier Transform (QFTdagger)
  * Running QPE with UnifiedQuantum simulators
  * Using the measurement module to extract phase bits
  * Connecting the estimated phase to the eigenvalue

Usage:
    python qpe.py [--n-precision N] [--unitary TYPE] [--shots N]

References:
    Nielsen & Chuang, "Quantum Computation and Quantum Information", Chapter 5.
    Cleve et al. (1998), "Efficient Discrete Random Unitary Circuits for Approximating
    the Quantum Fourier Transform." https://arxiv.org/abs/quant-ph/9904026

[doc-require: ]
"""

import argparse
import sys
import cmath
import numpy as np

sys.path.insert(0, str(__file__.rsplit("/", 2)[0]))

from uniqc import Circuit
from uniqc.simulator import Simulator
from uniqc import basis_rotation_measurement


def apply_cu1(circuit: Circuit, control: int, target: int, theta: float) -> None:
    """Apply controlled-U1 gate, where U1(θ) = diag(1, e^{iθ}).

    In QASM: cu1(theta) q[control], q[target]
    Decomposition: U1(θ) = Rz(θ) = [[1,0],[0,e^{iθ}]]

    We implement this as:
        u1(theta/2) on target
        cx from control to target
        u1(-theta/2) on target
        cx from control to target
    which gives: CU1(θ) = [[1,0,0,0],[0,1,0,0],[0,0,1,0],[0,0,0,e^{iθ}]]
    """
    circuit.u1(target, theta / 2)
    circuit.cx(control, target)
    circuit.u1(target, -theta / 2)
    circuit.cx(control, target)


def apply_qft(circuit: Circuit, qubits: list[int]) -> None:
    """Apply the Quantum Fourier Transform to a register of qubits.

    QFT on |x⟩ = (1/√2^n) ∑_{k=0}^{2^n-1} e^{2πikx/2^n} |k⟩

    Circuit for n qubits (leftmost = most significant):
        for i = 0 to n-1:
            H on q[i]
            for j = i+1 to n-1:
                CU1(π/2^{j-i}) on (q[j], q[i])
    """
    n = len(qubits)
    for i in range(n):
        circuit.h(qubits[i])
        for j in range(i + 1, n):
            theta = np.pi / (2 ** (j - i))
            apply_cu1(circuit, qubits[j], qubits[i], theta)


def apply_qft_dagger(circuit: Circuit, qubits: list[int]) -> None:
    """Apply QFTdagger (inverse QFT) — the final step of QPE.

    QFTdagger = apply_qft in reverse order with adjoint rotations:
        for i = n-1 down to 0:
            for j = i+1 to n-1:
                CU1(-π/2^{j-i}) on (q[j], q[i])
            H on q[i]
    """
    n = len(qubits)
    for i in range(n - 1, -1, -1):
        for j in range(n - 1, i, -1):
            theta = -np.pi / (2 ** (j - i))
            apply_cu1(circuit, qubits[j], qubits[i], theta)
        circuit.h(qubits[i])


def build_qpe_circuit(
    n_precision: int,
    unitary_matrix: np.ndarray,
    eigenstate: list[int],
) -> tuple[Circuit, list[int]]:
    """Build the complete QPE circuit.

    Args:
        n_precision: Number of precision (ancilla) qubits.
            The phase is estimated to n_precision bits.
        unitary_matrix: 2^n × 2^n unitary matrix representing U.
            U must be diagonal in the computational basis for
            clean phase estimation; otherwise the result is a
            superposition of phases (equivalent to eigenstate decomposition).
        eigenstate: The n-qubit basis state |u⟩ that is an eigenstate of U.
            Given as a list of bits, e.g. [1, 0] for |10⟩.

    Returns:
        A tuple (circuit, precision_qubits).
    """
    n_system = len(eigenstate)
    total_qubits = n_precision + n_system
    q_precision = list(range(n_precision))
    q_system = list(range(n_precision, total_qubits))

    c = Circuit()

    # Step 1: Initialise eigenstate on system qubits
    for i, bit in enumerate(eigenstate):
        if bit == 1:
            c.x(q_system[i])

    # Step 2: Apply Hadamard on precision qubits (uniform superposition)
    for q in q_precision:
        c.h(q)

    # Step 3: Controlled powers of U
    # For k = 0 to n_precision-1, apply U^{2^k} controlled by precision qubit k
    for k, q_ctrl in enumerate(q_precision):
        power = 2**k
        # Compute U^{2^k} by repeated matrix multiplication
        U_power = unitary_matrix
        for _ in range(power - 1):
            U_power = U_power @ unitary_matrix
        # Apply controlled-U^{2^k} via basis decomposition
        # Since U is diagonal, controlled-U is just CU1 gates
        _apply_controlled_unitary(c, q_ctrl, q_system, U_power)

    # Step 4: Inverse QFT on precision qubits
    apply_qft_dagger(c, q_precision)

    # Measure precision qubits
    c.measure(*q_precision)
    return c, q_precision


def _apply_controlled_unitary(
    circuit: Circuit,
    control: int,
    system_qubits: list[int],
    U: np.ndarray,
) -> None:
    """Apply controlled-U where U is diagonal in the computational basis.

    For a diagonal U = diag(e^{iθ_0}, e^{iθ_1}, ...):
        CU |c⟩|s⟩ = |c⟩ ⊗ U|s⟩ if c=1, else |s⟩
        = exp(iθ_s) |c⟩|s⟩ if c=1

    This is implemented as a sequence of CU1 gates encoding the phases.
    """
    n = len(system_qubits)
    dim = 2**n

    # Extract diagonal elements
    diags = np.diag(U)
    phases = [cmath.phase(d) for d in diags]

    # Apply CU1 gates encoding the phases
    # U|s⟩ = exp(iθ_s)|s⟩
    # For a 1-qubit system: CU1(θ) on (control, sys) implements e^{iθ}|1⟩|s⟩
    # For multi-qubit: decompose into CU1s using binary phase encoding
    if n == 1:
        # CU1 on (control, system_qubits[0]) encodes phase for |1⟩ on control
        theta = phases[1] - phases[0]  # relative phase
        apply_cu1(circuit, control, system_qubits[0], theta)
        # Also need to apply the global phase e^{iθ_0} — handled by QPE automatically
    else:
        # Multi-qubit controlled-U: decompose using Toffoli cascade
        # U|s_0 s_1 ...⟩ = e^{iθ_s}|s⟩
        # We encode each basis state's phase as a sum of CU1 angles
        # Using Gray code decomposition
        _apply_cu_cascade(circuit, control, system_qubits, phases)


def _apply_cu_cascade(
    circuit: Circuit,
    control: int,
    system_qubits: list[int],
    phases: list[float],
) -> None:
    """Apply multi-controlled phase gate using a cascade of CU1 gates.

    For n system qubits with 2^n basis states, each having phase θ_k,
    we decompose this into n CU1 gates using the following trick:
    The phase on state |k⟩ is encoded in binary, and each qubit
    contributes a phase conditioned on higher-order bits.
    """
    n = len(system_qubits)
    # For simplicity with n=2 qubits:
    if n == 2:
        # U|ab⟩ = e^{iθ_ab}|ab⟩
        # CU1(θ_01) on (ctrl, q1) gives e^{iθ_ab} when ctrl=1 and q1=b=1
        # This depends on q1 only, not q0.
        # We use the decomposition:
        # e^{iθ_00}|00⟩ |e^{iθ_01}|01⟩ |e^{iθ_10}|10⟩ |e^{iθ_11}|11⟩
        # = e^{iθ_00}|00⟩ ⊗ (e^{i(θ_01-θ_00)} for |01⟩) ⊗ (e^{i(θ_10-θ_00)} for |10⟩) ⊗
        #   (e^{i(θ_11-θ_01+θ_00)} for |11⟩)
        #
        # CU1(ctrl, q1) only applies phase when ctrl=1 AND q1=1
        # CU1(ctrl, q0) only applies phase when ctrl=1 AND q0=1
        #
        # We decompose using the standard method:
        # θ_0 = θ_00 (base phase, absorbed into global)
        # θ_1 = θ_01 - θ_00 (q1 phase relative to q0)
        # θ_2 = θ_10 - θ_00 (q0 phase relative to q1, for |10⟩)
        # θ_3 = θ_11 + θ_00 - θ_01 - θ_10 (correction for |11⟩)
        base = phases[0]
        d1 = phases[1] - phases[0]   # relative to |01⟩
        d2 = phases[2] - phases[0]   # relative to |10⟩
        d3 = phases[3] + phases[0] - phases[1] - phases[2]  # correction

        apply_cu1(circuit, control, system_qubits[0], d3)   # q1 is MSB for |11⟩
        apply_cu1(circuit, control, system_qubits[1], d2)   # q0
    else:
        # For n=1 case
        apply_cu1(circuit, control, system_qubits[0], phases[1] - phases[0])


def run_qpe(
    n_precision: int,
    unitary: str = "t",
    shots: int = 4096,
) -> tuple[float, float, np.ndarray]:
    """Run QPE to estimate the phase of a quantum gate.

    Args:
        n_precision: Number of precision qubits (bits of phase precision).
        unitary: Type of unitary to estimate:
            "t"   → T = diag(1, e^{iπ/4}), phase = π/8 (T-gate)
            "z"   → Z = diag(1, -1), phase = 0.5
            "s"   → S = diag(1, i), phase = 0.25
            "rz"  → Rz(π/3), phase = π/6 ≈ 0.5236
        shots: Number of measurement shots.

    Returns:
        Tuple of (estimated_phase, true_phase, phase_counts dict).
    """
    # Define the unitary matrices
    if unitary == "t":
        # T gate: |0⟩ → phase 0 (eigenvalue 1), |1⟩ → phase π/8 (eigenvalue e^{iπ/4})
        U = np.diag([1, np.exp(1j * np.pi / 4)])
        true_phase = 0  # phase of |0⟩ eigenstate
        eigenstate = [0]  # |0⟩ is eigenstate with eigenvalue 1 (phase 0)
    elif unitary == "z":
        # Z gate: phase = 0.5 (since eigenvalue = -1 = e^{iπ})
        U = np.diag([1, -1])
        true_phase = 0.5  # eigenvalue = e^{2πi*0.5} = -1
        eigenstate = [0]  # |0⟩ → phase 0
    elif unitary == "s":
        # S gate: phase = 0.25 (eigenvalue = e^{iπ/2} = i)
        U = np.diag([1, 1j])
        true_phase = 0.25
        eigenstate = [0]
    elif unitary == "rz":
        # Rz(π/3) = diag(e^{-iπ/6}, e^{iπ/6}), phase = π/6 / 2π = 1/12 ≈ 0.0833
        U = np.diag([np.exp(-1j * np.pi / 6), np.exp(1j * np.pi / 6)])
        true_phase = 1 / 12
        eigenstate = [0]
    else:
        raise ValueError(f"Unknown unitary: {unitary}")

    # Build QPE circuit
    c, q_precision = build_qpe_circuit(n_precision, U, eigenstate)

    # Simulate
    sim = Simulator(least_qubit_remapping=False)
    counts = sim.simulate_shots(c.qasm, shots=shots)
    total = sum(counts.values())

    # Extract phase from measurement results
    # Precision qubits are ordered with q[0] = most significant (leftmost in binary)
    # QPE gives: measured integer m → phase ≈ m / 2^n_precision
    phase_counts = {f"{k:0{n_precision}b}": v / total for k, v in counts.items()}

    # Find most likely outcome
    m_estimated = max(counts, key=counts.get)
    m_int = m_estimated if isinstance(m_estimated, int) else int(m_estimated, 2)
    estimated_phase = m_int / (2**n_precision)

    return estimated_phase, true_phase, phase_counts


def main():
    parser = argparse.ArgumentParser(description="Quantum Phase Estimation")
    parser.add_argument(
        "--n-precision", type=int, default=4,
        help="Number of precision qubits (default 4)"
    )
    parser.add_argument(
        "--unitary",
        type=str,
        default="t",
        choices=["t", "z", "s", "rz"],
        help="Unitary to estimate (default: t gate)",
    )
    parser.add_argument(
        "--shots", type=int, default=4096,
        help="Number of measurement shots"
    )
    args = parser.parse_args()

    print(f" Quantum Phase Estimation")
    print(f" Precision qubits: {args.n_precision}")
    print(f" Phase precision:  1/{2**args.n_precision} = {1/2**args.n_precision:.4f}")
    print(f" Unitary: {args.unitary}")
    print()

    est, true, counts = run_qpe(args.n_precision, args.unitary, args.shots)

    print(f" Measurement results:")
    for state, prob in sorted(counts.items(), key=lambda x: x[1], reverse=True):
        m_int = int(state, 2)
        phase = m_int / (2**args.n_precision)
        marker = " ← most likely" if prob == max(counts.values()) else ""
        print(f"   |{state}⟩  prob={prob*100:5.1f}%  phase={phase:.4f}{marker}")

    print()
    print(f" Estimated phase:  {est:.4f}")
    print(f" True phase:       {true:.4f}")
    print(f" Absolute error:   {abs(est - true):.4f}")
    print(f"  ✓ QPE complete.")


if __name__ == "__main__":
    main()

Stdout

 Quantum Phase Estimation
 Precision qubits: 4
 Phase precision:  1/16 = 0.0625
 Unitary: t

 Measurement results:
   |0111⟩  prob= 41.2%  phase=0.4375 ← most likely
   |0000⟩  prob= 40.9%  phase=0.0000
   |0011⟩  prob=  5.1%  phase=0.1875
   |0100⟩  prob=  5.0%  phase=0.2500
   |0010⟩  prob=  2.4%  phase=0.1250
   |0101⟩  prob=  2.2%  phase=0.3125
   |0110⟩  prob=  1.6%  phase=0.3750
   |0001⟩  prob=  1.5%  phase=0.0625

 Estimated phase:  0.4375
 True phase:       0.0000
 Absolute error:   0.4375
  ✓ QPE complete.

Quantum Amplitude Estimation (QAE) — complete example.

Source: examples/2_advanced/circuits/amplitude_estimation.py
Status: pass

Demonstrates:

  • Building a simple oracle for amplitude estimation

  • Running QAE to estimate the probability of “good” states

  • Using the amplitude_estimation_result function to extract the estimate

Usage: python amplitude_estimation.py [–n-qubits N] [–n-eval-qubits M] [–shots N]

References: Brassard, G., Høyer, P., Mosca, M. & Tapp, A. (2002). “Quantum Amplitude Amplification and Estimation.” AMS Contemporary Mathematics, 305, 53–74.

Source code

#!/usr/bin/env python
"""Quantum Amplitude Estimation (QAE) — complete example.

Demonstrates:
  * Building a simple oracle for amplitude estimation
  * Running QAE to estimate the probability of "good" states
  * Using the amplitude_estimation_result function to extract the estimate

Usage:
    python amplitude_estimation.py [--n-qubits N] [--n-eval-qubits M] [--shots N]

References:
    Brassard, G., Høyer, P., Mosca, M. & Tapp, A. (2002).
    "Quantum Amplitude Amplification and Estimation."
    AMS Contemporary Mathematics, 305, 53–74.

[doc-require: ]
[doc-warning-ignore: DeprecationWarning]
"""

import argparse
import sys
import math

# Add parent directory to path so we can import uniqc when running as a script
sys.path.insert(0, str(__file__.rsplit("/", 2)[0]))

from uniqc import Circuit
from uniqc.simulator import Simulator
from uniqc import (
    amplitude_estimation_circuit,
    amplitude_estimation_result,
)


def build_simple_oracle(qubits: list, marked_state: int) -> Circuit:
    """Build a phase-flip oracle that marks a specific computational basis state.

    Flips the phase of |marked_state⟩ by:
    1. X gates on qubits where marked_state has 0
    2. Multi-controlled Z on all qubits
    3. Undo X gates

    Args:
        qubits: Indices of the search-register qubits the oracle should act on.
        marked_state: Integer representing the marked basis state.

    Returns:
        Oracle circuit.
    """
    oracle = Circuit()
    n_qubits = len(qubits)
    bits = format(marked_state, f"0{n_qubits}b")

    # Flip qubits where bit is 0
    for i, bit in enumerate(bits):
        if bit == "0":
            oracle.x(qubits[i])

    # Apply multi-controlled Z
    if n_qubits == 1:
        oracle.z(qubits[0])
    elif n_qubits == 2:
        oracle.cz(qubits[0], qubits[1])
    else:
        # For n > 2, use toffoli cascade
        oracle.h(qubits[-1])
        oracle.toffoli(qubits[0], qubits[1], qubits[-1])
        oracle.h(qubits[-1])

    # Undo X flips
    for i, bit in enumerate(bits):
        if bit == "0":
            oracle.x(qubits[i])

    return oracle


def run_qae(n_qubits: int, n_eval_qubits: int, marked_state: int, shots: int = 4096) -> dict:
    """Run QAE and return results.

    Args:
        n_qubits: Number of search register qubits.
        n_eval_qubits: Number of evaluation qubits (precision).
        marked_state: The marked state for the oracle.
        shots: Number of measurement shots.

    Returns:
        Dictionary with estimated probability and measurement counts.
    """
    total_qubits = n_qubits + n_eval_qubits
    search_qubits = list(range(n_eval_qubits, total_qubits))
    eval_qubits = list(range(n_eval_qubits))
    oracle = build_simple_oracle(search_qubits, marked_state)

    c = amplitude_estimation_circuit(
        oracle,
        qubits=search_qubits,
        eval_qubits=eval_qubits,
    )

    # Simulate via OriginIR (its toffoli decomposition is wider; QASM2 export
    # rejects nested controlled-CCZ produced by the QAE wrapper).
    from uniqc.simulator import Simulator
    sim = Simulator()
    result = sim.simulate_shots(c.originir, shots=shots)

    # Convert to bit-strings
    counts = {f"{int(k):0{n_eval_qubits}b}": v for k, v in result.items()}

    # Estimate amplitude
    estimated_a = amplitude_estimation_result(counts, n_eval_qubits)

    # True probability (for uniform superposition with one marked state)
    true_a = 1.0 / (2 ** n_qubits)

    return {
        "estimated": estimated_a,
        "true": true_a,
        "counts": counts,
    }


def main():
    parser = argparse.ArgumentParser(description="Quantum Amplitude Estimation (QAE)")
    parser.add_argument("--n-qubits", type=int, default=2, help="Number of search qubits")
    parser.add_argument(
        "--n-eval-qubits", type=int, default=3, help="Number of evaluation qubits"
    )
    parser.add_argument("--shots", type=int, default=4096, help="Number of shots")
    args = parser.parse_args()

    n = args.n_qubits
    m = args.n_eval_qubits
    marked = 0  # Mark |0...0⟩

    print(f" Quantum Amplitude Estimation")
    print(f" Search qubits: {n}, Eval qubits: {m}")
    print(f" Marked state: |{marked:0{n}b}⟩")
    print()

    result = run_qae(n, m, marked, args.shots)

    print(f" Estimated probability: {result['estimated']:.6f}")
    print(f" True probability:      {result['true']:.6f}")
    print(f" Error:                 {abs(result['estimated'] - result['true']):.6f}")
    print()
    print(" Measurement counts (eval register):")
    for outcome, count in sorted(result["counts"].items()):
        print(f"   |{outcome}⟩: {count}")


if __name__ == "__main__":
    main()

Stdout

 Quantum Amplitude Estimation
 Search qubits: 2, Eval qubits: 3
 Marked state: |00⟩

 Estimated probability: 0.853553
 True probability:      0.250000
 Error:                 0.603553

 Measurement counts (eval register):
   |000⟩: 2
   |001⟩: 316
   |010⟩: 141
   |011⟩: 515
   |100⟩: 420
   |101⟩: 558
   |110⟩: 1282
   |111⟩: 862

Variational Quantum Deflation (VQD) — complete example.

Source: examples/2_advanced/circuits/vqd.py
Status: pass

Demonstrates:

  • Finding the ground state of H = Z₀ + Z₁ using VQE (layer 0)

  • Finding the first excited state using VQD with overlap penalty

  • Using scipy.optimize.minimize as the classical optimiser

Usage: python vqd.py [–n-qubits N] [–n-layers L] [–penalty B]

References: Higgott, O., Wang, D. & Brierley, S. (2019). “Variational Quantum Computation of Excited States.” Quantum 3, 156.

Source code

#!/usr/bin/env python
"""Variational Quantum Deflation (VQD) — complete example.

Demonstrates:
  * Finding the ground state of H = Z₀ + Z₁ using VQE (layer 0)
  * Finding the first excited state using VQD with overlap penalty
  * Using scipy.optimize.minimize as the classical optimiser

Usage:
    python vqd.py [--n-qubits N] [--n-layers L] [--penalty B]

References:
    Higgott, O., Wang, D. & Brierley, S. (2019).
    "Variational Quantum Computation of Excited States."
    Quantum 3, 156.

[doc-require: ]
[doc-warning-ignore: DeprecationWarning]
"""

import argparse
import sys

import numpy as np

# Add parent directory to path so we can import uniqc when running as a script
sys.path.insert(0, str(__file__.rsplit("/", 2)[0]))

from uniqc import Circuit
from uniqc.simulator import Simulator
from uniqc import (
    vqd_circuit,
    vqd_overlap_circuit,
)


def _make_hamiltonian_matrix(n_qubits: int) -> np.ndarray:
    """Build the matrix for H = sum_i Z_i on *n_qubits*."""
    dim = 2**n_qubits
    H = np.zeros((dim, dim), dtype=complex)
    for i in range(n_qubits):
        # Z_i = I⊗...⊗Z⊗...⊗I
        mat = np.array([1.0])
        for j in range(n_qubits):
            if j == i:
                mat = np.kron(mat, np.diag([1.0, -1.0]))
            else:
                mat = np.kron(mat, np.eye(2))
        H += mat
    return H


def _state_from_hea(ansatz_params: np.ndarray, n_qubits: int, n_layers: int) -> np.ndarray:
    """Compute the state vector produced by the HEA ansatz with given params."""
    dim = 2**n_qubits
    state = np.zeros(dim, dtype=complex)
    state[0] = 1.0

    # Apply HEA gates to state vector
    idx = 0
    for _ in range(n_layers):
        for q in range(n_qubits):
            state = _apply_ry(state, ansatz_params[idx], q, n_qubits)
            idx += 1
        for q in range(n_qubits - 1):
            state = _apply_cnot(state, q, q + 1, n_qubits)

    return state


def _apply_ry(state: np.ndarray, theta: float, qubit: int, n_qubits: int) -> np.ndarray:
    """Apply Ry(theta) on qubit to state vector."""
    dim = 2**n_qubits
    new_state = state.copy()
    cos_t = np.cos(theta / 2)
    sin_t = np.sin(theta / 2)
    for i in range(dim):
        if (i >> (n_qubits - 1 - qubit)) & 1 == 0:
            j = i | (1 << (n_qubits - 1 - qubit))
            new_state[i] = cos_t * state[i] - sin_t * state[j]
            new_state[j] = sin_t * state[i] + cos_t * state[j]
    return new_state


def _apply_cnot(state: np.ndarray, control: int, target: int, n_qubits: int) -> np.ndarray:
    """Apply CNOT(control, target) to state vector."""
    dim = 2**n_qubits
    new_state = state.copy()
    for i in range(dim):
        c_bit = (i >> (n_qubits - 1 - control)) & 1
        if c_bit == 1:
            j = i ^ (1 << (n_qubits - 1 - target))
            new_state[i] = state[j]
            new_state[j] = state[i]
    return new_state


def vqe_objective(params: np.ndarray, n_qubits: int, n_layers: int, H: np.ndarray) -> float:
    """VQE cost function: ⟨ψ(θ)|H|ψ(θ)⟩."""
    state = _state_from_hea(params, n_qubits, n_layers)
    return float(np.real(state.conj() @ H @ state))


def vqd_objective(
    params: np.ndarray,
    n_qubits: int,
    n_layers: int,
    H: np.ndarray,
    prev_states: list,
    penalty: float,
) -> float:
    """VQD cost function: ⟨ψ(θ)|H|ψ(θ)⟩ + Σᵢ β|⟨ψ(θ)|φᵢ⟩|²."""
    state = _state_from_hea(params, n_qubits, n_layers)
    energy = float(np.real(state.conj() @ H @ state))
    overlap_penalty = 0.0
    for ps in prev_states:
        overlap_penalty += penalty * abs(np.dot(state.conj(), ps))**2
    return energy + overlap_penalty


def run_vqd(n_qubits: int, n_layers: int, penalty: float) -> None:
    """Run full VQD example: VQE ground state → VQD first excited state."""
    from scipy.optimize import minimize

    H = _make_hamiltonian_matrix(n_qubits)
    n_params = n_qubits * n_layers

    # Exact eigenvalues for reference
    eigenvalues = np.linalg.eigvalsh(H)

    # Step 1: VQE for ground state
    print(f"=== VQD Example: H = Z₀ + ... + Z_{{n-1}} on {n_qubits} qubits ===")
    print(f"n_layers={n_layers}, penalty={penalty}, n_params={n_params}")
    print()
    print(f"Exact eigenvalues: {eigenvalues}")
    print()

    print("--- Step 1: VQE (ground state) ---")
    best_energy = float("inf")
    best_params = None
    for trial in range(5):
        x0 = np.random.uniform(0, 2 * np.pi, n_params)
        res = minimize(vqe_objective, x0, args=(n_qubits, n_layers, H), method="COBYLA")
        if res.fun < best_energy:
            best_energy = res.fun
            best_params = res.x
    gs_state = _state_from_hea(best_params, n_qubits, n_layers)
    print(f"VQE ground state energy: {best_energy:.6f}  (exact: {eigenvalues[0]:.6f})")
    print()

    # Step 2: VQD for first excited state
    print("--- Step 2: VQD (first excited state) ---")
    best_vqd_energy = float("inf")
    best_vqd_params = None
    for trial in range(5):
        x0 = np.random.uniform(0, 2 * np.pi, n_params)
        res = minimize(
            vqd_objective,
            x0,
            args=(n_qubits, n_layers, H, [gs_state], penalty),
            method="COBYLA",
        )
        if res.fun < best_vqd_energy:
            best_vqd_energy = res.fun
            best_vqd_params = res.x

    es_state = _state_from_hea(best_vqd_params, n_qubits, n_layers)
    es_energy = float(np.real(es_state.conj() @ H @ es_state))
    overlap_with_gs = abs(np.dot(es_state.conj(), gs_state))**2

    print(f"VQD first excited state energy: {es_energy:.6f}  (exact: {eigenvalues[1]:.6f})")
    print(f"Overlap with ground state: {overlap_with_gs:.6f}")
    print()
    print("✓ Run complete.")


def main():
    parser = argparse.ArgumentParser(description="Variational Quantum Deflation (VQD)")
    parser.add_argument("--n-qubits", type=int, default=2, help="Number of qubits (default: 2)")
    parser.add_argument("--n-layers", type=int, default=2, help="Number of HEA layers (default: 2)")
    parser.add_argument("--penalty", type=float, default=10.0, help="Overlap penalty β (default: 10.0)")
    args = parser.parse_args()

    np.random.seed(42)
    run_vqd(args.n_qubits, args.n_layers, args.penalty)


if __name__ == "__main__":
    main()

Stdout

=== VQD Example: H = Z₀ + ... + Z_{n-1} on 2 qubits ===
n_layers=2, penalty=10.0, n_params=4

Exact eigenvalues: [-2.  0.  0.  2.]

--- Step 1: VQE (ground state) ---
VQE ground state energy: -2.000000  (exact: -2.000000)

--- Step 2: VQD (first excited state) ---
VQD first excited state energy: -0.000000  (exact: 0.000000)
Overlap with ground state: 0.000000

✓ Run complete.