""" 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, }