Source code for QHyper.solvers.gate_based.pennylane.qaoa

import pennylane as qml
from pennylane import numpy as np

from typing import Callable, cast

from dataclasses import dataclass, field

from QHyper.problems.base import Problem
from QHyper.optimizers import (
        OptimizationResult, Optimizer, Dummy, OptimizationParameter)

from QHyper.converter import Converter
from QHyper.polynomial import Polynomial
from QHyper.solvers.base import Solver, SolverResult


[docs] @dataclass class QAOA(Solver): """ Clasic QAOA implementation. Attributes ---------- problem : Problem The problem to be solved. layers : int Number of layers. gamma : OptimizationParameter Vector of gamma angles used in cost Hamiltonian. Size of the vector should be equal to the number of layers. beta : OptimizationParameter Vector of beta angles used in mixing Hamiltonian. Size of the vector should be equal to the number of layers. optimizer : Optimizer Optimizer used in the classical part of the algorithm. penalty_weights : list[float] | None Penalty weights used for converting Problem to QUBO. They connect cost function with constraints. If not specified, all penalty weights are set to 1. backend : str Backend for PennyLane. mixer : str Mixer name. Currently only 'pl_x_mixer' is supported. qubo_cache : dict[tuple[float, ...], qml.Hamiltonian] Cache for QUBO. dev : qml.devices.LegacyDevice PennyLane device instance. """ problem: Problem layers: int gamma: OptimizationParameter beta: OptimizationParameter optimizer: Optimizer penalty_weights: list[float] | None = None backend: str = "default.qubit" mixer: str = "pl_x_mixer" qubo_cache: dict[tuple[float, ...], qml.Hamiltonian] = field( default_factory=dict, init=False) dev: qml.devices.LegacyDevice | None = field(default=None, init=False) def __init__( self, problem: Problem, layers: int, gamma: OptimizationParameter, beta: OptimizationParameter, penalty_weights: list[float] | None = None, optimizer: Optimizer = Dummy(), backend: str = "default.qubit", mixer: str = "pl_x_mixer" ) -> None: self.problem = problem self.optimizer = optimizer self.gamma = gamma self.beta = beta self.penalty_weights = penalty_weights self.layers = layers self.backend = backend self.mixer = mixer self.qubo_cache = {} def _get_num_of_wires(self) -> int: if self.dev is None: raise ValueError("Device not initialized") return len(self.dev.wires) def create_cost_operator(self, problem: Problem, penalty_weights: list[float] ) -> qml.Hamiltonian: if tuple(penalty_weights) not in self.qubo_cache: qubo = Converter.create_qubo(problem, penalty_weights) self.qubo_cache[tuple( penalty_weights)] = self._create_cost_operator(qubo) return self.qubo_cache[tuple(penalty_weights)] def _create_cost_operator(self, qubo: Polynomial) -> qml.Hamiltonian: result: qml.Hamiltonian | None = None const = 0 for variables, coeff in qubo.terms.items(): if not variables: const += coeff continue summand: qml.Hamiltonian | None = None for var in variables: if summand and str(var) in summand.wires: continue encoded_var = cast( qml.Hamiltonian, 0.5 * qml.Identity(str(var)) - 0.5 * qml.PauliZ(str(var)) ) summand = (summand @ encoded_var if summand else coeff * encoded_var) result = result + summand if result else summand assert result is not None return (result + const * qml.Identity(result.wires[0])).simplify() def _hadamard_layer(self, cost_operator: qml.Hamiltonian) -> None: for i in cost_operator.wires: qml.Hadamard(str(i)) def _create_mixing_hamiltonian(self, cost_operator: qml.Hamiltonian ) -> qml.Hamiltonian: return qml.qaoa.x_mixer([str(v) for v in cost_operator.wires]) def _circuit( self, angles: list[float], cost_operator: qml.Hamiltonian, ) -> None: def qaoa_layer(gamma: list[float], beta: list[float]) -> None: qml.qaoa.cost_layer(gamma, cost_operator) qml.qaoa.mixer_layer( beta, self._create_mixing_hamiltonian(cost_operator)) gamma, beta = angles[:len(angles) // 2], angles[len(angles) // 2:] self._hadamard_layer(cost_operator) qml.layer(qaoa_layer, self.layers, gamma, beta) def get_expval_circuit( self, penalty_weights: list[float] ) -> Callable[[list[float]], OptimizationResult]: cost_operator = self.create_cost_operator(self.problem, penalty_weights) self.dev = qml.device(self.backend, wires=cost_operator.wires) @qml.qnode(self.dev) def expval_circuit(angles: list[float]) -> OptimizationResult: self._circuit(angles, cost_operator) return cast(float, qml.expval(cost_operator)) def wrapper(angles: list[float]) -> OptimizationResult: return OptimizationResult( expval_circuit(angles), angles ) return wrapper
[docs] def get_probs_func( self, problem: Problem, penalty_weights: list[float] ) -> Callable[[list[float]], list[float]]: """Returns function that takes angles and returns probabilities Parameters ---------- penalty_weights : list[float] Penalty weights for converting Problem to QUBO Returns ------- Callable[[list[float]], float] Returns function that takes angles and returns probabilities """ cost_operator = self.create_cost_operator(problem, penalty_weights) self.dev = qml.device(self.backend, wires=cost_operator.wires) @qml.qnode(self.dev) def probability_circuit(angles: list[float]) -> list[float]: self._circuit(angles, cost_operator) return cast( list[float], qml.probs(wires=cost_operator.wires) ) return probability_circuit
def run_with_probs( self, problem: Problem, angles: list[float], penalty_weights: list[float], ) -> np.recarray: probs = self.get_probs_func(problem, penalty_weights)(angles) recarray = np.recarray((len(probs),), dtype=[(wire, 'i4') for wire in self.dev.wires]+[('probability', 'f8')]) for i, probability in enumerate(probs): solution = format(i, "b").zfill(self._get_num_of_wires()) recarray[i] = *solution, probability return recarray def _run_optimizer(self, penalty_weights: list[float], angles: OptimizationParameter) -> OptimizationResult: return self.optimizer.minimize( self.get_expval_circuit(penalty_weights), angles)
[docs] def solve(self, penalty_weights: list[float] | None = None, gamma: list[float] | None = None, beta: list[float] | None = None) -> SolverResult: if penalty_weights is None and self.penalty_weights is None: penalty_weights = [1.] * (len(self.problem.constraints) + 1) penalty_weights = (self.penalty_weights if penalty_weights is None else penalty_weights) assert penalty_weights is not None gamma_ = self.gamma if gamma is None else self.gamma.update(init=gamma) beta_ = self.beta if beta is None else self.beta.update(init=beta) angles = gamma_ + beta_ opt_res = self._run_optimizer(penalty_weights, angles) gamma_res = opt_res.params[:len(opt_res.params) // 2] beta_res = opt_res.params[len(opt_res.params) // 2:] return SolverResult( self.run_with_probs(self.problem, opt_res.params, penalty_weights), {'gamma': gamma_res, 'beta': beta_res}, opt_res.history, )