- from typing import Tuple
- import warnings
- import numpy as np
- from eqc_models.base.base import EqcModel
- from eqc_models.base.constraints import ConstraintsMixIn
- from eqc_models.base.operators import QUBO, Polynomial
- class QuadraticMixIn:
- """
- This class provides an instance method and property that
- manage quadratic models.
- """
- @property
- def sparse(self) -> Tuple[np.ndarray, np.ndarray]:
- """
- Put the linear and quadratic terms in a sparse (poly) format
-
- Returns
- ----------
-
- coefficients: List
- indices: List
- """
- C, J = self.H
- C = np.squeeze(C)
- n = self.n
- indices = []
- coefficients = []
-
- for i in range(n):
- if C[i] != 0:
- key = (0, i+1)
- indices.append(key)
- coefficients.append(C[i])
-
- J = np.triu(J) + np.tril(J, -1).T
- for i in range(n):
- for j in range(i, n):
- val = J[i, j]
- if val != 0:
- key = (i+1, j+1)
- indices.append(key)
- coefficients.append(val)
-
- return np.array(coefficients, dtype=np.float32), np.array(indices, dtype=np.int32)
-
- @property
- def polynomial(self) -> Polynomial:
- coefficients, indices = self.sparse
- return Polynomial(coefficients=coefficients, indices=indices)
- def evaluate(self, solution: np.ndarray) -> float:
- """
- Evaluate the solution using the original operator.
-
- """
- sol = np.array(solution)
- C, J = self.H
- return np.squeeze(sol.T @ J @ sol + C.T @ sol)
-
- @property
- def dynamic_range(self) -> float:
- """
- Dynamic range is a measure in decibels of the ratio of the largest
- magnitude coefficient in a problem to the smallest non-zero magnitude
- coefficient.
- The possible range of values are all greater than or equal to 0. The
- calculation is performed by finding the lowest non-zero of the
- absolute value of all the coefficients, which could be empty. The
- values are in two arrays, so the minimum and maximum values of these
- arrays are compared to each other. When there is no non-zero in either
- of the arrays, then an exception is raised indicating that the dynamic
- range of an operator of all zeros is undefined. If the lowest value is
- positive, then the maximum of the absolute values is divided by the
- lowest. The base-10 logarithm of that value is taken and multiplied by
- 10. This is the dynamic range.
- Returns
- ----------
-
- float
-
- """
- C, J = self.H
-
- try:
- min_c = np.min(np.abs(C[C!=0]))
- except IndexError:
- min_c = 1e308
- try:
- min_j = np.min(np.abs(J[J!=0]))
- except IndexError:
- min_j = 1e308
- max_c = np.max(np.abs(C))
- max_j = np.max(np.abs(J))
- lowest = min_c if min_c < min_j else min_j
- highest = max_c if max_c > max_j else max_j
- if lowest > highest:
- raise ValueError("Dynamic range of a Hamiltonian of all 0 is undefined")
- return 10*np.log10(highest / lowest)
- @property
- def qubo(self) -> QUBO:
- """
- Transform the model into QUBO form. Use `upper_bound` to determine
- a log-encoding of the variables.
-
- """
- bin_n = 0
- bits = []
-
- C, J = self.H
-
- upper_bound = self.upper_bound
- if np.sum(upper_bound)!=upper_bound.shape[0]:
- for i in range(upper_bound.shape[0]):
- bits.append(1+np.floor(np.log2(upper_bound[i])))
- bin_n += bits[-1]
- bin_n = int(bin_n)
- Q = np.zeros((bin_n, bin_n), dtype=np.float32)
- Q.shape
- powers = [2**np.arange(bit_count) for bit_count in bits]
- blocks = []
- linear_blocks = []
- for i in range(len(powers)):
-
- linear_blocks.append(C[i]*powers[i])
- row = []
- for j in range(len(powers)):
- mult = J[i,j]
- block = np.outer(powers[i], powers[j])
- block *= mult
- row.append(block)
- blocks.append(row)
- Q[:, :] = np.block(blocks)
- linear_operator = np.hstack(linear_blocks)
- Q += np.diag(linear_operator)
- else:
-
- Q = np.zeros_like(J)
- Q[:, :] = J
- Q += np.diag(np.squeeze(C))
- return QUBO(Q)
- class QuadraticModel(QuadraticMixIn, EqcModel):
- """
- Provides a quadratic operator and device sum constraint support.
- Parameters
- -----------
- J: Quadratic hamiltonian array.
- C: Linear hamiltonian array.
- Examples
- ---------
- >>> C = np.array([1, 2])
- >>> J = np.array([[2, 1], [1, 2]])
- >>> from eqc_models.base.quadratic import QuadraticModel
- >>> model = QuadraticModel(C, J)
- >>> model.upper_bound = np.array([1, 1])
- >>> qubo = model.qubo
- >>> qubo.Q # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
- array([[3., 1.],
- [1., 4.]])
- """
- def __init__(self, C: np.ndarray, J: np.ndarray):
- self._H = C, J
- class ConstrainedQuadraticModel(ConstraintsMixIn, QuadraticModel):
- """
- Provides a constrained quadratic operator and device sum constraint support.
- Parameters
- -----------
- J: Quadratic hamiltonian array.
- C: Linear hamiltonian array.
- lhs: Left hand side of the linear constraints.
- rhs: Right hand side of the linear constraints.
-
- Examples
- -------------------
- >>> C = np.array([1, 2])
- >>> J = np.array([[2, 1], [1, 2]])
- >>> lhs = np.array([[1, 1], [1, 1]])
- >>> rhs = np.array([1, 1])
- >>> from eqc_models.base.quadratic import ConstrainedQuadraticModel
- >>> model = ConstrainedQuadraticModel(C, J, lhs, rhs)
- >>> model.penalty_multiplier = 1
- >>> model.upper_bound = np.array([1, 1])
- >>> qubo = model.qubo
- >>> qubo.Q # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
- array([[1., 3.],
- [3., 2.]])
- """
- def __init__(self, C_obj: np.array, J_obj: np.array, lhs: np.array, rhs: np.array):
- self.quad_objective = J_obj
- self.linear_objective = C_obj
- self.lhs = lhs
- self.rhs = rhs
- self._penalty_multiplier = None
- @property
- def H(self) -> Tuple[np.ndarray, np.ndarray]:
- """
- Return a pair of arrays, the linear and quadratic portions of a quadratic
- operator that has the objective plus penalty functions multiplied by the
- penalty multiplier. The linear terms are the first array and the quadratic
- are the second.
-
- Returns
- -----------
-
- `np.ndarray, np.nedarray`
-
- """
- C = self.linear_objective
- J = self.quad_objective
- pC, pJ = self.penalties
- alpha = self.penalty_multiplier
- return C + alpha * pC, J + alpha * pJ
-
- @ConstraintsMixIn.penalty_multiplier.setter
- def penalty_multiplier(self, value):
- ConstraintsMixIn.penalty_multiplier.fset(self, value)
- @property
- def constraints(self):
- return self.lhs, self.rhs
- def evaluateObjective(self, solution: np.ndarray) -> float:
- J = self.quad_objective
- C = self.linear_objective
- return np.squeeze(C.T @ solution + solution.T@J@solution)