Source code for quantum_mind.problems
r"""The binary optimisation container used by trust-aware task allocation.
A :class:`Qubo` is the problem :math:`\min_x\, x^T Q x + \text{offset}` over :math:`x \in \{0, 1\}^n`.
:func:`quantum_mind.applications.allocation.trust_aware_allocation` builds one from each person's trust in
the robot, and small instances are solved exactly with :meth:`Qubo.brute_force`. General-purpose problem
builders (max-cut, knapsack, portfolio) and solvers are left to dedicated optimisation libraries.
"""
from __future__ import annotations
from dataclasses import dataclass, field
import itertools
import numpy as np
__all__ = ['Qubo']
[docs]
@dataclass
class Qubo:
r"""Quadratic unconstrained binary optimisation problem.
Minimise :math:`x^T Q x + \text{offset}` over :math:`x \in \{0, 1\}^n`.
Parameters
----------
Q : array_like
``(n, n)`` coefficient matrix (any square matrix; see :meth:`normalised`).
offset : float, optional
Constant energy offset.
labels : list of str, optional
Variable names (default ``x0, x1, ...``).
name : str, optional
Problem name.
"""
Q: np.ndarray
offset: float = 0.0
labels: list = field(default_factory=list)
name: str = 'QUBO'
def __post_init__(self):
self.Q = np.asarray(self.Q, float)
if not self.labels:
self.labels = ['x%d' % i for i in range(self.n)]
@property
def n(self):
"""int: number of binary variables."""
return self.Q.shape[0]
[docs]
def normalised(self):
"""Upper-triangular form with the same energies.
Returns
-------
Qubo
"""
U = np.triu(self.Q) + np.triu(self.Q.T, 1)
return Qubo(U, self.offset, list(self.labels), self.name)
[docs]
def energy(self, x):
"""Energy of bit strings.
Parameters
----------
x : array_like
One bit string (1-D) or one per row (2-D).
Returns
-------
float or numpy.ndarray
"""
x = np.asarray(x, float)
if x.ndim == 1:
return float(x @ self.Q @ x + self.offset)
return np.einsum('bi,ij,bj->b', x, self.Q, x) + self.offset
[docs]
def all_energies(self):
"""Energies of all :math:`2^n` bit strings.
Index k holds the bits of k with :math:`x_0` the least significant bit (Qiskit ordering).
Returns
-------
numpy.ndarray
Raises
------
ValueError
If n > 22.
"""
if self.n > 22:
raise ValueError('too many variables for exhaustive enumeration')
k = np.arange(2 ** self.n)
X = ((k[:, None] >> np.arange(self.n)) & 1).astype(float)
return self.energy(X)
[docs]
def brute_force(self):
"""Exact minimum by enumeration (small n).
Returns
-------
x : numpy.ndarray
Optimal bit string.
energy : float
"""
e = self.all_energies(); k = int(np.argmin(e))
return np.array([(k >> i) & 1 for i in range(self.n)]), float(e[k])
[docs]
def to_ising(self):
r"""Equivalent Ising model with spins :math:`s = 1 - 2x \in \{+1, -1\}`.
Returns
-------
h : numpy.ndarray
Local fields.
J : numpy.ndarray
Upper-triangular couplings.
c : float
Constant, so that energy :math:`= h \cdot s + \sum_{i<j} J_{ij} s_i s_j + c`.
"""
U = self.normalised().Q; n = self.n
h = np.zeros(n); J = np.zeros((n, n)); c = self.offset
for i in range(n):
# x_i^2 = x_i = (1 - s_i)/2
h[i] -= U[i, i] / 2; c += U[i, i] / 2
for j in range(i + 1, n):
q = U[i, j] / 4
J[i, j] += q; h[i] -= q; h[j] -= q; c += q
return h, J, c