File size: 6,746 Bytes
2db2be1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
"""
circuits.py — circuit builders per action.

Each builder takes a *validated* `QuantumSpec` and returns a Qiskit
`QuantumCircuit` (un-transpiled, already measured where measurement makes
sense). The Bouncer has already enforced bounds by the time we get here, so
builders assume well-formed inputs.

Notation convention: bitstring readout is little-endian in Qiskit (qubit 0 is
the rightmost / least-significant bit). Counts keys reflect this.
"""

from __future__ import annotations

import math
import numpy as np
from qiskit import QuantumCircuit

from .spec import QuantumSpec


# ── RANDOM ───────────────────────────────────────────────────────────────────

def build_random(spec: QuantumSpec) -> QuantumCircuit:
    """Hadamard on every qubit → measure all. True quantum RNG."""
    n = spec.qubits
    qc = QuantumCircuit(n, n)
    qc.h(range(n))
    qc.measure(range(n), range(n))
    return qc


# ── BELL ─────────────────────────────────────────────────────────────────────

def build_bell(spec: QuantumSpec) -> QuantumCircuit:
    """One Bell pair per requested pair: H(0) → CNOT(0,1), measured."""
    pairs = spec.pairs
    n = pairs * 2
    qc = QuantumCircuit(n, n)
    for p in range(pairs):
        a, b = 2 * p, 2 * p + 1
        qc.h(a)
        qc.cx(a, b)
    qc.measure(range(n), range(n))
    return qc


# ── GROVER ───────────────────────────────────────────────────────────────────

def grover_qubits(items: int) -> int:
    """Number of qubits needed to address `items` states."""
    return max(1, int(math.ceil(math.log2(items))))


def grover_iterations(items: int) -> int:
    """
    Optimal Grover iteration count for `items` states (1 marked).

    Uses the exact per-iteration rotation angle θ = arcsin(√(1/N)) and the
    standard optimum k = round(π/(4θ) − ½), clamped to ≥1. The cruder
    `round(π/4·√N)` is off for small N — e.g. it returns 2 for N=4, which
    over-rotates back to the classical baseline; the true optimum there is 1
    (and achieves ~100% success).
    """
    n = grover_qubits(items)
    N = 2 ** n
    theta = math.asin(math.sqrt(1.0 / N))
    k = round(math.pi / (4 * theta) - 0.5)
    return max(1, int(k))


def _diffuser(n: int) -> QuantumCircuit:
    """The Grover diffusion operator on n qubits (in-place)."""
    qc = QuantumCircuit(n, name="diffuser")
    qc.h(range(n))
    qc.x(range(n))

    # Multi-controlled Z built from H + multi-controlled X + H.
    qc.h(n - 1)
    qc.mcx(list(range(n - 1)), n - 1)
    qc.h(n - 1)

    qc.x(range(n))
    qc.h(range(n))
    return qc


def build_grover(spec: QuantumSpec) -> QuantumCircuit:
    """
    Grover search over `items` states.

    We use exactly the qubits needed to address `items` states
    (ceil(log2(items))), mark the highest-index state |11..1⟩ as the target,
    and apply the optimal number of Grover iterations.
    """
    items = spec.items
    n = grover_qubits(items)
    qc = QuantumCircuit(n, n)

    # Initialize uniform superposition.
    qc.h(range(n))

    # Oracle: mark |11..1⟩ via a multi-controlled Z (phase flip).
    qc.h(n - 1)
    qc.mcx(list(range(n - 1)), n - 1)
    qc.h(n - 1)

    # Optimal iteration count via the exact rotation-angle formula.
    iterations = grover_iterations(items)
    diff = _diffuser(n)
    for _ in range(iterations):
        qc.compose(diff, inplace=True)

    qc.measure(range(n), range(n))
    return qc


# ── QAOA ─────────────────────────────────────────────────────────────────────

def build_qaoa(spec: QuantumSpec) -> QuantumCircuit:
    """
    QAOA for MaxCut on a ring graph, depth p = spec.depth.

    The MaxCut Hamiltonian for a ring of `nodes` vertices is a sum of ZZ
    interactions over ring edges. We use the standard QAOA ansatz:

        |γ,β⟩ = e^{-i β H_M} e^{-i γ H_C}  (repeated p times)

    with H_M = Σ X_i (mixer) and H_C = Σ_{(i,j)∈ring} Z_i Z_j (cost).

    Angles γ, β are fixed heuristic values (γ≈0.8, β≈0.4) suitable for a demo.
    A real deployment would optimise them; here we expose a deterministic,
    reproducible circuit.
    """
    nodes = spec.nodes
    p = spec.depth
    qc = QuantumCircuit(nodes, nodes)

    gamma, beta = 0.8, 0.4  # heuristic demo angles

    # Initial state: uniform superposition (H_M ground state).
    qc.h(range(nodes))

    for _ in range(p):
        # Cost unitaries: ZZ on each ring edge.
        for i in range(nodes):
            j = (i + 1) % nodes
            qc.rzz(2 * gamma, i, j)
        # Mixer unitaries.
        qc.rx(2 * beta, range(nodes))

    qc.measure(range(nodes), range(nodes))
    return qc


# ── VQE (2-qubit H₂) ─────────────────────────────────────────────────────────
#
# Ported from the original `quantum_bouncer.py`. This is a self-contained
# 2-qubit variational eigensolver with a UCCSD-inspired ansatz and a grid
# search over θ. The measured circuit returned here is the *ansatz at the
# optimal θ*; the energy optimisation itself lives in `engine.py` because it
# needs to run many circuits and aggregate counts.

def vqe_ansatz(theta: float) -> QuantumCircuit:
    """UCCSD-inspired 2-qubit H₂ trial state, measured."""
    qc = QuantumCircuit(2, 2)
    qc.x(1)                 # Hartree-Fock reference |01⟩
    qc.ry(theta, 0)         # single-excitation rotation
    qc.cx(0, 1)             # entangling gate
    qc.measure([0, 1], [0, 1])
    return qc


def h2_ground_state_energy(distance: float) -> float:
    """Analytical STO-3G H₂ ground-state energy approximation [Ha]."""
    Re = 0.735
    De = 0.1372
    a = 4.0
    E_min = -1.1372
    return E_min + De * (1.0 - np.exp(-a * (distance - Re))) ** 2


# ── Dispatch ─────────────────────────────────────────────────────────────────

# Actions that yield a single, ready-to-run circuit via a builder.
BUILDERS = {
    "RANDOM": build_random,
    "BELL": build_bell,
    "GROVER": build_grover,
    "QAOA": build_qaoa,
}