Day 28: Quantum Approximate Optimization Algorithm (QAOA)

🎯 Mục tiêu

  • Hiểu nguyên lý hoạt động của QAOA
  • Triển khai QAOA cho bài toán MaxCut
  • Thiết kế cost function và parameter optimization
  • Áp dụng hybrid quantum-classical optimization

🧠 QAOA - Tổng Quan

Tại sao QAOA?

  • Combinatorial optimization: Giải quyết các bài toán tối ưu hóa tổ hợp
  • Quantum advantage: Tận dụng quantum superposition và entanglement
  • Hybrid approach: Kết hợp quantum và classical optimization
  • Near-term quantum: Phù hợp với NISQ (Noisy Intermediate-Scale Quantum) devices
from qiskit import QuantumCircuit, Aer, execute
from qiskit.circuit import Parameter
from qiskit.algorithms import QAOA
from qiskit.algorithms.optimizers import COBYLA, SPSA
from qiskit.quantum_info import Pauli
from qiskit.opflow import PauliSumOp, I, Z
import numpy as np
import matplotlib.pyplot as plt
import networkx as nx
from scipy.optimize import minimize

🔧 QAOA Fundamentals

1. QAOA Circuit Structure

def qaoa_circuit_demo():
    """
    Demo cấu trúc QAOA circuit cơ bản
    """
    # Parameters
    n_qubits = 4
    p = 2  # Number of layers
    
    # Create parameters
    gamma = Parameter('γ')
    beta = Parameter('β')
    
    # Create QAOA circuit
    qc = QuantumCircuit(n_qubits)
    
    # Initial state: |+⟩^⊗n
    for i in range(n_qubits):
        qc.h(i)
    
    # QAOA layers
    for layer in range(p):
        # Cost Hamiltonian layer (U_C)
        for i in range(n_qubits):
            qc.rz(gamma, i)
        
        # Mixing Hamiltonian layer (U_M)
        for i in range(n_qubits):
            qc.rx(beta, i)
    
    # Measure
    qc.measure_all()
    
    print("QAOA Circuit Structure:")
    print(qc)
    
    return qc

# Tạo QAOA circuit
qaoa_qc = qaoa_circuit_demo()

2. Cost Hamiltonian Construction

def create_cost_hamiltonian(graph):
    """
    Tạo cost Hamiltonian cho MaxCut problem
    """
    # Cost function: maximize sum of edge weights across cut
    # H_C = -∑(i,j)∈E w_ij * Z_i * Z_j
    
    cost_operators = []
    
    for edge in graph.edges():
        i, j = edge
        weight = graph[i][j].get('weight', 1.0)
        
        # Create Z_i * Z_j operator
        pauli_string = ['I'] * max(i, j) + 1
        pauli_string[i] = 'Z'
        pauli_string[j] = 'Z'
        
        # Convert to PauliSumOp
        pauli_op = PauliSumOp.from_list([(''.join(pauli_string), -weight)])
        cost_operators.append(pauli_op)
    
    # Sum all operators
    cost_hamiltonian = sum(cost_operators)
    
    return cost_hamiltonian

def create_mixing_hamiltonian(n_qubits):
    """
    Tạo mixing Hamiltonian: H_M = -∑_i X_i
    """
    mixing_operators = []
    
    for i in range(n_qubits):
        pauli_string = ['I'] * n_qubits
        pauli_string[i] = 'X'
        
        pauli_op = PauliSumOp.from_list([(''.join(pauli_string), -1.0)])
        mixing_operators.append(pauli_op)
    
    mixing_hamiltonian = sum(mixing_operators)
    
    return mixing_hamiltonian

# Test với graph đơn giản
test_graph = nx.Graph()
test_graph.add_edge(0, 1, weight=1.0)
test_graph.add_edge(1, 2, weight=1.0)
test_graph.add_edge(2, 3, weight=1.0)
test_graph.add_edge(3, 0, weight=1.0)

cost_ham = create_cost_hamiltonian(test_graph)
mixing_ham = create_mixing_hamiltonian(4)

