Source code for tno.quantum.optimization.qubo.solvers._dqo._daqo_solver

"""This module contains the `DAQOSolver` class.

This class builds quantum circuits for quantum adiabatic optimization.
"""

from __future__ import annotations

from types import MappingProxyType
from typing import TYPE_CHECKING, Any, Literal, SupportsInt

import numpy as np
import pennylane as qml
from tno.quantum.optimization.qubo.components import Solver
from tno.quantum.utils import BackendConfig
from tno.quantum.utils.validation import check_int

from tno.quantum.optimization.qubo.solvers._dqo._daqo_result import DAQOResult
from tno.quantum.optimization.qubo.solvers._dqo.layers_lib import (
    CostLayer,
    InitialLayer,
)

if TYPE_CHECKING:
    from collections.abc import Mapping
    from typing import TypeAlias

    from numpy.typing import ArrayLike, NDArray
    from tno.quantum.optimization.qubo.components import QUBO

    Schedule = Literal["sinusoidal", "linear"] | ArrayLike


[docs] class DAQOSolver(Solver[DAQOResult]): """Digital Adiabatic Quantum Optimization solver. The :py:class:`DAQOSolver` class solves QUBOs using digital adiabatic quantum optimization. Example: >>> from tno.quantum.optimization.qubo.components import QUBO >>> from tno.quantum.optimization.qubo.solvers import DAQOSolver >>> >>> qubo = QUBO([[1, 2, 3], [4, -50, 6], [7, 8, 9]]) >>> solver = DAQOSolver(n_layers=4) >>> result = solver.solve(qubo) >>> result.best_bitvector BitVector(010) """ non_deterministic = True metadata = MappingProxyType({"supported_schedules": ("linear", "sinusoidal")})
[docs] def __init__( self, n_layers: SupportsInt, backend: BackendConfig | Mapping[str, Any] | None = None, schedule: Schedule = "sinusoidal", ) -> None: r"""Init :py:class:`DAQOSolver`. Args: n_layers: integer value representing the number of layers. The number of layers is equal to the number of Trotter steps. backend: backend used to sample the circuit. schedule: Annealing schedule to use for the adiabatic evolution. If ``"sinusoidal"`` or ``"linear"`` is provided, the respective predefined sinusoidal or linear schedule is used. If a 1D array of length `n_layers` is provided, it is used as the schedule for the problem Hamiltonian, and the schedule for the initial Hamiltonian will be set complementary, that is, such that they sum to one. If a 2D array of shape ``(2, n_layers)`` is provided, the two rows will be used as schedules for the initial Hamiltonian and problem Hamiltonian, respectively. """ self._n_layers = check_int(n_layers, "n_layers", l_bound=1) self._backend = get_default_backend_if_none(backend) self.schedule = schedule
[docs] def sample_qubo(self, qubo: QUBO) -> dict[str, int]: """Sample a QUBO problem. The :py:class:`QUBO` problem is transformed to the corresponding Lenz-Ising model. This Lenz-Ising model is encoded in the cost and counterdiabatic layers. Args: qubo: QUBO problem to sample. Returns: Dictionary with samples. Each key is a bitstring and each correspending value is the numner of times that bitstring was measured. """ if qubo.size == 0: return {"": 1} external_fields, interactions, _ = qubo.to_ising() device = self._backend.get_instance(wires=range(len(external_fields))) quantum_script = self._build_quantum_script(interactions, external_fields) return qml.execute([quantum_script], device)[0] # type: ignore[no-any-return]
def _solve(self, qubo: QUBO) -> DAQOResult: """Solve QUBO using digital adiabatic quantum optimization.""" samples = self.sample_qubo(qubo) properties = { "n_layers": self._n_layers, "backend": self._backend, "schedule": self._schedule, } return DAQOResult.from_result(qubo, samples, properties) def _build_quantum_script( self, interactions: NDArray[np.float64], external_fields: NDArray[np.float64] ) -> qml.tape.QuantumScript: """Build the quantum script for the digital adiabatic quantum optimization.""" norm_constant = max(abs(interactions).max(), abs(external_fields).max()) interactions /= norm_constant external_fields /= norm_constant init_schedule = self.schedule[0, :] cost_schedule = self.schedule[1, :] cost_ham_layer = CostLayer(external_fields, interactions) initial_ham_layer = InitialLayer(len(external_fields)) with qml.queuing.AnnotatedQueue() as queue: initial_ham_layer.prep() for init_strength, cost_strength in zip( init_schedule, cost_schedule, strict=False ): initial_ham_layer(init_strength) cost_ham_layer(cost_strength) qml.counts() shots = check_int(self._backend.options.get("shots", 1000), "shots", l_bound=1) return qml.tape.QuantumScript.from_queue(queue, shots=shots) @property def n_layers(self) -> int: """Get the number of layers.""" return self._n_layers @property def backend(self) -> BackendConfig: """Get the backend configuration.""" return self._backend @property def schedule(self) -> NDArray[np.float64]: """Get the annealing schedules for the initial and problem Hamiltonian. Returns: 2D array of shape ``(2, n_layers)``, whose first row is the annealing schedule for the initial Hamiltonian, and whose second row is the annealing schedule for the problem Hamiltonian. """ return self._schedule @schedule.setter def schedule(self, schedule: Schedule) -> None: """Set the annealing schedule.""" if isinstance(schedule, str): self._schedule = self._schedule_from_str(schedule) return schedule = np.asarray(schedule, dtype=np.float64) if schedule.ndim == 1: self._schedule = self._schedule_from_1d(schedule) return if schedule.ndim == 2: self._schedule = self._schedule_from_2d(schedule) return msg = ( "Invalid schedule value: must either be a string, or a 1D ArrayLike of" "length `n_layers`, or 2D ArrayLike object of shape ``(2, n_layers)``." ) raise ValueError(msg) def _schedule_from_str(self, schedule: str) -> NDArray[np.float64]: """Get annealing schedules from string. Returns: Tuple of two numpy arrays containing the annealing schedules. """ if schedule == "linear": linear = np.linspace(0.0, 1.0, self._n_layers + 2)[1:-1] return np.array([1 - linear, linear], dtype=np.float64) if schedule == "sinusoidal": linear = np.linspace(0.0, 1.0, self._n_layers + 2)[1:-1] sinusoidal = np.sin(0.5 * np.pi * np.sin(0.5 * np.pi * linear) ** 2) ** 2 return np.array([1 - sinusoidal, sinusoidal], dtype=np.float64) msg = ( f"Unsupported schedule '{schedule}'. Supported values are: " f"{self.metadata['supported_schedules']}" ) raise ValueError(msg) def _schedule_from_1d(self, schedule: NDArray[np.float64]) -> NDArray[np.float64]: """Get annealing schedules from 1D array. Returns: Tuple of two numpy arrays containing the annealing schedules. """ if schedule.ndim != 1: msg = ( f"Expected 1-dimensional array, " f"but {schedule.ndim}-dimensional array was given" ) raise ValueError(msg) if len(schedule) != self._n_layers: msg = f"Schedule must have length {self._n_layers}, got {len(schedule)}" raise ValueError(msg) if np.min(schedule) < 0.0 or np.max(schedule) > 1.0: msg = "All values in a 1D schedule must be in the range $[0, 1]$" raise ValueError(msg) if schedule[0] >= schedule[-1]: msg = ( "The first value in a 1D schedule should be " "much smaller than the last value" ) raise ValueError(msg) return np.array([1 - schedule, schedule]) def _schedule_from_2d(self, schedule: NDArray[np.float64]) -> NDArray[np.float64]: """Get annealing schedules from 2D array. Returns: Tuple of two numpy arrays containing the annealing schedules. """ if schedule.ndim != 2: msg = ( f"Expected 2-dimensional array, " f"but {schedule.ndim}-dimensional array was given" ) raise ValueError(msg) if schedule.shape != (2, self._n_layers): msg = f"Schedule must have shape (2, {self._n_layers}), got {len(schedule)}" raise ValueError(msg) if schedule[0, 0] <= schedule[0, -1]: msg = ( "The first row of a 2D schedule represents the schedule for the " "initial Hamiltonian, whose first value should be much larger than " "the last value in the schedule." ) raise ValueError(msg) if schedule[1, 0] >= schedule[1, -1]: msg = ( "The second row of a 2D schedule represents the schedule for the " "problem Hamiltonian, whose first value should be much smaller than " "the last value in the schedule." ) raise ValueError(msg) if schedule[0, 0] < schedule[1, 0]: msg = ( "In a 2D schedule, the first value of the schedule of the " "initial Hamiltonian should be much larger than the first value of " "the schedule of the problem Hamiltonian." ) raise ValueError(msg) if schedule[0, -1] > schedule[1, -1]: msg = ( "In a 2D schedule, the last value of the schedule of the " "initial Hamiltonian should be much smaller than the last value of " "the schedule of the problem Hamiltonian." ) raise ValueError(msg) return schedule
def get_default_backend_if_none( backend: BackendConfig | Mapping[str, Any] | None = None, ) -> BackendConfig: """Set default backend if the one provided is ``None``. Default training backend ``{"name": "default.qubit", "options": {}}``. Args: backend: backend configuration or ``None``. Raises: KeyError: If `backend` does not contain key ``"name"``. Returns: Given backend or the default training backend. """ return BackendConfig.from_mapping( backend if backend is not None else {"name": "default.qubit"} ) DigitalAdiabaticQuantumOptimizationSolver: TypeAlias = DAQOSolver