File size: 3,306 Bytes
d3527de
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""
QAOA simulation engine using Qiskit statevector simulation.

Key trick: instead of decomposing the Hamiltonian into individual Rz/Rzz gates
for simulation purposes, we pre-compute the classical cost for every basis state,
convert to phases via exp(-i*gamma*cost), and apply the full diagonal unitary in
one shot using qiskit.circuit.library.Diagonal. This is mathematically identical
but far more efficient and noise-free for statevector simulation.
"""

import numpy as np
from qiskit import QuantumCircuit
from qiskit.circuit.library import Diagonal
from qiskit.quantum_info import Statevector


def simulate_qaoa(gamma: float, beta: float, cost_function, num_qubits: int,
                  shots: int = 1024, seed: int | None = None):
    """
    Simulate a 1-layer (p=1) QAOA circuit and return measurement statistics
    together with the exact complex statevector.

    Parameters
    ----------
    gamma : float
        Cost-layer variational angle.
    beta : float
        Mixer-layer variational angle.
    cost_function : callable
        Accepts `num_qubits` binary integer arguments (MSB first) and returns
        a scalar cost value.
    num_qubits : int
        Number of qubits (3 for standard/equality, 4 for inequality).
    shots : int
        Number of measurement shots for probability estimation.
    seed : int | None
        Random seed for shot sampling (ensures reproducibility).

    Returns
    -------
    probs : np.ndarray, shape (2**num_qubits,)
        Empirical probability of each basis state.
    expected_energy : float
        Weighted average cost over all basis states.
    cost_values : np.ndarray, shape (2**num_qubits,)
        Classical cost for each basis state (diagonal of H_C).
    statevector : Statevector
        Full complex statevector after circuit execution (needed for disc viz).
    """
    num_states = 2 ** num_qubits

    # --- Pre-compute diagonal of H_C ---
    cost_values = np.array([
        cost_function(*[int(b) for b in format(i, f"0{num_qubits}b")])
        for i in range(num_states)
    ])

    # --- Build circuit ---
    phases = np.exp(-1j * gamma * cost_values)
    qc = QuantumCircuit(num_qubits)
    qc.h(range(num_qubits))                    # equal superposition
    qc.append(Diagonal(phases), range(num_qubits))  # cost layer
    qc.rx(2 * beta, range(num_qubits))         # mixer layer

    statevector = Statevector.from_instruction(qc)

    # --- Sample shots ---
    if seed is not None:
        np.random.seed(seed)
    counts = statevector.sample_counts(shots=shots)

    probs = np.zeros(num_states)
    for bitstring, count in counts.items():
        probs[int(bitstring, 2)] = count / shots

    expected_energy = float(np.sum(probs * cost_values))

    return probs, expected_energy, cost_values, statevector


def get_statevector_after_init(num_qubits: int) -> Statevector:
    """Return the statevector after Hadamard initialization only (no cost/mixer)."""
    qc = QuantumCircuit(num_qubits)
    qc.h(range(num_qubits))
    return Statevector.from_instruction(qc)


def get_statevector_after_cost(gamma: float, cost_function, num_qubits: int) -> Statevector:
    """Return the statevector after init + cost layer (beta = 0)."""
    return simulate_qaoa(gamma, 0.0, cost_function, num_qubits, shots=1)[3]