print("Cost Hamiltonian:")
print(cost_ham)
print("\nMixing Hamiltonian:")
print(mixing_ham)

🎯 MaxCut Problem với QAOA

1. MaxCut Problem Setup

def maxcut_problem_demo():
    """
    Demo QAOA cho MaxCut problem
    """
    # Tạo graph
    G = nx.Graph()
    G.add_edge(0, 1, weight=1.0)
    G.add_edge(1, 2, weight=1.0)
    G.add_edge(2, 3, weight=1.0)
    G.add_edge(3, 0, weight=1.0)
    G.add_edge(0, 2, weight=0.5)
    
    # Visualize graph
    plt.figure(figsize=(8, 6))
    pos = nx.spring_layout(G)
    nx.draw(G, pos, with_labels=True, node_color='lightblue', 
            node_size=500, font_size=16, font_weight='bold')
    
    # Draw edge weights
    edge_labels = nx.get_edge_attributes(G, 'weight')
    nx.draw_networkx_edge_labels(G, pos, edge_labels=edge_labels)
    
    plt.title('MaxCut Problem Graph')
    plt.show()
    
    return G

# Tạo MaxCut problem
maxcut_graph = maxcut_problem_demo()

2. QAOA Implementation cho MaxCut

def qaoa_maxcut_implementation(graph, p=2):
    """
    Triển khai QAOA cho MaxCut
    """
    n_qubits = len(graph.nodes())
    
    # Create cost and mixing Hamiltonians
    cost_hamiltonian = create_cost_hamiltonian(graph)
    mixing_hamiltonian = create_mixing_hamiltonian(n_qubits)
    
    # Create QAOA
    qaoa = QAOA(
        optimizer=COBYLA(maxiter=100),
        quantum_instance=Aer.get_backend('qasm_simulator'),
        reps=p
    )
    
    # Solve
    result = qaoa.solve(cost_hamiltonian)
    
    print(f"Optimal parameters: {result.optimal_parameters}")
    print(f"Optimal value: {result.optimal_value}")
    print(f"Optimal point: {result.optimal_point}")
    
    return result, qaoa

# Chạy QAOA cho MaxCut
qaoa_result, qaoa_algorithm = qaoa_maxcut_implementation(maxcut_graph)

3. Classical Solution Comparison

def classical_maxcut_solution(graph):
    """
    Giải MaxCut bằng classical algorithm để so sánh
    """
    # Simple greedy algorithm
    nodes = list(graph.nodes())
    n = len(nodes)
    
    best_cut = 0
    best_partition = None
    
    # Try all possible partitions
    for i in range(2**(n-1)):  # Only need to check half due to symmetry
        partition = []
        for j in range(n):
            if (i >> j) & 1:
                partition.append(nodes[j])
        
        # Calculate cut value
        cut_value = 0
        for edge in graph.edges():
            u, v = edge
            if (u in partition) != (v in partition):  # Different sides
                cut_value += graph[u][v]['weight']
        
        if cut_value > best_cut:
            best_cut = cut_value
            best_partition = partition
    
    return best_cut, best_partition

# So sánh classical vs quantum
classical_cut, classical_partition = classical_maxcut_solution(maxcut_graph)
print(f"Classical MaxCut value: {classical_cut}")
print(f"Classical partition: {classical_partition}")
print(f"QAOA MaxCut value: {-qaoa_result.optimal_value}")  # Note: QAOA minimizes, so we negate

🔄 Parameter Optimization

1. Manual Parameter Optimization

