Source code for tno.quantum.optimization.qubo.solvers._classical._rs_solver
"""This module contains the ``RSSolver`` class."""
from __future__ import annotations
from typing import TYPE_CHECKING
import numpy as np
from numpy.random import RandomState
from numpy.typing import NDArray
from tno.quantum.optimization.qubo.components import BasicResult, Freq, Solver
from tno.quantum.utils import BitVector
from tno.quantum.utils.validation import (
check_int,
check_random_state,
check_real,
)
if TYPE_CHECKING:
from tno.quantum.optimization.qubo.components import QUBO
[docs]class RSSolver(Solver[BasicResult]):
r"""Relaxation sampler solver.
The Relaxation Sampler Solver solves QUBOs by randomly sampling
bitvectors from the distribution given by the (fractional) solution to the
relaxed QUBO.
Example:
>>> from tno.quantum.optimization.qubo.components import QUBO
>>> from tno.quantum.optimization.qubo.solvers import RSSolver
>>>
>>> qubo = QUBO([[1, 2, 3], [4, -50, 6], [7, 8, 9]])
>>> solver = RSSolver()
>>> result = solver.solve(qubo)
>>> result.best_bitvector
BitVector(010)
"""
[docs] def __init__(
self,
random_state: int | RandomState | None = None,
num_samples: int = 100,
alpha: float = 0.0,
) -> None:
"""Init :py:class:`RSSolver`.
Args:
random_state: Random state for reproducibility. Defaults to ``None``.
num_samples: Number of bitvectors to sample.
alpha: Parameter controlling minimum randomness in the distribution.
If `alpha` is ``0.0``, the distribution remains unchanged. If `alpha` is
greater than ``0.0``, the distribution is a convex combination of the
original distribution and a uniform distribution. Must be in the range
$[0, 1]$.
Raises:
ValueError: If `alpha` is not in $[0, 1]$.
"""
self.random_state = check_random_state(random_state, "random_state")
self.num_samples = check_int(num_samples, "num_samples", l_bound=1)
self.alpha = check_real(alpha, "alpha", l_bound=0, u_bound=1.0)
@staticmethod
def _adjust_distribution(
distribution: NDArray[np.float64],
alpha: float = 0.0,
) -> NDArray[np.float64]:
"""Adjust distribution.
The new distribution is a convex combination of the original
distribution and a uniform, with combination parameter `alpha`.
If `alpha` is zero, the distribution stays unchanged.
Args:
distribution: original distribution.
alpha: convex combination parameter.
Returns:
The adjusted distribution.
"""
return (1 - alpha) * distribution + alpha * 0.5
def _solve(self, qubo: QUBO) -> BasicResult:
"""Use the Relaxation Sampler solver to solve the QUBO.
The solution to the QUBO relaxation is used as a probability
distribution.
"""
distribution = qubo.compute_bounds()
distribution = np.clip(distribution, 0.0, 1.0)
distribution = self._adjust_distribution(distribution, self.alpha)
rand_values = self.random_state.random_sample((self.num_samples, qubo.size))
bit_vector_matrix = (rand_values < distribution).T.astype(np.uint8)
energies = (
np.einsum(
"ij,jk,ki->i", bit_vector_matrix.T, qubo.matrix, bit_vector_matrix
)
+ qubo.offset
)
min_idx = np.argmin(energies)
best_value = energies[min_idx]
best_bitvector = bit_vector_matrix.T[min_idx]
unique, unique_indices, unique_counts = np.unique(
bit_vector_matrix.T, axis=0, return_index=True, return_counts=True
)
frequencies = Freq(
[BitVector(v) for v in unique], energies[unique_indices], unique_counts
)
return BasicResult.from_result(
BitVector(best_bitvector), best_value, frequencies
)