""" qaoa_circuit.py -- QUBO → Ising → Pauli → QAOA Circuit Builds and simulates QAOA (Quantum Approximate Optimization Algorithm) circuits for solving QUBO problems on the Hachimoji state space. Pipeline: QUBO → Ising Hamiltonian → Pauli strings → Quantum circuit → (Optional: Cirq simulation) → Measurement → Solution The circuit uses: - Cost Hamiltonian: e^{-iγ H_C} where H_C = Σ h_i Z_i + Σ J_{ij} Z_i Z_j - Mixer Hamiltonian: e^{-iβ H_M} where H_M = Σ X_i - p layers of alternating cost and mixer evolution Reference: qaoa_adapter.py -- pauli_to_cirq, qaoa_solve_qubo """ from __future__ import annotations import math import random from dataclasses import dataclass, field from typing import Any, Optional import numpy as np # Try to import Cirq for circuit simulation try: import cirq _HAS_CIRQ = True except ImportError: _HAS_CIRQ = False from qubo_builder import QUBO, qubo_to_ising, ising_to_pauli, extract_dominant_state # ========================================================================= # QAOA Circuit Builder # ========================================================================= def build_qaoa_circuit_description( pauli: dict, p_layers: int = 2, gamma: Optional[list[float]] = None, beta: Optional[list[float]] = None, ) -> dict: """Build a JSON-serializable QAOA circuit description. Args: pauli: Pauli dict from ising_to_pauli p_layers: Number of QAOA layers gamma: Cost angles per layer (default: all 0.5) beta: Mixer angles per layer (default: all 0.5) Returns: Circuit description dict with gate sequence """ n = pauli["n"] if gamma is None: gamma = [0.5] * p_layers if beta is None: beta = [0.5] * p_layers # Gate sequence gates: list[dict] = [] # Initial state: |+⟩^⊗n (Hadamard on all qubits) for i in range(n): gates.append({"gate": "H", "target": i}) # QAOA layers for layer in range(p_layers): g = gamma[layer] if layer < len(gamma) else gamma[-1] b = beta[layer] if layer < len(beta) else beta[-1] # Cost Hamiltonian: e^{-iγ H_C} for ps_str, coeff in pauli["terms"]: angle = 2.0 * g * coeff if abs(angle) < 1e-15: continue z_pos = [i for i, c in enumerate(ps_str) if c == "Z"] if len(z_pos) == 1: gates.append({ "gate": "RZ", "target": z_pos[0], "angle": angle, }) elif len(z_pos) == 2: gates.append({ "gate": "CZ", "control": z_pos[0], "target": z_pos[1], "angle": angle, }) # Mixer Hamiltonian: e^{-iβ H_M} = RX(2β) on each qubit for i in range(n): gates.append({ "gate": "RX", "target": i, "angle": 2.0 * b, }) # Measurement for i in range(n): gates.append({"gate": "MEASURE", "target": i}) # Circuit depth = number of non-trivial gates circuit_depth = len([g for g in gates if g["gate"] not in ("H", "MEASURE")]) return { "n_qubits": n, "p_layers": p_layers, "gamma": gamma, "beta": beta, "gates": gates, "circuit_depth": circuit_depth, "num_terms": len(pauli["terms"]), "offset": pauli["offset"], } def _build_qaoa_unitary( pauli: dict, gamma: list[float], beta: list[float], ) -> np.ndarray: """Build the QAOA unitary matrix U(γ,β) = e^{-iβH_M} e^{-iγH_C} ... |+⟩. Uses explicit matrix construction for small n (n ≤ 8). Returns: 2^n × 2^n unitary matrix """ n = pauli["n"] dim = 2 ** n # Start with identity U = np.eye(dim, dtype=complex) # Initial Hadamard H_mat = np.array([[1, 1], [1, -1]], dtype=complex) / math.sqrt(2) H_full = _tensor_power(H_mat, n) U = H_full @ U p_layers = len(gamma) for layer in range(p_layers): g = gamma[layer] b = beta[layer] # Cost evolution: e^{-iγ H_C} H_C = _build_ising_hamiltonian_matrix(pauli) cost_U = _matrix_exp(-1j * g * H_C) U = cost_U @ U # Mixer evolution: e^{-iβ H_M} where H_M = Σ X_i mixer = np.zeros((dim, dim), dtype=complex) for i in range(n): X_i = _pauli_at_i(n, i, np.array([[0, 1], [1, 0]], dtype=complex)) mixer += X_i mixer_U = _matrix_exp(-1j * b * mixer) U = mixer_U @ U return U def _build_ising_hamiltonian_matrix(pauli: dict) -> np.ndarray: """Build the Ising Hamiltonian matrix from Pauli terms.""" n = pauli["n"] dim = 2 ** n H = np.zeros((dim, dim), dtype=complex) for ps_str, coeff in pauli["terms"]: z_pos = [i for i, c in enumerate(ps_str) if c == "Z"] if len(z_pos) == 1: op = _pauli_at_i(n, z_pos[0], np.array([[1, 0], [0, -1]], dtype=complex)) H += coeff * op elif len(z_pos) == 2: ZZ = _pauli_at_i(n, z_pos[0], np.diag([1, -1]).astype(complex)) ZZ = ZZ @ _pauli_at_i(n, z_pos[1], np.diag([1, -1]).astype(complex)) H += coeff * ZZ # Add offset as identity H += pauli["offset"] * np.eye(dim, dtype=complex) return H def _pauli_at_i(n: int, i: int, P: np.ndarray) -> np.ndarray: """Build Pauli operator P acting on qubit i in an n-qubit system.""" result = np.eye(1, dtype=complex) for q in range(n): if q == i: result = np.kron(result, P) else: result = np.kron(result, np.eye(2, dtype=complex)) return result def _tensor_power(A: np.ndarray, k: int) -> np.ndarray: """Compute A^{⊗k} (k-fold tensor power).""" result = np.eye(1, dtype=complex) for _ in range(k): result = np.kron(result, A) return result def _matrix_exp(A: np.ndarray) -> np.ndarray: """Compute matrix exponential e^A via eigendecomposition.""" from scipy.linalg import expm return expm(A) # ========================================================================= # QAOA Simulation (Numpy-based for portability) # ========================================================================= def simulate_qaoa_numpy( qubo: QUBO, p: int = 2, shots: int = 1024, gamma: Optional[list[float]] = None, beta: Optional[list[float]] = None, ) -> dict: """Simulate QAOA using numpy statevector simulation. For n ≤ 8 qubits, we can simulate the full quantum circuit. Returns: { 'optimal_state': str, # bitstring of best solution 'energy': float, # QUBO energy of best solution 'counts': dict, # measurement histogram 'approximation_ratio': float, 'circuit_depth': int, 'parameters': {'gamma': [...], 'beta': [...]}, } """ n = qubo.n if gamma is None: # Default: linearly decreasing gamma gamma = [0.8 * (1 - k / max(p, 1)) + 0.1 for k in range(p)] if beta is None: # Default: linearly increasing beta beta = [0.1 + 0.4 * (k / max(p, 1)) for k in range(p)] # QUBO → Ising → Pauli ising = qubo_to_ising(qubo) pauli = ising_to_pauli(ising) # Circuit description circuit_desc = build_qaoa_circuit_description(pauli, p, gamma, beta) # Full statevector simulation dim = 2 ** n # Initial state: |0...0⟩ psi = np.zeros(dim, dtype=complex) psi[0] = 1.0 # Apply Hadamard to all qubits H = np.array([[1, 1], [1, -1]], dtype=complex) / math.sqrt(2) H_all = _tensor_power(H, n) psi = H_all @ psi for layer in range(p): g = gamma[layer] b = beta[layer] # Cost evolution: e^{-iγ H_C} H_C = _build_cost_hamiltonian_efficient(n, ising) cost_U = _matrix_exp(-1j * g * H_C) psi = cost_U @ psi # Mixer: e^{-iβ H_M} mixer_U = _build_mixer_unitary(n, b) psi = mixer_U @ psi # Get statevector probabilities probs = np.abs(psi) ** 2 # For n <= 10, evaluate ALL states from statevector directly # Pick the state with highest probability that has lowest QUBO energy counts: dict[str, int] = {} best_bits = None best_energy = float("inf") best_prob = 0.0 if n <= 10: # Direct evaluation: check all 2^n states for state_idx in range(dim): bits = format(int(state_idx), f"0{n}b") solution = [int(b) for b in bits] energy = qubo.energy(solution) prob = probs[state_idx] # Track counts for return value if prob > 1e-12: counts[bits] = int(prob * shots) # Pick best: prioritize lower energy, break ties by higher probability if energy < best_energy - 1e-12: best_energy = energy best_bits = bits best_prob = prob elif abs(energy - best_energy) < 1e-12 and prob > best_prob: best_bits = bits best_prob = prob else: # For larger n, sample and track best energy found rng = np.random.default_rng(seed) outcomes = rng.choice(dim, size=shots, p=probs) for outcome in outcomes: bits = format(int(outcome), f"0{n}b") counts[bits] = counts.get(bits, 0) + 1 solution = [int(b) for b in bits] energy = qubo.energy(solution) if energy < best_energy: best_energy = energy best_bits = bits if best_bits is None: best_bits = "0" * n best_solution = [int(b) for b in best_bits] # Compute approximation ratio # Find true ground state by brute force if n <= 8: from qubo_builder import brute_force_qubo bf = brute_force_qubo(qubo) ground_energy = bf["energy"] if ground_energy < 0: approx_ratio = best_energy / ground_energy if ground_energy != 0 else 1.0 else: approx_ratio = ground_energy / best_energy if best_energy != 0 else 1.0 approx_ratio = min(1.0, max(0.0, approx_ratio)) else: approx_ratio = 0.0 # Cannot compute for n > 8 return { "optimal_state": best_bits, "energy": best_energy, "counts": counts, "approximation_ratio": approx_ratio, "circuit_depth": circuit_desc["circuit_depth"], "parameters": {"gamma": gamma, "beta": beta}, "dominant_hachimoji": extract_dominant_state(best_solution), } def _build_cost_hamiltonian_efficient(n: int, ising: dict) -> np.ndarray: """Build Ising Hamiltonian matrix efficiently for small n.""" dim = 2 ** n H = np.zeros((dim, dim), dtype=complex) # Linear terms h_i Z_i for i in range(n): h_i = ising["h"][i] if abs(h_i) < 1e-15: continue # Z_i is diagonal: +h_i for |0⟩, -h_i for |1⟩ for state in range(dim): bit = (state >> i) & 1 sign = 1 if bit == 0 else -1 H[state, state] += h_i * sign # Quadratic terms J_{ij} Z_i Z_j for (i, j), Jij in ising["J"].items(): if abs(Jij) < 1e-15: continue for state in range(dim): bi = (state >> i) & 1 bj = (state >> j) & 1 sign = 1 if (bi == bj) else -1 H[state, state] += Jij * sign # Offset H += ising["offset"] * np.eye(dim, dtype=complex) return H def _build_mixer_unitary(n: int, beta: float) -> np.ndarray: """Build mixer unitary e^{-iβ Σ X_i}. Since X_i commute, e^{-iβ Σ X_i} = ⊗_i e^{-iβ X_i} """ RX = np.array([ [math.cos(beta), -1j * math.sin(beta)], [-1j * math.sin(beta), math.cos(beta)], ], dtype=complex) return _tensor_power(RX, n) # ========================================================================= # QAOA Parameter Optimization # ========================================================================= def optimize_qaoa_parameters( qubo: QUBO, p: int = 2, shots: int = 1024, n_trials: int = 20, ) -> dict: """Optimize QAOA parameters (γ, β) via grid search. Returns the best parameters found and their performance. """ best_result = None best_energy = float("inf") best_params = None # Grid search over parameter space gamma_values = np.linspace(0.1, 1.0, 5) beta_values = np.linspace(0.1, 0.8, 4) for g0 in gamma_values: for b0 in beta_values: gamma = [g0 * (1 - k / max(p, 1)) + 0.05 for k in range(p)] beta = [b0 * (k / max(p, 1)) + 0.1 for k in range(p)] result = simulate_qaoa_numpy(qubo, p=p, shots=shots, gamma=gamma, beta=beta) if result["energy"] < best_energy: best_energy = result["energy"] best_result = result best_params = (gamma, beta) if best_result is not None: best_result["best_gamma"] = best_params[0] best_result["best_beta"] = best_params[1] return best_result or simulate_qaoa_numpy(qubo, p=p, shots=shots) # ========================================================================= # Main QAOA Solver Interface # ========================================================================= def qaoa_solve( qubo: QUBO, p: int = 2, shots: int = 1024, optimize_params: bool = True, ) -> dict: """Build QAOA circuit, simulate, and optimize. Args: qubo: QUBO problem p: QAOA layers shots: Measurement shots optimize_params: If True, search for optimal γ, β Returns: { 'optimal_state': str, # Hachimoji state name (Greek) 'energy': float, # Ground state energy 'approximation_ratio': float, 'circuit_depth': int, 'parameters': {'gamma': [...], 'beta': [...]}, 'counts': dict, # Measurement histogram 'solution': list[int], # Binary assignment } """ if optimize_params and p <= 3: result = optimize_qaoa_parameters(qubo, p=p, shots=shots) else: result = simulate_qaoa_numpy(qubo, p=p, shots=shots) # Map bitstring to Hachimoji state solution = [int(b) for b in result["optimal_state"]] dominant = extract_dominant_state(solution) return { "optimal_state": dominant, "energy": result["energy"], "approximation_ratio": result.get("approximation_ratio", 0.0), "circuit_depth": result["circuit_depth"], "parameters": result["parameters"], "counts": result.get("counts", {}), "solution": solution, "bitstring": result["optimal_state"], } if __name__ == "__main__": from finsler_metric import make_uniform_hachimoji_states from qubo_builder import finsler_to_qubo states = make_uniform_hachimoji_states() qubo = finsler_to_qubo(states) result = qaoa_solve(qubo, p=2, shots=1024) print(f"QAOA result:") print(f" Optimal state: {result['optimal_state']}") print(f" Energy: {result['energy']:.6f}") print(f" Approximation ratio: {result['approximation_ratio']:.4f}") print(f" Circuit depth: {result['circuit_depth']}") print(f" Bitstring: {result['bitstring']}")