def manual_parameter_optimization(graph, p=2):
    """
    Tối ưu hóa parameters thủ công
    """
    n_qubits = len(graph.nodes())
    cost_hamiltonian = create_cost_hamiltonian(graph)
    
    def objective_function(params):
        # Split parameters into gamma and beta
        gamma = params[:p]
        beta = params[p:]
        
        # Create QAOA circuit
        qc = QuantumCircuit(n_qubits)
        
        # Initial state
        for i in range(n_qubits):
            qc.h(i)
        
        # QAOA layers
        for layer in range(p):
            # Cost layer
            for i in range(n_qubits):
                qc.rz(gamma[layer], i)
            
            # Mixing layer
            for i in range(n_qubits):
                qc.rx(beta[layer], i)
        
        qc.measure_all()
        
        # Execute
        backend = Aer.get_backend('qasm_simulator')
        job = execute(qc, backend, shots=1000)
        result = job.result()
        counts = result.get_counts()
        
        # Calculate expectation value
        expectation = 0
        for bitstring, count in counts.items():
            # Convert bitstring to measurement result
            measurement = [int(bit) for bit in bitstring]
            
            # Calculate cost for this measurement
            cost = 0
            for edge in graph.edges():
                i, j = edge
                if measurement[i] != measurement[j]:  # Different sides
                    cost += graph[i][j]['weight']
            
            expectation += (count / 1000) * cost
        
        return -expectation  # Minimize negative of expectation
    
    # Initial parameters
    initial_params = np.random.rand(2*p) * 2 * np.pi
    
    # Optimize
    result = minimize(objective_function, initial_params, method='COBYLA')
    
    print(f"Optimal parameters: {result.x}")
    print(f"Optimal value: {-result.fun}")
    
    return result

# Chạy manual optimization
manual_result = manual_parameter_optimization(maxcut_graph)

2. Parameter Landscape Visualization

def visualize_parameter_landscape(graph, p=1):
    """
    Trực quan hóa parameter landscape
    """
    n_qubits = len(graph.nodes())
    cost_hamiltonian = create_cost_hamiltonian(graph)
    
    def objective_2d(params):
        # For p=1, we have 2 parameters: gamma and beta
        gamma, beta = params
        
        # Create circuit
        qc = QuantumCircuit(n_qubits)
        
        # Initial state
        for i in range(n_qubits):
            qc.h(i)
        
        # Single QAOA layer
        for i in range(n_qubits):
            qc.rz(gamma, i)
        
        for i in range(n_qubits):
            qc.rx(beta, i)
        
        qc.measure_all()
        
        # Execute
        backend = Aer.get_backend('qasm_simulator')
        job = execute(qc, backend, shots=1000)
        result = job.result()
        counts = result.get_counts()
        
        # Calculate expectation
        expectation = 0
        for bitstring, count in counts.items():
            measurement = [int(bit) for bit in bitstring]
            cost = 0
            for edge in graph.edges():
                i, j = edge
                if measurement[i] != measurement[j]:
                    cost += graph[i][j]['weight']
            expectation += (count / 1000) * cost
        
        return -expectation
    
    # Create parameter grid
    gamma_range = np.linspace(0, 2*np.pi, 20)
    beta_range = np.linspace(0, 2*np.pi, 20)
    gamma_grid, beta_grid = np.meshgrid(gamma_range, beta_range)
    
    # Calculate objective values
    Z = np.zeros_like(gamma_grid)
    for i in range(len(gamma_range)):
        for j in range(len(beta_range)):
            Z[i, j] = objective_2d([gamma_grid[i, j], beta_grid[i, j]])
    
    # Plot
    plt.figure(figsize=(10, 8))
    contour = plt.contourf(gamma_grid, beta_grid, Z, levels=20, cmap='viridis')
    plt.colorbar(contour, label='Objective Value')
    plt.xlabel('γ (gamma)')
    plt.ylabel('β (beta)')
    plt.title('QAOA Parameter Landscape (p=1)')
    plt.show()
    
    return gamma_grid, beta_grid, Z

# Visualize parameter landscape
gamma_g, beta_g, obj_values = visualize_parameter_landscape(maxcut_graph)

📊 Performance Analysis

1. QAOA vs Classical Comparison

