eqc-models 0.9.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. eqc_models-0.9.8.data/platlib/compile_extensions.py +23 -0
  2. eqc_models-0.9.8.data/platlib/eqc_models/__init__.py +15 -0
  3. eqc_models-0.9.8.data/platlib/eqc_models/algorithms/__init__.py +4 -0
  4. eqc_models-0.9.8.data/platlib/eqc_models/algorithms/base.py +10 -0
  5. eqc_models-0.9.8.data/platlib/eqc_models/algorithms/penaltymultiplier.py +169 -0
  6. eqc_models-0.9.8.data/platlib/eqc_models/allocation/__init__.py +6 -0
  7. eqc_models-0.9.8.data/platlib/eqc_models/allocation/allocation.py +367 -0
  8. eqc_models-0.9.8.data/platlib/eqc_models/allocation/portbase.py +128 -0
  9. eqc_models-0.9.8.data/platlib/eqc_models/allocation/portmomentum.py +137 -0
  10. eqc_models-0.9.8.data/platlib/eqc_models/assignment/__init__.py +5 -0
  11. eqc_models-0.9.8.data/platlib/eqc_models/assignment/qap.py +82 -0
  12. eqc_models-0.9.8.data/platlib/eqc_models/assignment/setpartition.py +170 -0
  13. eqc_models-0.9.8.data/platlib/eqc_models/base/__init__.py +72 -0
  14. eqc_models-0.9.8.data/platlib/eqc_models/base/base.py +150 -0
  15. eqc_models-0.9.8.data/platlib/eqc_models/base/constraints.py +276 -0
  16. eqc_models-0.9.8.data/platlib/eqc_models/base/operators.py +201 -0
  17. eqc_models-0.9.8.data/platlib/eqc_models/base/polyeval.c +11363 -0
  18. eqc_models-0.9.8.data/platlib/eqc_models/base/polyeval.cpython-310-darwin.so +0 -0
  19. eqc_models-0.9.8.data/platlib/eqc_models/base/polyeval.pyx +72 -0
  20. eqc_models-0.9.8.data/platlib/eqc_models/base/polynomial.py +274 -0
  21. eqc_models-0.9.8.data/platlib/eqc_models/base/quadratic.py +250 -0
  22. eqc_models-0.9.8.data/platlib/eqc_models/decoding.py +20 -0
  23. eqc_models-0.9.8.data/platlib/eqc_models/graph/__init__.py +5 -0
  24. eqc_models-0.9.8.data/platlib/eqc_models/graph/base.py +63 -0
  25. eqc_models-0.9.8.data/platlib/eqc_models/graph/hypergraph.py +307 -0
  26. eqc_models-0.9.8.data/platlib/eqc_models/graph/maxcut.py +155 -0
  27. eqc_models-0.9.8.data/platlib/eqc_models/graph/maxkcut.py +184 -0
  28. eqc_models-0.9.8.data/platlib/eqc_models/ml/__init__.py +15 -0
  29. eqc_models-0.9.8.data/platlib/eqc_models/ml/classifierbase.py +99 -0
  30. eqc_models-0.9.8.data/platlib/eqc_models/ml/classifierqboost.py +423 -0
  31. eqc_models-0.9.8.data/platlib/eqc_models/ml/classifierqsvm.py +237 -0
  32. eqc_models-0.9.8.data/platlib/eqc_models/ml/clustering.py +323 -0
  33. eqc_models-0.9.8.data/platlib/eqc_models/ml/clusteringbase.py +112 -0
  34. eqc_models-0.9.8.data/platlib/eqc_models/ml/decomposition.py +363 -0
  35. eqc_models-0.9.8.data/platlib/eqc_models/ml/forecast.py +255 -0
  36. eqc_models-0.9.8.data/platlib/eqc_models/ml/forecastbase.py +139 -0
  37. eqc_models-0.9.8.data/platlib/eqc_models/ml/regressor.py +220 -0
  38. eqc_models-0.9.8.data/platlib/eqc_models/ml/regressorbase.py +97 -0
  39. eqc_models-0.9.8.data/platlib/eqc_models/ml/reservoir.py +106 -0
  40. eqc_models-0.9.8.data/platlib/eqc_models/sequence/__init__.py +5 -0
  41. eqc_models-0.9.8.data/platlib/eqc_models/sequence/tsp.py +217 -0
  42. eqc_models-0.9.8.data/platlib/eqc_models/solvers/__init__.py +12 -0
  43. eqc_models-0.9.8.data/platlib/eqc_models/solvers/qciclient.py +707 -0
  44. eqc_models-0.9.8.data/platlib/eqc_models/utilities/__init__.py +6 -0
  45. eqc_models-0.9.8.data/platlib/eqc_models/utilities/fileio.py +38 -0
  46. eqc_models-0.9.8.data/platlib/eqc_models/utilities/polynomial.py +137 -0
  47. eqc_models-0.9.8.data/platlib/eqc_models/utilities/qplib.py +375 -0
  48. eqc_models-0.9.8.dist-info/LICENSE.txt +202 -0
  49. eqc_models-0.9.8.dist-info/METADATA +139 -0
  50. eqc_models-0.9.8.dist-info/RECORD +52 -0
  51. eqc_models-0.9.8.dist-info/WHEEL +5 -0
  52. eqc_models-0.9.8.dist-info/top_level.txt +2 -0
