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,72 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
cimport numpy as cnp
|
|
3
|
+
cnp.import_array()
|
|
4
|
+
fDTYPE = np.float64
|
|
5
|
+
ctypedef cnp.float64_t fDTYPE_t
|
|
6
|
+
iDTYPE = np.int64
|
|
7
|
+
ctypedef cnp.int64_t iDTYPE_t
|
|
8
|
+
|
|
9
|
+
def poly_eval(coeff, indices, solution):
|
|
10
|
+
cdef int i
|
|
11
|
+
cdef int n = 0
|
|
12
|
+
cdef cnp.ndarray values
|
|
13
|
+
# assert len(solution.shape) == 2, "Solution must be 2-d array"
|
|
14
|
+
# assert len(coeff.shape) == 1, "Coefficients must be 1-d array"
|
|
15
|
+
# assert len(indices.shape) == 2, "Inidices must be 2-d array"
|
|
16
|
+
# assert indices.shape[0] == coeff.shape[0], "Indices and coefficients must have the same first dimension"
|
|
17
|
+
if len(solution.shape) == 1:
|
|
18
|
+
n = solution.shape[0]
|
|
19
|
+
solution = solution.reshape((1, n))
|
|
20
|
+
# convert the solution to floating point instead of int
|
|
21
|
+
if solution.dtype == np.int64:
|
|
22
|
+
solution = solution.astype(np.float64)
|
|
23
|
+
values = np.zeros((solution.shape[0],))
|
|
24
|
+
# print(coeff.dtype, indices.dtype, solution.dtype)
|
|
25
|
+
for i in range(solution.shape[0]):
|
|
26
|
+
values[i] = poly_eval_c(coeff, indices, solution[i])
|
|
27
|
+
return np.squeeze(values)
|
|
28
|
+
|
|
29
|
+
cdef double poly_eval_c(cnp.ndarray[fDTYPE_t, ndim=1] coeff, cnp.ndarray[iDTYPE_t, ndim=2] indices,
|
|
30
|
+
cnp.ndarray[fDTYPE_t, ndim=1] solution):
|
|
31
|
+
# verify data types
|
|
32
|
+
# assert coeff.dtype == fDTYPE
|
|
33
|
+
# assert indices.dtype == iDTYPE
|
|
34
|
+
# check some bounds for memory safety
|
|
35
|
+
# assert indices.shape[0] == coeff.shape[0]
|
|
36
|
+
if np.max(indices) > solution.shape[0]:
|
|
37
|
+
raise ValueError("indices describe different size solution than provided")
|
|
38
|
+
elif np.min(indices) < 0:
|
|
39
|
+
raise ValueError("indices includes negative values, which must all be positive")
|
|
40
|
+
|
|
41
|
+
# streamline this code
|
|
42
|
+
# objective = 0
|
|
43
|
+
# for i in range(len(self.coefficients)):
|
|
44
|
+
# term = self.coefficients[i]
|
|
45
|
+
# for j in self.indices[i]:
|
|
46
|
+
# if j > 0:
|
|
47
|
+
# term *= solution[j-1]
|
|
48
|
+
# objective += term
|
|
49
|
+
# return objective
|
|
50
|
+
#
|
|
51
|
+
|
|
52
|
+
cdef int coeff_count = coeff.shape[0]
|
|
53
|
+
cdef int degree_count = indices.shape[1]
|
|
54
|
+
cdef double term = 0
|
|
55
|
+
cdef double ttl = 0
|
|
56
|
+
cdef int i = 0
|
|
57
|
+
cdef int j = 0
|
|
58
|
+
cdef double value = 0
|
|
59
|
+
|
|
60
|
+
# initialize terms, compute
|
|
61
|
+
for i in range(coeff_count):
|
|
62
|
+
term = coeff[i]
|
|
63
|
+
for j in range(degree_count):
|
|
64
|
+
if indices[i, j] > 0:
|
|
65
|
+
term *= solution[indices[i, j] - 1]
|
|
66
|
+
ttl += term
|
|
67
|
+
|
|
68
|
+
# collapse terms into first index and return
|
|
69
|
+
# for i in range(1, coeff_count):
|
|
70
|
+
# terms[0] += terms[i]
|
|
71
|
+
|
|
72
|
+
return ttl # terms[0]
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
# (C) Quantum Computing Inc., 2024.
|
|
2
|
+
from typing import Tuple, Union, List
|
|
3
|
+
import numpy as np
|
|
4
|
+
from eqc_models.base.base import EqcModel
|
|
5
|
+
from eqc_models.base.operators import Polynomial
|
|
6
|
+
from eqc_models.base.constraints import ConstraintsMixIn
|
|
7
|
+
|
|
8
|
+
class PolynomialMixin:
|
|
9
|
+
"""This class provides an instance method and property that
|
|
10
|
+
manage polynomial models.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
@property
|
|
14
|
+
def H(self) -> Tuple[np.ndarray, np.ndarray]:
|
|
15
|
+
"""
|
|
16
|
+
Hamiltonian specified as a polynomial : coefficients, indices
|
|
17
|
+
|
|
18
|
+
indices are of the format [0, idx-1, ..., idx-d] which must be non-decreasing
|
|
19
|
+
and each idx-j is a 1-based index of the variable which is a power in the
|
|
20
|
+
term. For a polynomial where the highest degree is 3 and specifying a term
|
|
21
|
+
such as x_1x_2, the index array is [0, 1, 2]. Another example, x_1^2x_2 is
|
|
22
|
+
[1, 1, 2].
|
|
23
|
+
|
|
24
|
+
"""
|
|
25
|
+
return self.coefficients, self.indices
|
|
26
|
+
|
|
27
|
+
@H.setter
|
|
28
|
+
def H(self, value : Tuple[np.ndarray, np.ndarray]):
|
|
29
|
+
""" Set H directly as coefficients, indices """
|
|
30
|
+
|
|
31
|
+
coefficients, indices = value
|
|
32
|
+
self.coefficients = coefficients
|
|
33
|
+
self.indices = indices
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def sparse(self) -> Tuple[np.ndarray, np.ndarray]:
|
|
37
|
+
return self.H
|
|
38
|
+
|
|
39
|
+
def evaluate(self, solution : np.ndarray) -> float:
|
|
40
|
+
"""
|
|
41
|
+
Evaluate polynomial at solution
|
|
42
|
+
|
|
43
|
+
:solution: 1-d numpy array with the same length as the number of variables
|
|
44
|
+
|
|
45
|
+
returns a floating point value
|
|
46
|
+
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
value = self.polynomial.evaluate(np.array(solution))
|
|
50
|
+
|
|
51
|
+
return value
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def dynamic_range(self) -> float:
|
|
55
|
+
"""
|
|
56
|
+
Dynamic range is a measure in decibels of the ratio of the largest
|
|
57
|
+
magnitude coefficient in a problem to the smallest non-zero magnitude
|
|
58
|
+
coefficient.
|
|
59
|
+
|
|
60
|
+
The possible range of values are all greater than or equal to 0. The
|
|
61
|
+
calculation is performed by finding the lowest non-zero of the
|
|
62
|
+
absolute value of all the coefficients, which could be empty. In that
|
|
63
|
+
case, the dynamic range is undefined, so an exception is raised. If
|
|
64
|
+
it is positive, then the maximum of the absolute values is divided
|
|
65
|
+
by the lowest. The base-10 logarithm of that value is taken and mul-
|
|
66
|
+
tiplied by 10. This is the dynamic range.
|
|
67
|
+
|
|
68
|
+
Returns
|
|
69
|
+
----------
|
|
70
|
+
|
|
71
|
+
float
|
|
72
|
+
|
|
73
|
+
"""
|
|
74
|
+
H = self.H
|
|
75
|
+
coefficients = np.array(H[0])
|
|
76
|
+
try:
|
|
77
|
+
lowest = np.min(np.abs(coefficients[coefficients!=0]))
|
|
78
|
+
except IndexError:
|
|
79
|
+
raise ValueError("Dynamic range of a Hamiltonian of all 0 is undefined")
|
|
80
|
+
highest = np.max(np.abs(coefficients))
|
|
81
|
+
return 10*np.log10(highest / lowest)
|
|
82
|
+
|
|
83
|
+
class PolynomialModel(PolynomialMixin, EqcModel):
|
|
84
|
+
"""
|
|
85
|
+
Polynomial model base class.
|
|
86
|
+
|
|
87
|
+
Parameters
|
|
88
|
+
------------
|
|
89
|
+
coefficients: An array of polynomial coeffients.
|
|
90
|
+
indices: An array of polynomial indices.
|
|
91
|
+
|
|
92
|
+
Examples
|
|
93
|
+
------------
|
|
94
|
+
|
|
95
|
+
>>> coeffs = np.array([1, 2, 3])
|
|
96
|
+
>>> indices = np.array([[0, 0, 1], [0, 1, 1], [1, 1, 1]])
|
|
97
|
+
>>> from eqc_models.base.polynomial import PolynomialModel
|
|
98
|
+
>>> polynomial = PolynomialModel(coeffs, indices)
|
|
99
|
+
>>> solution = np.array([1, 1, 1])
|
|
100
|
+
>>> value = polynomial.evaluate(solution)
|
|
101
|
+
>>> int(value)
|
|
102
|
+
6
|
|
103
|
+
>>> polynomial.H # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
|
|
104
|
+
(array([1, 2, 3]), array([[0, 0, 1],
|
|
105
|
+
[0, 1, 1],
|
|
106
|
+
[1, 1, 1]]))
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
def __init__(self, coefficients : Union[List, np.ndarray], indices : Union[List, np.ndarray]) -> None:
|
|
110
|
+
self.coefficients = coefficients
|
|
111
|
+
self.indices = indices
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def polynomial(self) -> Polynomial:
|
|
115
|
+
coefficients, indices = self.H
|
|
116
|
+
return Polynomial(coefficients=coefficients, indices=indices)
|
|
117
|
+
|
|
118
|
+
class ConstrainedPolynomialModel(ConstraintsMixIn, PolynomialModel):
|
|
119
|
+
"""
|
|
120
|
+
Constrained Polynomial model base class.
|
|
121
|
+
|
|
122
|
+
Parameters
|
|
123
|
+
------------
|
|
124
|
+
coefficients: An array of polynomial coeffients.
|
|
125
|
+
indices: An array of polynomial indices.
|
|
126
|
+
lhs: Left hand side of the linear constraints.
|
|
127
|
+
rhs: Right hand side of the linear constraints.
|
|
128
|
+
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
def __init__(self, coefficients : Union[List, np.ndarray], indices : Union[List, np.ndarray],
|
|
132
|
+
lhs : np.ndarray, rhs: np.ndarray):
|
|
133
|
+
self.coefficients = np.array(coefficients)
|
|
134
|
+
self.indices = np.array(indices).astype(np.int64)
|
|
135
|
+
self.max_order = self.indices.shape[1]
|
|
136
|
+
self.lhs = lhs
|
|
137
|
+
self.rhs = rhs
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def penalties(self):
|
|
141
|
+
"""
|
|
142
|
+
Penalty terms specified as a polynomial: coefficients, indices
|
|
143
|
+
|
|
144
|
+
indices are of the format [0, idx-1, ..., idx-d] which must be non-decreasing
|
|
145
|
+
and each idx-j is a 1-based index of the variable which is a power in the
|
|
146
|
+
term. For a polynomial where the highest degree is 3 and specifying a term
|
|
147
|
+
such as x_1x_2, the index array is [0, 1, 2]. Another example, x_1^2x_2 is
|
|
148
|
+
[1, 1, 2].
|
|
149
|
+
|
|
150
|
+
Only linear equality constraints are supported. Translate Ax=b into
|
|
151
|
+
penalties using the superclass.
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
indices = []
|
|
155
|
+
coefficients = []
|
|
156
|
+
def lpad(index):
|
|
157
|
+
missing = self.max_order - len(index)
|
|
158
|
+
if missing > 0:
|
|
159
|
+
index = (0,) * missing + index
|
|
160
|
+
assert len(index) > 0
|
|
161
|
+
return np.array(index)
|
|
162
|
+
Pl, Pq = super(ConstrainedPolynomialModel, self).penalties
|
|
163
|
+
for i in range(Pl.shape[0]):
|
|
164
|
+
if Pl[i] != 0:
|
|
165
|
+
indices.append(lpad((0, i+1)))
|
|
166
|
+
coefficients.append(Pl[i])
|
|
167
|
+
for j in range(i, Pq.shape[1]):
|
|
168
|
+
if Pq[i, j] != 0:
|
|
169
|
+
indices.append(lpad((i+1, j+1)))
|
|
170
|
+
value = Pq[i, j]
|
|
171
|
+
if i!=j:
|
|
172
|
+
value += Pq[j, i]
|
|
173
|
+
coefficients.append(value)
|
|
174
|
+
return coefficients, indices
|
|
175
|
+
|
|
176
|
+
def evaluatePenalties(self, solution : np.ndarray, include_offset=False) -> float:
|
|
177
|
+
"""
|
|
178
|
+
Take the polynomial form of the penalties from the penalties property
|
|
179
|
+
and evaluate the solution. The offset can be included by passing a True
|
|
180
|
+
value to the `include_offset` keyword argument.
|
|
181
|
+
|
|
182
|
+
Parameters
|
|
183
|
+
-----------
|
|
184
|
+
|
|
185
|
+
solution : np.ndarray
|
|
186
|
+
Solution to evaluate for a penalty value
|
|
187
|
+
include_offset : bool
|
|
188
|
+
Optional argument indicating whether or not to include the offset value.
|
|
189
|
+
|
|
190
|
+
Returns
|
|
191
|
+
---------
|
|
192
|
+
|
|
193
|
+
Penalty value : float
|
|
194
|
+
|
|
195
|
+
Examples
|
|
196
|
+
---------
|
|
197
|
+
|
|
198
|
+
>>> coeff = np.array([-1.0, -1.0])
|
|
199
|
+
>>> indices = np.array([(0, 1), (0, 2)])
|
|
200
|
+
>>> lhs = np.array([[1.0, 1.0]])
|
|
201
|
+
>>> rhs = np.array([1.0])
|
|
202
|
+
>>> model = ConstrainedPolynomialModel(coeff, indices, lhs, rhs)
|
|
203
|
+
>>> sol = np.array([1.0, 1.0])
|
|
204
|
+
>>> lhs@sol - rhs
|
|
205
|
+
array([1.])
|
|
206
|
+
>>> model.evaluatePenalties(sol)+model.offset
|
|
207
|
+
1.0
|
|
208
|
+
>>> model.evaluatePenalties(sol)
|
|
209
|
+
0.0
|
|
210
|
+
>>> model.evaluatePenalties(sol, include_offset=True)
|
|
211
|
+
1.0
|
|
212
|
+
|
|
213
|
+
"""
|
|
214
|
+
|
|
215
|
+
# get the coefficients and indices for the penalty polynomial
|
|
216
|
+
# use the Polynomial operator to evaluate the solution
|
|
217
|
+
coefficients, indices = self.penalties
|
|
218
|
+
solution = np.array(solution, dtype=np.float64)
|
|
219
|
+
polynomial = Polynomial(coefficients, indices)
|
|
220
|
+
if include_offset:
|
|
221
|
+
value = self.offset
|
|
222
|
+
else:
|
|
223
|
+
value = 0
|
|
224
|
+
value += polynomial.evaluate(solution)
|
|
225
|
+
return value
|
|
226
|
+
|
|
227
|
+
def evaluateObjective(self, solution : np.ndarray) -> float:
|
|
228
|
+
"""
|
|
229
|
+
Take the polynomial coeff and indices from constructor and evalute the
|
|
230
|
+
solution with it.
|
|
231
|
+
|
|
232
|
+
Parameters
|
|
233
|
+
-----------
|
|
234
|
+
|
|
235
|
+
solution : np.ndarray
|
|
236
|
+
Soluttion to evaluate the objective value
|
|
237
|
+
|
|
238
|
+
Returns
|
|
239
|
+
--------
|
|
240
|
+
|
|
241
|
+
objective value : float
|
|
242
|
+
|
|
243
|
+
"""
|
|
244
|
+
coefficients = self.coefficients
|
|
245
|
+
indices = self.indices
|
|
246
|
+
solution = np.array(solution, dtype=np.float64)
|
|
247
|
+
polynomial = Polynomial(coefficients, indices)
|
|
248
|
+
return polynomial.evaluate(solution)
|
|
249
|
+
|
|
250
|
+
@property
|
|
251
|
+
def H(self) -> Tuple[np.ndarray, np.ndarray]:
|
|
252
|
+
""" Provide the sparse format for the Hamiltonian """
|
|
253
|
+
|
|
254
|
+
p_coeff, p_indices = self.penalties
|
|
255
|
+
coefficients, indices = self.coefficients, self.indices
|
|
256
|
+
terms = {}
|
|
257
|
+
alpha = self.alpha
|
|
258
|
+
for index, coeff in zip(p_indices, p_coeff):
|
|
259
|
+
index = tuple(index)
|
|
260
|
+
assert len(index) > 1
|
|
261
|
+
if index not in terms:
|
|
262
|
+
terms[index] = alpha * coeff
|
|
263
|
+
else:
|
|
264
|
+
terms[index] += alpha * coeff
|
|
265
|
+
for index, coeff in zip(indices, coefficients):
|
|
266
|
+
index = tuple(index)
|
|
267
|
+
if index not in terms:
|
|
268
|
+
terms[index] = coeff
|
|
269
|
+
else:
|
|
270
|
+
terms[index] += coeff
|
|
271
|
+
indices = [index for index in terms.keys()]
|
|
272
|
+
indices.sort()
|
|
273
|
+
coefficients = [terms[tuple(index)] for index in indices]
|
|
274
|
+
return coefficients, indices
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# (C) Quantum Computing Inc., 2024.
|
|
2
|
+
from typing import Tuple
|
|
3
|
+
import warnings
|
|
4
|
+
import numpy as np
|
|
5
|
+
from eqc_models.base.base import EqcModel
|
|
6
|
+
from eqc_models.base.constraints import ConstraintsMixIn
|
|
7
|
+
from eqc_models.base.operators import QUBO, Polynomial
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class QuadraticMixIn:
|
|
11
|
+
"""
|
|
12
|
+
This class provides an instance method and property that
|
|
13
|
+
manage quadratic models.
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def sparse(self) -> Tuple[np.ndarray, np.ndarray]:
|
|
19
|
+
"""
|
|
20
|
+
Put the linear and quadratic terms in a sparse (poly) format
|
|
21
|
+
|
|
22
|
+
Returns
|
|
23
|
+
----------
|
|
24
|
+
|
|
25
|
+
coefficients: List
|
|
26
|
+
indices: List
|
|
27
|
+
|
|
28
|
+
"""
|
|
29
|
+
C, J = self.H
|
|
30
|
+
C = np.squeeze(C)
|
|
31
|
+
n = self.n
|
|
32
|
+
indices = []
|
|
33
|
+
coefficients = []
|
|
34
|
+
# build a key (ordered tuple of indices) of length 2 for each element
|
|
35
|
+
for i in range(n):
|
|
36
|
+
if C[i] != 0:
|
|
37
|
+
key = (0, i+1)
|
|
38
|
+
indices.append(key)
|
|
39
|
+
coefficients.append(C[i])
|
|
40
|
+
# make J upper triangular
|
|
41
|
+
J = np.triu(J) + np.tril(J, -1).T
|
|
42
|
+
for i in range(n):
|
|
43
|
+
for j in range(i, n):
|
|
44
|
+
val = J[i, j]
|
|
45
|
+
if val != 0:
|
|
46
|
+
key = (i+1, j+1)
|
|
47
|
+
indices.append(key)
|
|
48
|
+
coefficients.append(val)
|
|
49
|
+
|
|
50
|
+
return np.array(coefficients, dtype=np.float32), np.array(indices, dtype=np.int32)
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def polynomial(self) -> Polynomial:
|
|
54
|
+
coefficients, indices = self.sparse
|
|
55
|
+
return Polynomial(coefficients=coefficients, indices=indices)
|
|
56
|
+
|
|
57
|
+
def evaluate(self, solution: np.ndarray) -> float:
|
|
58
|
+
"""
|
|
59
|
+
Evaluate the solution using the original operator.
|
|
60
|
+
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
sol = np.array(solution)
|
|
64
|
+
C, J = self.H
|
|
65
|
+
return np.squeeze(sol.T @ J @ sol + C.T @ sol)
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def dynamic_range(self) -> float:
|
|
69
|
+
"""
|
|
70
|
+
Dynamic range is a measure in decibels of the ratio of the largest
|
|
71
|
+
magnitude coefficient in a problem to the smallest non-zero magnitude
|
|
72
|
+
coefficient.
|
|
73
|
+
|
|
74
|
+
The possible range of values are all greater than or equal to 0. The
|
|
75
|
+
calculation is performed by finding the lowest non-zero of the
|
|
76
|
+
absolute value of all the coefficients, which could be empty. The
|
|
77
|
+
values are in two arrays, so the minimum and maximum values of these
|
|
78
|
+
arrays are compared to each other. When there is no non-zero in either
|
|
79
|
+
of the arrays, then an exception is raised indicating that the dynamic
|
|
80
|
+
range of an operator of all zeros is undefined. If the lowest value is
|
|
81
|
+
positive, then the maximum of the absolute values is divided by the
|
|
82
|
+
lowest. The base-10 logarithm of that value is taken and multiplied by
|
|
83
|
+
10. This is the dynamic range.
|
|
84
|
+
|
|
85
|
+
Returns
|
|
86
|
+
----------
|
|
87
|
+
|
|
88
|
+
float
|
|
89
|
+
|
|
90
|
+
"""
|
|
91
|
+
C, J = self.H
|
|
92
|
+
# if either C or J are all 0, then set min to very large value
|
|
93
|
+
try:
|
|
94
|
+
min_c = np.min(np.abs(C[C!=0]))
|
|
95
|
+
except IndexError:
|
|
96
|
+
min_c = 1e308
|
|
97
|
+
try:
|
|
98
|
+
min_j = np.min(np.abs(J[J!=0]))
|
|
99
|
+
except IndexError:
|
|
100
|
+
min_j = 1e308
|
|
101
|
+
max_c = np.max(np.abs(C))
|
|
102
|
+
max_j = np.max(np.abs(J))
|
|
103
|
+
lowest = min_c if min_c < min_j else min_j
|
|
104
|
+
highest = max_c if max_c > max_j else max_j
|
|
105
|
+
if lowest > highest:
|
|
106
|
+
raise ValueError("Dynamic range of a Hamiltonian of all 0 is undefined")
|
|
107
|
+
return 10*np.log10(highest / lowest)
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def qubo(self) -> QUBO:
|
|
111
|
+
"""
|
|
112
|
+
Transform the model into QUBO form. Use `upper_bound` to determine
|
|
113
|
+
a log-encoding of the variables.
|
|
114
|
+
|
|
115
|
+
"""
|
|
116
|
+
bin_n = 0
|
|
117
|
+
bits = []
|
|
118
|
+
#
|
|
119
|
+
C, J = self.H
|
|
120
|
+
# upper_bound is an array of the maximum values each variable can take
|
|
121
|
+
upper_bound = self.upper_bound
|
|
122
|
+
if np.sum(upper_bound)!=upper_bound.shape[0]:
|
|
123
|
+
for i in range(upper_bound.shape[0]):
|
|
124
|
+
bits.append(1+np.floor(np.log2(upper_bound[i])))
|
|
125
|
+
bin_n += bits[-1]
|
|
126
|
+
bin_n = int(bin_n)
|
|
127
|
+
Q = np.zeros((bin_n, bin_n), dtype=np.float32)
|
|
128
|
+
Q.shape
|
|
129
|
+
powers = [2**np.arange(bit_count) for bit_count in bits]
|
|
130
|
+
blocks = []
|
|
131
|
+
linear_blocks = []
|
|
132
|
+
for i in range(len(powers)):
|
|
133
|
+
# add the linear terms to the diagonal
|
|
134
|
+
linear_blocks.append(C[i]*powers[i])
|
|
135
|
+
row = []
|
|
136
|
+
for j in range(len(powers)):
|
|
137
|
+
mult = J[i,j]
|
|
138
|
+
block = np.outer(powers[i], powers[j])
|
|
139
|
+
block *= mult
|
|
140
|
+
row.append(block)
|
|
141
|
+
blocks.append(row)
|
|
142
|
+
Q[:, :] = np.block(blocks)
|
|
143
|
+
linear_operator = np.hstack(linear_blocks)
|
|
144
|
+
Q += np.diag(linear_operator)
|
|
145
|
+
else:
|
|
146
|
+
# in this case, the fomulation already has only binary variables
|
|
147
|
+
Q = np.zeros_like(J)
|
|
148
|
+
Q[:, :] = J
|
|
149
|
+
Q += np.diag(np.squeeze(C))
|
|
150
|
+
|
|
151
|
+
return QUBO(Q)
|
|
152
|
+
|
|
153
|
+
class QuadraticModel(QuadraticMixIn, EqcModel):
|
|
154
|
+
"""
|
|
155
|
+
Provides a quadratic operator and device sum constraint support.
|
|
156
|
+
|
|
157
|
+
Parameters
|
|
158
|
+
-----------
|
|
159
|
+
|
|
160
|
+
J: Quadratic hamiltonian array.
|
|
161
|
+
C: Linear hamiltonian array.
|
|
162
|
+
|
|
163
|
+
Examples
|
|
164
|
+
---------
|
|
165
|
+
|
|
166
|
+
>>> C = np.array([1, 2])
|
|
167
|
+
>>> J = np.array([[2, 1], [1, 2]])
|
|
168
|
+
>>> from eqc_models.base.quadratic import QuadraticModel
|
|
169
|
+
>>> model = QuadraticModel(C, J)
|
|
170
|
+
>>> model.upper_bound = np.array([1, 1])
|
|
171
|
+
>>> qubo = model.qubo
|
|
172
|
+
>>> qubo.Q # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
|
|
173
|
+
array([[3., 1.],
|
|
174
|
+
[1., 4.]])
|
|
175
|
+
|
|
176
|
+
"""
|
|
177
|
+
|
|
178
|
+
def __init__(self, C: np.ndarray, J: np.ndarray):
|
|
179
|
+
self._H = C, J
|
|
180
|
+
|
|
181
|
+
class ConstrainedQuadraticModel(ConstraintsMixIn, QuadraticModel):
|
|
182
|
+
"""
|
|
183
|
+
Provides a constrained quadratic operator and device sum constraint support.
|
|
184
|
+
|
|
185
|
+
Parameters
|
|
186
|
+
-----------
|
|
187
|
+
|
|
188
|
+
J: Quadratic hamiltonian array.
|
|
189
|
+
C: Linear hamiltonian array.
|
|
190
|
+
lhs: Left hand side of the linear constraints.
|
|
191
|
+
rhs: Right hand side of the linear constraints.
|
|
192
|
+
|
|
193
|
+
Examples
|
|
194
|
+
-------------------
|
|
195
|
+
|
|
196
|
+
>>> C = np.array([1, 2])
|
|
197
|
+
>>> J = np.array([[2, 1], [1, 2]])
|
|
198
|
+
>>> lhs = np.array([[1, 1], [1, 1]])
|
|
199
|
+
>>> rhs = np.array([1, 1])
|
|
200
|
+
>>> from eqc_models.base.quadratic import ConstrainedQuadraticModel
|
|
201
|
+
>>> model = ConstrainedQuadraticModel(C, J, lhs, rhs)
|
|
202
|
+
>>> model.penalty_multiplier = 1
|
|
203
|
+
>>> model.upper_bound = np.array([1, 1])
|
|
204
|
+
>>> qubo = model.qubo
|
|
205
|
+
>>> qubo.Q # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
|
|
206
|
+
array([[1., 3.],
|
|
207
|
+
[3., 2.]])
|
|
208
|
+
|
|
209
|
+
"""
|
|
210
|
+
|
|
211
|
+
def __init__(self, C_obj: np.array, J_obj: np.array, lhs: np.array, rhs: np.array):
|
|
212
|
+
self.quad_objective = J_obj
|
|
213
|
+
self.linear_objective = C_obj
|
|
214
|
+
self.lhs = lhs
|
|
215
|
+
self.rhs = rhs
|
|
216
|
+
self._penalty_multiplier = None
|
|
217
|
+
|
|
218
|
+
@property
|
|
219
|
+
def H(self) -> Tuple[np.ndarray, np.ndarray]:
|
|
220
|
+
"""
|
|
221
|
+
Return a pair of arrays, the linear and quadratic portions of a quadratic
|
|
222
|
+
operator that has the objective plus penalty functions multiplied by the
|
|
223
|
+
penalty multiplier. The linear terms are the first array and the quadratic
|
|
224
|
+
are the second.
|
|
225
|
+
|
|
226
|
+
Returns
|
|
227
|
+
-----------
|
|
228
|
+
|
|
229
|
+
`np.ndarray, np.nedarray`
|
|
230
|
+
|
|
231
|
+
"""
|
|
232
|
+
|
|
233
|
+
C = self.linear_objective
|
|
234
|
+
J = self.quad_objective
|
|
235
|
+
pC, pJ = self.penalties
|
|
236
|
+
alpha = self.penalty_multiplier
|
|
237
|
+
return C + alpha * pC, J + alpha * pJ
|
|
238
|
+
|
|
239
|
+
@ConstraintsMixIn.penalty_multiplier.setter
|
|
240
|
+
def penalty_multiplier(self, value):
|
|
241
|
+
ConstraintsMixIn.penalty_multiplier.fset(self, value)
|
|
242
|
+
|
|
243
|
+
@property
|
|
244
|
+
def constraints(self):
|
|
245
|
+
return self.lhs, self.rhs
|
|
246
|
+
|
|
247
|
+
def evaluateObjective(self, solution: np.ndarray) -> float:
|
|
248
|
+
J = self.quad_objective
|
|
249
|
+
C = self.linear_objective
|
|
250
|
+
return np.squeeze(C.T @ solution + solution.T@J@solution)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# (C) Quantum Computing Inc., 2024.
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
# setting EPSILON to the most precise value Dirac devices can currently achieve
|
|
5
|
+
EPSILON = 0.0001
|
|
6
|
+
|
|
7
|
+
class BisectionMixin:
|
|
8
|
+
|
|
9
|
+
def decode(self, solution):
|
|
10
|
+
""" Use a bisection method to determine the a binary solution from fractional values """
|
|
11
|
+
model = self.model
|
|
12
|
+
|
|
13
|
+
lower = np.min(solution)
|
|
14
|
+
upper = np.max(solution)
|
|
15
|
+
|
|
16
|
+
while upper - lower > EPSILON:
|
|
17
|
+
middle = (upper + lower) / 2
|
|
18
|
+
test_solution = (np.where(solution>=middle).astype(np.int32))
|
|
19
|
+
# test feasibility
|
|
20
|
+
# if model.is_feasible(test_solution):
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# (C) Quantum Computing Inc., 2024.
|
|
2
|
+
from typing import List, Set
|
|
3
|
+
import networkx as nx
|
|
4
|
+
from ..base import QuadraticModel
|
|
5
|
+
|
|
6
|
+
class GraphModel(QuadraticModel):
|
|
7
|
+
""" """
|
|
8
|
+
def __init__(self, G : nx.Graph):
|
|
9
|
+
self.G = G
|
|
10
|
+
self.linear_objective, self.quad_objective = self.costFunction()
|
|
11
|
+
|
|
12
|
+
class NodeModel(GraphModel):
|
|
13
|
+
"""
|
|
14
|
+
Base class for a model where the decision variables correspond to
|
|
15
|
+
the graph nodes.
|
|
16
|
+
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def variables(self) -> List[str]:
|
|
21
|
+
""" Provide a variable name to index lookup; order enforced by sorting the list before returning """
|
|
22
|
+
names = [node for node in self.G.nodes]
|
|
23
|
+
names.sort()
|
|
24
|
+
return names
|
|
25
|
+
|
|
26
|
+
def costFunction(self):
|
|
27
|
+
"""
|
|
28
|
+
Parameters
|
|
29
|
+
-------------
|
|
30
|
+
|
|
31
|
+
None
|
|
32
|
+
|
|
33
|
+
Returns
|
|
34
|
+
--------------
|
|
35
|
+
|
|
36
|
+
:C: linear operator (vector array of coefficients) for cost function
|
|
37
|
+
:J: quadratic operator (N by N matrix array of coefficients ) for cost function
|
|
38
|
+
|
|
39
|
+
"""
|
|
40
|
+
raise NotImplementedError("NodeModel does not implement costFunction")
|
|
41
|
+
|
|
42
|
+
def modularity(self, partition : Set[Set]) -> float:
|
|
43
|
+
""" Calculate modularity from a partition (set of communities) """
|
|
44
|
+
|
|
45
|
+
return nx.community.modularity(self.G, partition)
|
|
46
|
+
|
|
47
|
+
class TwoPartitionModel(NodeModel):
|
|
48
|
+
"""
|
|
49
|
+
Base class for a generic graph paritioning model. Override the
|
|
50
|
+
cost function and evaluation methods to implement a two-partition
|
|
51
|
+
algorithm.
|
|
52
|
+
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
class EdgeModel(GraphModel):
|
|
56
|
+
""" Create a model where the variables are edge-based """
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def variables(self) -> List[str]:
|
|
60
|
+
""" Provide a variable name to index lookup; order enforced by sorting the list before returning """
|
|
61
|
+
names = [f"({u},{v})" for u, v in self.G.edges]
|
|
62
|
+
names.sort()
|
|
63
|
+
return names
|