def qaoa_performance_analysis():
    """
    Phân tích hiệu suất QAOA vs classical algorithms
    """
    # Test trên nhiều graph sizes
    graph_sizes = [4, 6, 8, 10]
    p_values = [1, 2, 3]
    
    results = {}
    
    for size in graph_sizes:
        # Create random graph
        G = nx.random_regular_graph(3, size)  # 3-regular graph
        for edge in G.edges():
            G[edge[0]][edge[1]]['weight'] = np.random.rand()
        
        # Classical solution
        classical_cut, _ = classical_maxcut_solution(G)
        
        results[size] = {'classical': classical_cut, 'qaoa': {}}
        
        for p in p_values:
            try:
                result, _ = qaoa_maxcut_implementation(G, p)
                qaoa_cut = -result.optimal_value
                results[size]['qaoa'][p] = qaoa_cut
            except:
                results[size]['qaoa'][p] = None
    
    # Plot results
    plt.figure(figsize=(12, 8))
    
    for size in graph_sizes:
        classical = results[size]['classical']
        qaoa_values = [results[size]['qaoa'].get(p, 0) for p in p_values]
        
        plt.subplot(2, 2, graph_sizes.index(size) + 1)
        plt.bar(['Classical'] + [f'QAOA p={p}' for p in p_values], 
                [classical] + qaoa_values)
        plt.title(f'Graph Size {size}')
        plt.ylabel('MaxCut Value')
        plt.xticks(rotation=45)
    
    plt.tight_layout()
    plt.show()
    
    return results

# Chạy performance analysis
performance_results = qaoa_performance_analysis()

2. Approximation Ratio Analysis

def approximation_ratio_analysis():
    """
    Phân tích approximation ratio của QAOA
    """
    # Test trên nhiều instances
    n_instances = 20
    graph_size = 6
    
    approximation_ratios = []
    
    for i in range(n_instances):
        # Create random graph
        G = nx.random_regular_graph(3, graph_size)
        for edge in G.edges():
            G[edge[0]][edge[1]]['weight'] = np.random.rand()
        
        # Classical optimal
        classical_cut, _ = classical_maxcut_solution(G)
        
        # QAOA solution
        try:
            result, _ = qaoa_maxcut_implementation(G, p=2)
            qaoa_cut = -result.optimal_value
            
            # Approximation ratio
            ratio = qaoa_cut / classical_cut
            approximation_ratios.append(ratio)
        except:
            continue
    
    # Plot distribution
    plt.figure(figsize=(10, 6))
    plt.hist(approximation_ratios, bins=10, alpha=0.7, edgecolor='black')
    plt.axvline(np.mean(approximation_ratios), color='red', linestyle='--', 
                label=f'Mean: {np.mean(approximation_ratios):.3f}')
    plt.xlabel('Approximation Ratio')
    plt.ylabel('Frequency')
    plt.title('QAOA Approximation Ratio Distribution')
    plt.legend()
    plt.show()
    
    print(f"Average approximation ratio: {np.mean(approximation_ratios):.3f}")
    print(f"Standard deviation: {np.std(approximation_ratios):.3f}")
    
    return approximation_ratios

# Chạy approximation ratio analysis
approx_ratios = approximation_ratio_analysis()

📚 Bài Tập Thực Hành

Bài tập 1: QAOA cho Graph Coloring

def qaoa_graph_coloring():
    """
    Áp dụng QAOA cho Graph Coloring problem
    """
    # Graph coloring: minimize number of colors needed
    # Cost function: penalize adjacent vertices with same color
    
    G = nx.Graph()
    G.add_edge(0, 1)
    G.add_edge(1, 2)
    G.add_edge(2, 3)
    G.add_edge(3, 0)
    G.add_edge(0, 2)
    
    # Create cost Hamiltonian for graph coloring
    # H_C = ∑(i,j)∈E δ(c_i, c_j) where δ is Kronecker delta
    
    n_qubits = len(G.nodes()) * 2  # 2 qubits per node for 4 colors
    
    def create_coloring_cost_hamiltonian(graph, n_colors=4):
        cost_operators = []
        
        for edge in graph.edges():
            i, j = edge
            
            # For each color, penalize if both vertices have same color
            for color in range(n_colors):
                # Create operator that checks if both vertices have same color
                pauli_string = ['I'] * (len(graph.nodes()) * n_colors)
                
                # Set qubits for color 'color' for both vertices
                pauli_string[i * n_colors + color] = 'Z'
                pauli_string[j * n_colors + color] = 'Z'
                
                # This operator gives +1 if both vertices have same color
                pauli_op = PauliSumOp.from_list([(''.join(pauli_string), 1.0)])
                cost_operators.append(pauli_op)
        
        return sum(cost_operators)
    
    cost_ham = create_coloring_cost_hamiltonian(G)
    
    # Implement QAOA for graph coloring
    # (This is a simplified version - full implementation would be more complex)
    
    print("Graph Coloring Cost Hamiltonian created")
    return G, cost_ham

