Source code for eqc_models.base.polynomial
- from typing import Tuple, Union, List
- import numpy as np
- from eqc_models.base.base import EqcModel
- from eqc_models.base.operators import Polynomial
- from eqc_models.base.constraints import ConstraintsMixIn
- class PolynomialMixin:
-     """This class provides an instance method and property that
-     manage polynomial models.
-     """
-     @property
-     def H(self) -> Tuple[np.ndarray, np.ndarray]:
-         """ 
-         Hamiltonian specified as a polynomial : coefficients, indices 
-         
-         indices are of the format [0, idx-1, ..., idx-d] which must be non-decreasing
-         and each idx-j is a 1-based index of the variable which is a power in the 
-         term. For a polynomial where the highest degree is 3 and specifying a term 
-         such as x_1x_2, the index array is [0, 1, 2]. Another example, x_1^2x_2 is
-         [1, 1, 2].
-         """
-         return self.coefficients, self.indices
-     
-     @H.setter
-     def H(self, value : Tuple[np.ndarray, np.ndarray]):
-         """ Set H directly as coefficients, indices """
-         coefficients, indices = value
-         self.coefficients = coefficients
-         self.indices = indices
-     @property
-     def sparse(self) -> Tuple[np.ndarray, np.ndarray]:
-         return self.H
-     
-     def evaluate(self, solution : np.ndarray) -> float:
-         """ 
-         Evaluate polynomial at solution 
-         
-         :solution: 1-d numpy array with the same length as the number of variables
-         returns a floating point value
-         """
-         value = self.polynomial.evaluate(np.array(solution))
-         return value
-     
-     @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. In that
-         case, the dynamic range is undefined, so an exception is raised. If
-         it is positive, then the maximum of the absolute values is divided
-         by the lowest. The base-10 logarithm of that value is taken and mul-
-         tiplied by 10. This is the dynamic range.
-         Returns
-         ----------
-         
-         float
-         
-         """
-         H = self.H
-         coefficients = np.array(H[0])
-         try:
-             lowest = np.min(np.abs(coefficients[coefficients!=0]))
-         except IndexError:
-             raise ValueError("Dynamic range of a Hamiltonian of all 0 is undefined")
-         highest  = np.max(np.abs(coefficients))
-         return 10*np.log10(highest / lowest)
- [docs]
- class PolynomialModel(PolynomialMixin, EqcModel):
-     """
-     Polynomial model base class.
-     Parameters
-     ------------
-     coefficients: An array of polynomial coeffients.
-     indices: An array of polynomial indices.
-     Examples
-     ------------
-     >>> coeffs = np.array([1, 2, 3])
-     >>> indices = np.array([[0, 0, 1], [0, 1, 1], [1, 1, 1]])
-     >>> from eqc_models.base.polynomial import PolynomialModel        
-     >>> polynomial = PolynomialModel(coeffs, indices)
-     >>> solution = np.array([1, 1, 1])
-     >>> value = polynomial.evaluate(solution)
-     >>> int(value)
-     6
-     >>> polynomial.H # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
-     (array([1, 2, 3]), array([[0, 0, 1],
-        [0, 1, 1],
-        [1, 1, 1]]))    
-     """
-     def __init__(self, coefficients : Union[List, np.ndarray], indices : Union[List, np.ndarray]) -> None:
-         self.coefficients = coefficients
-         self.indices = indices
-     @property
-     def polynomial(self) -> Polynomial:
-         coefficients, indices = self.H
-         return Polynomial(coefficients=coefficients, indices=indices)
- [docs]
- class ConstrainedPolynomialModel(ConstraintsMixIn, PolynomialModel):
-     """
-     Constrained Polynomial model base class.
-     Parameters
-     ------------
-     coefficients: An array of polynomial coeffients.
-     indices: An array of polynomial indices.
-     lhs: Left hand side of the linear constraints.
-     rhs: Right hand side of the linear constraints.
-     """
-     def __init__(self, coefficients : Union[List, np.ndarray], indices : Union[List, np.ndarray],
-                  lhs : np.ndarray, rhs: np.ndarray):
-         self.coefficients = np.array(coefficients)
-         self.indices = np.array(indices).astype(np.int64)
-         self.max_order = self.indices.shape[1]
-         self.lhs = lhs
-         self.rhs = rhs
-     @property
-     def penalties(self):
-         """ 
-         Penalty terms specified as a polynomial: coefficients, indices 
-         
-         indices are of the format [0, idx-1, ..., idx-d] which must be non-decreasing
-         and each idx-j is a 1-based index of the variable which is a power in the 
-         term. For a polynomial where the highest degree is 3 and specifying a term 
-         such as x_1x_2, the index array is [0, 1, 2]. Another example, x_1^2x_2 is
-         [1, 1, 2].
-         Only linear equality constraints are supported. Translate Ax=b into 
-         penalties using the superclass.
-         """
-         indices = []
-         coefficients = []
-         def lpad(index):
-             missing = self.max_order - len(index)
-             if missing > 0:
-                 index = (0,) * missing + index
-             assert len(index) > 0
-             return np.array(index)
-         Pl, Pq = super(ConstrainedPolynomialModel, self).penalties
-         for i in range(Pl.shape[0]):
-             if Pl[i] != 0:
-                 indices.append(lpad((0, i+1)))
-                 coefficients.append(Pl[i])
-             for j in range(i, Pq.shape[1]):
-                 if Pq[i, j] != 0:
-                     indices.append(lpad((i+1, j+1)))
-                     value = Pq[i, j]
-                     if i!=j:
-                         value += Pq[j, i]
-                     coefficients.append(value)
-         return coefficients, indices
- [docs]
-     def evaluatePenalties(self, solution : np.ndarray, include_offset=False) -> float:
-         """
-         Take the polynomial form of the penalties from the penalties property
-         and evaluate the solution. The offset can be included by passing a True
-         value to the `include_offset` keyword argument.
-         Parameters
-         -----------
-         solution : np.ndarray
-             Solution to evaluate for a penalty value
-         include_offset : bool
-             Optional argument indicating whether or not to include the offset value.
-         Returns
-         ---------
-         Penalty value : float
-         Examples 
-         ---------
-         >>> coeff = np.array([-1.0, -1.0])
-         >>> indices = np.array([(0, 1), (0, 2)])
-         >>> lhs = np.array([[1.0, 1.0]])
-         >>> rhs = np.array([1.0])
-         >>> model = ConstrainedPolynomialModel(coeff, indices, lhs, rhs)
-         >>> sol = np.array([1.0, 1.0])
-         >>> lhs@sol - rhs
-         array([1.])
-         >>> model.evaluatePenalties(sol)+model.offset
-         1.0
-         >>> model.evaluatePenalties(sol)
-         0.0
-         >>> model.evaluatePenalties(sol, include_offset=True)
-         1.0
-         
-         """
-         coefficients, indices = self.penalties
-         solution = np.array(solution, dtype=np.float64)
-         polynomial = Polynomial(coefficients, indices)
-         if include_offset:
-             value = self.offset
-         else:
-             value = 0
-         value += polynomial.evaluate(solution)
-         return value
- [docs]
-     def evaluateObjective(self, solution : np.ndarray) -> float:
-         """
-         Take the polynomial coeff and indices from constructor and evalute the 
-         solution with it.
-         Parameters
-         -----------
-         solution : np.ndarray
-             Soluttion to evaluate the objective value
-         Returns
-         --------
-         objective value : float
-         """
-         coefficients = self.coefficients
-         indices = self.indices
-         solution = np.array(solution, dtype=np.float64)
-         polynomial = Polynomial(coefficients, indices)
-         return polynomial.evaluate(solution)
-     @property
-     def H(self) -> Tuple[np.ndarray, np.ndarray]:
-         """ Provide the sparse format for the Hamiltonian """
-         p_coeff, p_indices = self.penalties
-         coefficients, indices = self.coefficients, self.indices
-         terms = {}
-         alpha = self.alpha
-         for index, coeff in zip(p_indices, p_coeff):
-             index = tuple(index)
-             assert len(index) > 1
-             if index not in terms:
-                 terms[index] = alpha * coeff
-             else:
-                 terms[index] += alpha * coeff
-         for index, coeff in zip(indices, coefficients):
-             index = tuple(index)
-             if index not in terms:
-                 terms[index] = coeff
-             else:
-                 terms[index] += coeff
-         indices = [index for index in terms.keys()]
-         indices.sort()
-         coefficients = [terms[tuple(index)] for index in indices]
-         return coefficients, indices