qac-l-cloud / src /circuits.py
IHat0's picture
Upload folder using huggingface_hub
2db2be1 verified
Raw
History Blame Contribute Delete
6.75 kB
"""
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,
}