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.
- eqc_models-0.9.8.data/platlib/compile_extensions.py +23 -0
- eqc_models-0.9.8.data/platlib/eqc_models/__init__.py +15 -0
- eqc_models-0.9.8.data/platlib/eqc_models/algorithms/__init__.py +4 -0
- eqc_models-0.9.8.data/platlib/eqc_models/algorithms/base.py +10 -0
- eqc_models-0.9.8.data/platlib/eqc_models/algorithms/penaltymultiplier.py +169 -0
- eqc_models-0.9.8.data/platlib/eqc_models/allocation/__init__.py +6 -0
- eqc_models-0.9.8.data/platlib/eqc_models/allocation/allocation.py +367 -0
- eqc_models-0.9.8.data/platlib/eqc_models/allocation/portbase.py +128 -0
- eqc_models-0.9.8.data/platlib/eqc_models/allocation/portmomentum.py +137 -0
- eqc_models-0.9.8.data/platlib/eqc_models/assignment/__init__.py +5 -0
- eqc_models-0.9.8.data/platlib/eqc_models/assignment/qap.py +82 -0
- eqc_models-0.9.8.data/platlib/eqc_models/assignment/setpartition.py +170 -0
- eqc_models-0.9.8.data/platlib/eqc_models/base/__init__.py +72 -0
- eqc_models-0.9.8.data/platlib/eqc_models/base/base.py +150 -0
- eqc_models-0.9.8.data/platlib/eqc_models/base/constraints.py +276 -0
- eqc_models-0.9.8.data/platlib/eqc_models/base/operators.py +201 -0
- eqc_models-0.9.8.data/platlib/eqc_models/base/polyeval.c +11363 -0
- eqc_models-0.9.8.data/platlib/eqc_models/base/polyeval.cpython-310-darwin.so +0 -0
- eqc_models-0.9.8.data/platlib/eqc_models/base/polyeval.pyx +72 -0
- eqc_models-0.9.8.data/platlib/eqc_models/base/polynomial.py +274 -0
- eqc_models-0.9.8.data/platlib/eqc_models/base/quadratic.py +250 -0
- eqc_models-0.9.8.data/platlib/eqc_models/decoding.py +20 -0
- eqc_models-0.9.8.data/platlib/eqc_models/graph/__init__.py +5 -0
- eqc_models-0.9.8.data/platlib/eqc_models/graph/base.py +63 -0
- eqc_models-0.9.8.data/platlib/eqc_models/graph/hypergraph.py +307 -0
- eqc_models-0.9.8.data/platlib/eqc_models/graph/maxcut.py +155 -0
- eqc_models-0.9.8.data/platlib/eqc_models/graph/maxkcut.py +184 -0
- eqc_models-0.9.8.data/platlib/eqc_models/ml/__init__.py +15 -0
- eqc_models-0.9.8.data/platlib/eqc_models/ml/classifierbase.py +99 -0
- eqc_models-0.9.8.data/platlib/eqc_models/ml/classifierqboost.py +423 -0
- eqc_models-0.9.8.data/platlib/eqc_models/ml/classifierqsvm.py +237 -0
- eqc_models-0.9.8.data/platlib/eqc_models/ml/clustering.py +323 -0
- eqc_models-0.9.8.data/platlib/eqc_models/ml/clusteringbase.py +112 -0
- eqc_models-0.9.8.data/platlib/eqc_models/ml/decomposition.py +363 -0
- eqc_models-0.9.8.data/platlib/eqc_models/ml/forecast.py +255 -0
- eqc_models-0.9.8.data/platlib/eqc_models/ml/forecastbase.py +139 -0
- eqc_models-0.9.8.data/platlib/eqc_models/ml/regressor.py +220 -0
- eqc_models-0.9.8.data/platlib/eqc_models/ml/regressorbase.py +97 -0
- eqc_models-0.9.8.data/platlib/eqc_models/ml/reservoir.py +106 -0
- eqc_models-0.9.8.data/platlib/eqc_models/sequence/__init__.py +5 -0
- eqc_models-0.9.8.data/platlib/eqc_models/sequence/tsp.py +217 -0
- eqc_models-0.9.8.data/platlib/eqc_models/solvers/__init__.py +12 -0
- eqc_models-0.9.8.data/platlib/eqc_models/solvers/qciclient.py +707 -0
- eqc_models-0.9.8.data/platlib/eqc_models/utilities/__init__.py +6 -0
- eqc_models-0.9.8.data/platlib/eqc_models/utilities/fileio.py +38 -0
- eqc_models-0.9.8.data/platlib/eqc_models/utilities/polynomial.py +137 -0
- eqc_models-0.9.8.data/platlib/eqc_models/utilities/qplib.py +375 -0
- eqc_models-0.9.8.dist-info/LICENSE.txt +202 -0
- eqc_models-0.9.8.dist-info/METADATA +139 -0
- eqc_models-0.9.8.dist-info/RECORD +52 -0
- eqc_models-0.9.8.dist-info/WHEEL +5 -0
- 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)
|