# Chạy graph coloring QAOA
coloring_graph, coloring_cost = qaoa_graph_coloring()

Bài tập 2: QAOA với Custom Mixing Hamiltonian

def custom_mixing_hamiltonian_qaoa():
    """
    QAOA với custom mixing Hamiltonian
    """
    # Create custom mixing Hamiltonian: H_M = -∑_i (X_i + Y_i)
    
    def create_custom_mixing_hamiltonian(n_qubits):
        mixing_operators = []
        
        for i in range(n_qubits):
            # X operator
            pauli_string_x = ['I'] * n_qubits
            pauli_string_x[i] = 'X'
            pauli_op_x = PauliSumOp.from_list([(''.join(pauli_string_x), -1.0)])
            
            # Y operator
            pauli_string_y = ['I'] * n_qubits
            pauli_string_y[i] = 'Y'
            pauli_op_y = PauliSumOp.from_list([(''.join(pauli_string_y), -1.0)])
            
            mixing_operators.extend([pauli_op_x, pauli_op_y])
        
        return sum(mixing_operators)
    
    # Test với simple graph
    G = nx.Graph()
    G.add_edge(0, 1, weight=1.0)
    G.add_edge(1, 2, weight=1.0)
    G.add_edge(2, 0, weight=1.0)
    
    cost_ham = create_cost_hamiltonian(G)
    custom_mixing_ham = create_custom_mixing_hamiltonian(3)
    
    print("Custom mixing Hamiltonian created")
    print(custom_mixing_ham)
    
    return G, cost_ham, custom_mixing_ham

# Chạy custom mixing QAOA
custom_graph, custom_cost, custom_mixing = custom_mixing_hamiltonian_qaoa()

Bài tập 3: QAOA Parameter Scheduling

def qaoa_parameter_scheduling():
    """
    Tối ưu hóa parameter scheduling cho QAOA
    """
    # Implement different parameter initialization strategies
    
    def linear_schedule(p):
        """Linear parameter schedule"""
        gamma = np.linspace(0, 2*np.pi, p)
        beta = np.linspace(0, np.pi, p)
        return np.concatenate([gamma, beta])
    
    def random_schedule(p):
        """Random parameter initialization"""
        return np.random.rand(2*p) * 2 * np.pi
    
    def optimal_schedule(p):
        """Optimal parameter schedule based on theory"""
        # For MaxCut, optimal parameters follow specific patterns
        gamma = np.ones(p) * np.pi / 4
        beta = np.ones(p) * np.pi / 4
        return np.concatenate([gamma, beta])
    
    # Test different schedules
    G = nx.Graph()
    G.add_edge(0, 1, weight=1.0)
    G.add_edge(1, 2, weight=1.0)
    G.add_edge(2, 3, weight=1.0)
    G.add_edge(3, 0, weight=1.0)
    
    p = 3
    schedules = {
        'Linear': linear_schedule(p),
        'Random': random_schedule(p),
        'Optimal': optimal_schedule(p)
    }
    
    results = {}
    for name, params in schedules.items():
        print(f"Testing {name} schedule: {params}")
        # Here you would run QAOA with these parameters
        results[name] = params
    
    return results

# Chạy parameter scheduling
schedule_results = qaoa_parameter_scheduling()

🎯 Kết Quả Mong Đợi

  • Hiểu rõ nguyên lý QAOA và ứng dụng cho optimization problems
  • Có thể triển khai QAOA cho MaxCut và các bài toán khác
  • Tối ưu hóa parameters hiệu quả
  • So sánh hiệu suất với classical algorithms

📖 Tài Liệu Tham Khảo