@@ -0,0 +1,150 @@
1
+ # (C) Quantum Computing Inc., 2024.
2
+ import os
3
+ import logging
4
+ from typing import (Dict, List, Tuple, Union)
5
+ from warnings import warn
6
+ import numpy as np
7
+ from eqc_models.base.operators import Polynomial, QUBO, OperatorNotAvailableError
8
+
9
+ log = logging.getLogger(name=__name__)
10
+
11
+ # base class
12
+ class EqcModel:
13
+ """
14
+
15
+ EqcModel subclasses must provide these properties/methods.
16
+
17
+ :decode: takes a raw solution and translates it into the original problem
18
+ formulation
19
+ :H: property which returns a Hamiltonian operator
20
+ :upper_bound: Let D be an array of length n which contains the largest possible value
21
+ allowed for x[i], which is the variable at index i, 0<=i<n. This means that a x[i]
22
+ is in the domain [0,D[i]]. If the solution type of x[i] is integer, then x[i] is
23
+ in the set of integers, Z, and also 0<=x[i]<=floor(D[i]).
24
+ :qudit_limits: maximum value permitted for each qudit
25
+
26
+ >>> model = EqcModel()
27
+ >>> ub = np.array([1, 1.5, 2])
28
+ >>> model.upper_bound = ub # doctest: +ELLIPSIS
29
+ Traceback (most recent call last):
30
+ ...
31
+ ValueError: ...
32
+ >>> model.upper_bound = np.ones((3,))
33
+ >>> (model.upper_bound==np.ones((3,))).all()
34
+ True
35
+ >>> model.upper_bound = 2*np.ones((3,))
36
+ >>> (model.upper_bound==2).all()
37
+ True
38
+
39
+ """
40
+
41
+ _upper_bound = None
42
+ _H = None
43
+ _machine_slacks = 0
44
+
45
+ def decode(self, solution : np.ndarray) -> np.ndarray:
46
+ """ Manipulate the solution to match the variable count """
47
+
48
+ # ignore any slacks that may have been added during encoding
49
+ solution = solution[:-self.machine_slacks]
50
+
51
+ return solution
52
+
53
+ @property
54
+ def upper_bound(self) -> np.array:
55
+ """
56
+ An array of upper bound values for every variable in the model. Must be integer.
57
+
58
+ """
59
+
60
+ return self._upper_bound
61
+
62
+ @upper_bound.setter
63
+ def upper_bound(self, value : np.array):
64
+ value = np.array(value)
65
+ if (value != value.astype(np.int64)).any():
66
+ raise ValueError("Upper bound values must be integer")
67
+ self._upper_bound = value.astype(np.int64)
68
+
69
+ @property
70
+ def domains(self) -> np.array:
71
+ if self._upper_bound is None:
72
+ raise ValueError("Variable domains are required for model definition")
73
+ return self._upper_bound
74
+
75
+ @domains.setter
76
+ def domains(self, value):
77
+ warn("The domains property is deprecated in favor of naming it upper_bound", DeprecationWarning)
78
+ self._upper_bound = value
79
+
80
+ @property
81
+ def n(self) -> int:
82
+ """ Return the number of variables """
83
+ return int(max(self.upper_bound.shape))
84
+
85
+ @property
86
+ def H(self):
87
+ """ Hamiltonian operator of unknown type """
88
+ return self._H
89
+
90
+ @H.setter
91
+ def H(self, value):
92
+ """ The H setter ensures that the Hamiltonian is properly formatted. """
93
+
94
+ raise NotImplementedError("H property setter not implemented in subclass, can't be set directly")
95
+
96
+ @property
97
+ def sparse(self) -> Tuple[np.ndarray, np.ndarray]:
98
+ # Implement this for the particular subclasses
99
+ raise NotImplementedError("sparse must be implemented in a subclass")
100
+
101
+ @property
102
+ def machine_slacks(self):
103
+ """ Number of slack qudits to add to the model """
104
+ return self._machine_slacks
105
+
106
+ @machine_slacks.setter
107
+ def machine_slacks(self, value:int):
108
+ assert int(value) == value, "value not integer"
109
+ self._machine_slacks = value
110
+
111
+ def evaluateObjective(self, solution : np.ndarray) -> float:
112
+ raise NotImplementedError("evaluateObjective must be implemented in a subclass")
113
+
114
+ def createConfigElements(self) -> Dict:
115
+ obj = {"number_of_nonzero": None}
116
+ return obj
117
+ def createBenchmarkConfig(self, fname : str) -> None:
118
+ obj = self.createConfigElements()
119
+
120
+ @property
121
+ def dynamic_range(self) -> float:
122
+ raise NotImplementedError("EqcModel does not implement dynamic_range")
123
+
124
+ @property
125
+ def polynomial(self) -> Polynomial:
126
+ raise OperatorNotAvailableError("Polynomial operator not available")
127
+
128
+ @property
129
+ def qubo(self) -> QUBO:
130
+ raise OperatorNotAvailableError("QUBO operator not available")
131
+
132
+ class SumConstraintMixin:
133
+
134
+ _sum_constraint = None
135
+
136
+ @property
137
+ def sum_constraint(self):
138
+ return self._sum_constraint
139
+
140
+ @sum_constraint.setter
141
+ def sum_constraint(self, value : Union[float, int]):
142
+ assert value >= 0, "sum_constraint must be greater than or equal to one"
143
+ self._sum_constraint = value
144
+
145
+ class ModelSolver:
146
+ """ Provide a common interface for solver implementations.
147
+ Store a model, implement a solve method."""
148
+
149
+ def solve(self, model:EqcModel, *args, **kwargs) -> Dict:
150
+ raise NotImplementedError()
@@ -0,0 +1,276 @@
1
+ # (C) Quantum Computing Inc., 2024.
2
+ """
3
+ Four useful classes are provided in this module.
4
+
5
+ ConstraintsMixin
6
+ Converts equality constraints to penalties. Depends on the value provided
7
+ for penalty_multiplier.
8
+
9
+ InequalitiesMixin
10
+ Allows inequality constraints, converting to equality constraints before
11
+ penalties by adding slack variables.
12
+
13
+ ConstraintModel
14
+ An example implementation of the ConstraintsMixin.
15
+
16
+ InequalityConstraintModel
17
+ An example implementation of the InequalitiesMixin.
18
+
19
+ >>> lhs = np.array([[1, 1],
20
+ ... [2, 2]])
21
+ >>> rhs = np.array([1, 1])
22
+ >>> senses = ["LE", "GE"]
23
+ >>> model = InequalityConstraintModel()
24
+ >>> model.constraints = lhs, rhs
25
+ >>> model.senses = senses
26
+ >>> A, b = model.constraints
27
+ >>> A
28
+ array([[ 1., 1., 1., 0.],
29
+ [ 2., 2., 0., -1.]])
30
+ >>> model.penalty_multiplier = 1.0
31
+ >>> model.checkPenalty(np.array([1, 0, 0, 1]))
32
+ 0.0
33
+ >>> model.checkPenalty(np.array([1, 1, 0, 0]))
34
+ 10.0
35
+ """
36
+ from typing import (List, Tuple)
37
+ import numpy as np
38
+ from eqc_models.base.base import EqcModel
39
+
40
+
41
+ class ConstraintsMixIn:
42
+ """
43
+ This mixin class contains methods and attributes which transform
44
+ linear constraints into penalties.
45
+
46
+ """
47
+ lhs = None
48
+ rhs = None
49
+ # alpha is the internal name for the penalty multiplier
50
+ # defaulting to 1 as a user experience enhancement
51
+ # while forcing the user to choose a value helps to
52
+ # remind of the need, it is not friendly to get a None
53
+ # type error when an attempt to use it is made.
54
+ alpha = 1
55
+ linear_objective = None
56
+ quad_objective = None
57
+
58
+ @property
59
+ def penalties(self) -> Tuple[np.ndarray, np.ndarray]:
60
+ """ Returns two numpy arrays, one linear and one quadratic pieces of an operator """
61
+ lhs, rhs = self.constraints
62
+ if lhs is None or rhs is None:
63
+ raise ValueError("Constraints lhs and/or rhs are undefined. " +
64
+ "Both must be instantiated numpy arrays.")
65
+ Pq = lhs.T @ lhs
66
+ Pl = -2 * rhs.T @ lhs
67
+ return Pl.T, Pq
68
+
69
+ @property
70
+ def penalty_multiplier(self) -> float:
71
+ return self.alpha
72
+
73
+ @penalty_multiplier.setter
74
+ def penalty_multiplier(self, value: float):
75
+ self.alpha = value
76
+
77
+ @property
78
+ def constraints(self):
79
+ return self.lhs, self.rhs
80
+
81
+ @constraints.setter
82
+ def constraints(self, value: Tuple[np.ndarray, np.ndarray]):
83
+ self.lhs, self.rhs = value
84
+
85
+ @property
86
+ def offset(self) -> float:
87
+ """ Calculate the offset due to the conversion of constraints to penalties """
88
+
89
+ lhs, rhs = self.constraints
90
+
91
+ return np.squeeze(rhs.T@rhs)
92
+
93
+ def evaluate(self, solution : np.ndarray, alpha : float = None, includeoffset:bool=False):
94
+ """
95
+ Compute the objective value plus penalties for the given solution. Including
96
+ the offset will ensure the penalty contribution is non-negative.
97
+
98
+ Parameters
99
+ ----------
100
+
101
+ solution : np.array
102
+ The solution vector for the problem.
103
+
104
+ alpha : float
105
+ Penalty multiplier, optional. This can be used to test different
106
+ multipliers for determination of sufficiently large values.
107
+
108
+ """
109
+ if alpha is None:
110
+ alpha = self.penalty_multiplier
111
+ penalty = self.evaluatePenalties(solution)
112
+ penalty *= alpha
113
+ if includeoffset:
114
+ penalty += alpha * self.offset
115
+ return penalty + self.evaluateObjective(solution)
116
+
117
+ def evaluatePenalties(self, solution) -> float:
118
+ """
119
+ Evaluate penalty function without alpha or offset
120
+
121
+ Parameters
122
+ ----------
123
+
124
+ solution : np.array
125
+ The solution vector for the problem.
126
+
127
+ """
128
+
129
+ Pl, Pq = self.penalties
130
+ qpart = solution.T@Pq@solution
131
+ lpart = Pl.T@solution
132
+ ttlpart = qpart + lpart
133
+ return ttlpart
134
+
135
+ def checkPenalty(self, solution : np.ndarray):
136
+ """
137
+ Get the penalty of the solution.
138
+
139
+ Parameters
140
+ ----------
141
+
142
+ solution : np.array
143
+ The solution vector for the problem.
144
+
145
+ """
146
+
147
+ penalty = self.evaluatePenalties(solution)
148
+ penalty += self.penalty_multiplier * self.offset
149
+ assert penalty >= 0, "Inconsistent model, penalty cannot be less than 0."
150
+
151
+ return penalty
152
+
153
+ class InequalitiesMixin:
154
+ """
155
+ This mixin enables inequality constraints by automatically
156
+ generating slack variables for each inequality
157
+
158
+ This mixin adds a `senses` attribute which has a value for each
159
+ constraint. The values are one of 'EQ', 'LE' or 'GE' for equal
160
+ to, less than or equal to or greater than or equal to. The effect
161
+ of the value is to control whether a slack is added and what
162
+ the sign of the slack variable in the constraint is. Negative
163
+ is used for GE, positive is used for LE and all slack variables
164
+ get a coefficient magnitude of 1.
165
+
166
+ The constraints are modified on demand, so the class members,
167
+ `lhs` and `rhs` remain unmodified.
168
+
169
+ """
170
+
171
+ _senses = None
172
+ @property
173
+ def senses(self) -> List[str]:
174
+ """ Comparison operator by constraint """
175
+
176
+ return self._senses
177
+
178
+ @senses.setter
179
+ def senses(self, value : List[str]):
180
+ self._senses = value
181
+
182
+ @property
183
+ def num_slacks(self) -> int:
184
+ """
185
+ The number of slack variables. Will match the number of inequality
186
+ constraints.
187
+
188
+ Returns
189
+ -------
190
+
191
+ number : int
192
+
193
+ """
194
+ G = self.lhs
195
+ m = G.shape[0]
196
+ senses = self.senses
197
+ num_slacks = sum([0 if senses[i] == "EQ" else 1 for i in range(m)])
198
+ return num_slacks
199
+
200
+ @property
201
+ def constraints(self) -> Tuple[np.ndarray, np.ndarray]:
202
+ """
203
+ Get the general form of the constraints, add slacks where needed
204
+ and return a standard, equality constraint form.
205
+
206
+ """
207
+ G = self.lhs
208
+ h = self.rhs
209
+ senses = self.senses
210
+
211
+ m = G.shape[0]
212
+ n = G.shape[1]
213
+ num_slacks = self.num_slacks
214
+ # Adjusted dimensions for slack variables
215
+ slack_vars = np.zeros((m, num_slacks))
216
+ ii = 0
217
+ for i in range(m):
218
+ rule = senses[i]
219
+ if rule == "LE":
220
+ # Add slack variable for less than or equal constraint
221
+ slack_vars[i, ii] = 1
222
+ ii += 1
223
+ elif rule == "GE":
224
+ # Add negated slack variable for greater than or equal constraint
225
+ slack_vars[i, ii] = -1
226
+ ii += 1
227
+ A = np.hstack((G, slack_vars))
228
+ b = h
229
+
230
+ return A, b
231
+
232
+ @constraints.setter
233
+ def constraints(self, value : Tuple[np.ndarray, np.ndarray]):
234
+ if len(value) != 2:
235
+ raise ValueError("Constraints must be specified as a 2-tuple")
236
+ self.lhs, self.rhs = value
237
+
238
+ class ConstraintModel(ConstraintsMixIn, EqcModel):
239
+ """
240
+ Abstract class for representing linear constrained optimization problems as
241
+ EQC models.
242
+
243
+ """
244
+
245
+ class InequalityConstraintModel(InequalitiesMixin, ConstraintModel):
246
+ """
247
+ Abstract class for a linear constrained optimization model with inequality constraints
248
+
249
+ """
250
+
251
+ # class MIPBinaryModel(ConstraintModel):
252
+ #
253
+ # binary_only_variables = None
254
+ #
255
+ # @property
256
+ # def binaries(self) -> List:
257
+ # return self.binary_only_variables
258
+ #
259
+ # @binaries.setter
260
+ # def binaries(self, value : List) -> None:
261
+ # for item in value:
262
+ # assert int(item) == item, "Index of binary variable must be integer"
263
+ # self.binary_only_variables = value
264
+ #
265
+ # @property
266
+ # def penalties(self) -> Tuple[np.ndarray, np.ndarray]:
267
+ # # get the explicit constraint penalties
268
+ # Pl, Pq = super(MIPBinaryModel, self).penalties
269
+ # if self.binary_only_variables is not None:
270
+ # # add penalties which enforce the selection of 0 or 1 for each binar variable
271
+ # for idx in self.binaries:
272
+ # # add the values x(x-1)^2 -> x^3-2x^2+x
273
+ # indices = [[idx+1, idx+1, idx+1], [0, idx+1, idx+1], [0, 0, idx+1]]
274
+ # coefficients = [1, -2, 1]
275
+ #
276
+ # return Pl, Pq
@@ -0,0 +1,201 @@
1
+ """
2
+ QUBO and Polynomial operators are used in EQC Models to pass the appropriate
3
+ format to the solver. All models must output one or both of QUBO or Polynomial
4
+ types. Each Solver checks for the appropriate type. If a model does not provide
5
+ the type, then it must raise OperatorNotAvailableError.
6
+
7
+ >>> Q = np.array([[1, 2], [0, 1]])
8
+ >>> qubo = QUBO(Q)
9
+ >>> (qubo.Q == np.array([[1, 1], [1, 1]])).all()
10
+ True
11
+ >>> coefficients = [1, 1, 2]
12
+ >>> indices = [(1, 1), (2, 2), (1, 2)]
13
+ >>> poly = Polynomial(coefficients, indices)
14
+ >>> indices = [(1, 1), (2, 2), (2, 1)]
15
+ >>> poly = Polynomial(coefficients, indices)
16
+ Traceback (most recent call last):
17
+ File "<stdin>", line 1, in <module>
18
+ ValueError: Input data to polynomial is not correct: index order must be monotonic
19
+ >>> s = np.array([1, 1])
20
+ >>> (poly.evaluate(s)==qubo.evaluate(s)).all()
21
+ True
22
+ """
23
+ from typing import List
24
+ from dataclasses import dataclass
25
+ import numpy as np
26
+ # polyeval module is a Cython module useful for speeding up computation
27
+ # of an operator's value at a solution. If the Cython module is not
28
+ # available, the poly_eval variable will be set to None, triggering the
29
+ # use of a pure Python method for evaluation
30
+ try:
31
+ from .polyeval import poly_eval
32
+ except ImportError:
33
+ poly_eval = None
34
+
35
+ class OperatorNotAvailableError(Exception):
36
+ """ General error to raise when an operator type is not implemented """
37
+
38
+ @dataclass
39
+ class QUBO:
40
+ """
41
+ Contains a QUBO in a symmetric matrix
42
+
43
+ If the matrix Q is not symmetric already, it will be after __post_init__
44
+
45
+ Parameters
46
+ ----------
47
+
48
+ qubo : np.array
49
+ 2d symmetric matrix of values which describe a quadratic unconstrained
50
+ binary optimization problem.
51
+
52
+ """
53
+ Q: np.ndarray
54
+
55
+ def __post_init__(self):
56
+ Q2 = (self.Q + self.Q.T) / 2
57
+ self.Q = Q2
58
+
59
+ def evaluate(self, solution : np.ndarray):
60
+ return solution.T@self.Q@solution
61
+
62
+ @dataclass
63
+ class Polynomial:
64
+ """
65
+ Represents an operator and evalute the operator at a point or set of points.
66
+ The operator must be a polynomial, possibly multivariate, with powers of up
67
+ to 5, at the time of this version. The representation of a polynomial uses
68
+ a sparse method with two components per term. A term is described by the
69
+ coefficient and a tuple of integers which indicate the variable indexes of
70
+ the term. The coefficients can be any value which fits in 32-bit floating
71
+ point representation, but the dynamic range of the coefficients should be
72
+ within the limit of the hardware's sensitivity for best results. The term
73
+ index tuple length must be consistent across all terms. If a term does not
74
+ have a variable index for all positions, such as with a term which is the
75
+ square of a variable when other terms have third-order powers, then there
76
+ must be a placeholder of 0 for the unused power. The variable indexes must
77
+ be in the tuple in ascending order. Here are some examples (suppose the max
78
+ degree is 4):
79
+
80
+ - :math:`x_1^2`: :code:`(0, 0, 1, 1)`
81
+ - :math:`x_1 x_2 x_3`: :code:`(0, 1, 2, 3)`
82
+ - :math:`x_2^2 x_3^2`: :code:`(2, 2, 3, 3)`
83
+
84
+ while it does not affect the optimiztion, a constant term can be applied to
85
+ the polynomial by using an index of all zeros :code:`(0, 0, 0, 0)`. When
86
+ listing the coefficients, the position in the array must correspond to the
87
+ position in the array of indexes. Also, the indices must be ordered with
88
+ linear terms first, quadratic terms next and so forth. A polynomial operator
89
+ does not have an explicit domain. It could be evaluated on an array of any
90
+ real numbers.
91
+
92
+ Parameters
93
+ ----------
94
+
95
+ coefficients : List, np.array
96
+ Floating point values for the coefficients of a polynomial. Must
97
+ correspond to the entries in the indices array.
98
+ indices : List, np.array
99
+ List of tuples or 2d np.array with integer values describing the term
100
+ which the corresponding coefficient value is used for.
101
+
102
+ Examples
103
+ --------
104
+
105
+ >>> coefficients = [-1, -1, 2]
106
+ >>> indices = [(0, 1), (0, 2), (1, 2)]
107
+ >>> polynomial = Polynomial(coefficients, indices)
108
+ >>> test_solution = np.array([1, 1])
109
+ >>> polynomial.evaluate(test_solution)
110
+ [0]
111
+ >>> test_solution = np.array([1, 0])
112
+ >>> polynomial.evaluate(test_solution)
113
+ [-1]
114
+ >>> test_solution = np.array([5, -1])
115
+ >>> polynomial.evaluate(test_solution)
116
+ [-14]
117
+ >>> test_solution = np.array([2.5, -2.5])
118
+ >>> polynomial.evaluate(test_solution)
119
+ [-12.5]
120
+
121
+ """
122
+ coefficients : List
123
+ indices : List
124
+
125
+ def __post_init__(self):
126
+ issues = set()
127
+ degree_count = None
128
+ if len(self.coefficients)!=len(self.indices):
129
+ issues.add("coefficients and indices must be the same length")
130
+ # ensure indices are not numpy
131
+ self.indices = [[int(val) for val in index] for index in self.indices]
132
+ for i in range(len(self.coefficients)):
133
+ if degree_count is None:
134
+ degree_count = len(self.indices[i])
135
+ elif len(self.indices[i])!=degree_count:
136
+ issues.add("term rank is not consistent")
137
+ for j in range(1, degree_count):
138
+ if self.indices[i][j] < self.indices[i][j-1]:
139
+ issues.add("index order must be monotonic")
140
+ try:
141
+ coeff = float(self.coefficients[i])
142
+ except TypeError:
143
+ issues.add("coefficient data types must be coercible to float type")
144
+ except ValueError:
145
+ issues.add("coefficient values must be coercible to float values")
146
+ if len(issues) > 0:
147
+ msg = "Input data to polynomial is not correct: "
148
+ msg += "; ".join([issue for issue in issues])
149
+ raise ValueError(msg)
150
+
151
+ def pure_evaluate(self, solution : np.ndarray) -> np.ndarray:
152
+ """
153
+ Evaluation in pure python
154
+
155
+ Parameters
156
+ ----------
157
+
158
+ solution : np.array
159
+ Solution to evaluate, is optionally 2-d, which results in multiple evaluations
160
+
161
+ Returns
162
+ -------
163
+
164
+ np.array
165
+
166
+ """
167
+
168
+ if len(solution.shape) == 1:
169
+ solution = solution.reshape((1, solution.shape[0]))
170
+ objective = [0 for k in range(solution.shape[0])]
171
+ for k in range(solution.shape[0]):
172
+ for i in range(len(self.coefficients)):
173
+ term = self.coefficients[i]
174
+ for j in self.indices[i]:
175
+ if j > 0:
176
+ term *= solution[k, j-1]
177
+ objective[k] += term
178
+ return objective
179
+
180
+ def evaluate(self, solution: np.ndarray):
181
+ """
182
+ Evaluate the polynomial at the solution point. If the Cython module is available,
183
+ use that for speedup, otherwise evaluate with Python loops.
184
+
185
+ Parameters
186
+ ----------
187
+
188
+ solution : np.array
189
+ Solution to evaluate. Is optinoally 2-d, which results in multiple exaluations.
190
+
191
+ Returns
192
+ -------
193
+
194
+ 1-d array of values which match the coerced dtype of the inputs.
195
+
196
+ """
197
+ if poly_eval is None:
198
+ return self.pure_evaluate(solution)
199
+ else:
200
+ return poly_eval(np.array(self.coefficients, dtype=np.float64),
201
+ np.array(self.indices, dtype=np.int64), solution)