Spaces:
Runtime error
Runtime error
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,
}
|