Source code for tno.quantum.optimization.qubo.components._results._result_interface

"""This module contains the abstract ``ResultInterface`` class.

This class can be used to store the results returned from the solve method, and includes
functions for storing, loading, visualizing and other helper functions.
"""

from __future__ import annotations

import logging
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Any, SupportsFloat, SupportsInt

import numpy as np
from matplotlib import pyplot as plt
from numpy.typing import ArrayLike, NDArray

from tno.quantum.utils import BitVector, BitVectorLike
from tno.quantum.utils.serialization import Serializable
from tno.quantum.utils.validation import check_int, check_real, check_timedelta

if TYPE_CHECKING:
    from datetime import timedelta
    from typing import Self

    from matplotlib.axes import Axes

    from tno.quantum.optimization.qubo.components._freq import Freq

logger = logging.getLogger(__name__)

DEFAULT_PLOT_NBINS = 20


plot_settings: dict[str, Any]
plot_settings = {
    "color": "#649EC9",
    "fontsize": {"title": 24, "axes": 20, "legend": 20, "labelsize": 16},
    "ticksize": (3, 12),
    "spine_linewidth": 2,
    "figsize": (12, 9),
}


[docs]class ResultInterface(ABC, Serializable): """Abstract result base class representing the result of a solver."""
[docs] def __init__( self, best_bitvector: BitVectorLike, best_value: SupportsFloat, freq: Freq, *, execution_time: timedelta | SupportsInt = 0, num_attempts: SupportsInt = 1, ) -> None: """Init of :py:class:`ResultInterface`. Args: best_bitvector: Bitvector corresponding to the best result. best_value: Objective value corresponding to the best result. freq: Frequency object containing the frequency of found bitvectors and energies. execution_time: Time to successfully execute the solve method. num_attempts: Number of attempts it took to successfully execute the solve method. """ self._best_bitvector = BitVector(best_bitvector) self._best_value = check_real(best_value, "best_value") self._freq = freq self._execution_time = check_timedelta( execution_time, "execution_time", l_bound=0 ) self._num_attempts = check_int(num_attempts, "num_attempts", l_bound=0)
[docs] @classmethod @abstractmethod def from_result(cls, *args: Any, **kwargs: Any) -> Self: """Abstract method which parses the result of solver backend. The arguments for this function are determined by the implementing class. """
@property def best_bitvector(self) -> BitVector: """Bitvector corresponding to the best result.""" return self._best_bitvector @property def best_value(self) -> float: """Objective value corresponding to the best result.""" return self._best_value @property def freq(self) -> Freq: """Frequency object containing frequency of found bitvectors and energies.""" return self._freq @property def execution_time(self) -> timedelta: """Time to successfully execute the solve method.""" return self._execution_time @execution_time.setter def execution_time(self, value: Any) -> None: self._execution_time = check_timedelta(value, "execution_time", l_bound=0) @property def num_attempts(self) -> int: """Number of attempts it took to successfully execute the solve method.""" return self._num_attempts @num_attempts.setter def num_attempts(self, value: Any) -> None: self._num_attempts = check_int(value, "num_attempts", l_bound=0)
[docs] def get_energy_quantiles( self, q: ArrayLike = [0, 0.25, 0.5, 0.75, 1.0], # noqa: B006 **kwargs: Any, ) -> float | NDArray[np.float64]: """Computes the quantiles of the energy. Args: q: Quantile or sequence of quantiles to compute. Each values of `q` must be in the interval $[0, 1]$ . Defaults to ``[0, 0.25, 0.5, 0.75, 1.0]``. kwargs: Additional keyword arguments for the ``numpy.quantile`` method. Returns: If `q` is a single quantile, then the result is a scalar representing the quantile `q` of the energy. If multiple quantiles are given, then an :py:class:`NDArray` is returned with the requested quantile values. """ temp = [] for _, energy, occurrences in self.freq: temp += [energy] * occurrences if isinstance(q, float): return float(np.quantile(temp, q, **kwargs)) return np.asarray(np.quantile(temp, np.asarray(q), **kwargs), dtype=np.float64)
[docs] def plot_hist( self, num_bins: int = DEFAULT_PLOT_NBINS, ax: Axes | None = None, ) -> None: """Plot a histogram of the result. Args: num_bins: Number of bins of the histogram. Defaults to 20. ax: Axes to plot on. If ``None`` (default) a new figure with axis is created. """ if ax is None: _, ax = plt.subplots(figsize=plot_settings["figsize"]) ax.hist( x=self.freq.energies, bins=num_bins, weights=self.freq.num_occurrences, color=plot_settings["color"], ) ax.set_title("Energy Histogram", fontsize=plot_settings["fontsize"]["title"]) ax.set_xlabel("Energies", fontsize=plot_settings["fontsize"]["axes"]) ax.set_ylabel( "Number of Occurrences", fontsize=plot_settings["fontsize"]["axes"] ) for spine in ax.spines.values(): spine.set_linewidth(2) ax.tick_params( labelsize=plot_settings["fontsize"]["labelsize"], width=plot_settings["ticksize"][0], length=plot_settings["ticksize"][1], )
[docs] def get_hist_bin_data( self, num_bins: int = DEFAULT_PLOT_NBINS ) -> tuple[NDArray[np.float64], NDArray[np.float64]]: """Computes histogram of frequency energies and return the bins and their sizes. Args: num_bins: Number of bins of the histogram. Defaults to 20. Returns: Two :py:class:`NDArray` s. The first array is the height of each bin and the second gives the edges of the bins. """ return np.histogram( self.freq.energies, bins=num_bins, weights=self.freq.num_occurrences )
[docs] def check_linear_equality_constraint( self, A: ArrayLike, # noqa: N803 b: ArrayLike, ) -> NDArray[np.bool_]: """Checks if the linear equality constraint $Ax = b$ is met. Args: A: Coefficient matrix of the constraint. b: Right hand side vector. Returns: :py:class:`NDArray` with boolean values for each row of $A$. Element $i$ is ``True`` if constraint $i$ is met ($A_i x = b_i$) and is ``False`` otherwise. """ return np.asarray( np.asarray(A) @ self.best_bitvector == np.asarray(b), dtype=bool )
[docs] def check_linear_inequality_constraint( self, A: ArrayLike, # noqa: N803 b: ArrayLike, ) -> NDArray[np.bool_]: r"""Checks if the linear inequality constraint $Ax \le b$ is met. Args: A: Coefficient matrix of the constraint. b: Right hand side vector. Returns: :py:class:`NDArray` with boolean values for each row of $A$. Element $i$ is ``True`` if constraint $i$ is met ($A_ix \le b_i$) and is ``False`` otherwise. """ return np.asarray( np.asarray(A) @ self.best_bitvector <= np.asarray(b), dtype=bool )