eqc-models 0.11.1__py3-none-any.whl → 0.13.0__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.11.1.data → eqc_models-0.13.0.data}/platlib/eqc_models/base/base.py +11 -0
- eqc_models-0.13.0.data/platlib/eqc_models/base/binaries.py +33 -0
- {eqc_models-0.11.1.data → eqc_models-0.13.0.data}/platlib/eqc_models/base/constraints.py +39 -0
- {eqc_models-0.11.1.data → eqc_models-0.13.0.data}/platlib/eqc_models/base/polyeval.c +4375 -3426
- eqc_models-0.13.0.data/platlib/eqc_models/base/polyeval.cpython-310-darwin.so +0 -0
- {eqc_models-0.11.1.data → eqc_models-0.13.0.data}/platlib/eqc_models/base/polynomial.py +21 -2
- {eqc_models-0.11.1.data → eqc_models-0.13.0.data}/platlib/eqc_models/base/quadratic.py +2 -2
- {eqc_models-0.11.1.data → eqc_models-0.13.0.data}/platlib/eqc_models/base/results.py +81 -1
- eqc_models-0.13.0.data/platlib/eqc_models/graph/__init__.py +11 -0
- {eqc_models-0.11.1.data → eqc_models-0.13.0.data}/platlib/eqc_models/graph/base.py +8 -4
- eqc_models-0.13.0.data/platlib/eqc_models/graph/rcshortestpath.py +81 -0
- eqc_models-0.13.0.data/platlib/eqc_models/graph/shortestpath.py +190 -0
- {eqc_models-0.11.1.data → eqc_models-0.13.0.data}/platlib/eqc_models/ml/classifierbase.py +30 -5
- {eqc_models-0.11.1.data → eqc_models-0.13.0.data}/platlib/eqc_models/ml/classifierqboost.py +14 -1
- {eqc_models-0.11.1.data → eqc_models-0.13.0.data}/platlib/eqc_models/ml/classifierqsvm.py +28 -0
- {eqc_models-0.11.1.data → eqc_models-0.13.0.data}/platlib/eqc_models/ml/clustering.py +5 -5
- {eqc_models-0.11.1.data → eqc_models-0.13.0.data}/platlib/eqc_models/ml/clusteringbase.py +1 -2
- {eqc_models-0.11.1.data → eqc_models-0.13.0.data}/platlib/eqc_models/ml/decomposition.py +0 -1
- {eqc_models-0.11.1.data → eqc_models-0.13.0.data}/platlib/eqc_models/ml/regressorbase.py +0 -1
- {eqc_models-0.11.1.data → eqc_models-0.13.0.data}/platlib/eqc_models/process/base.py +6 -1
- {eqc_models-0.11.1.data → eqc_models-0.13.0.data}/platlib/eqc_models/process/mpc.py +1 -1
- {eqc_models-0.11.1.data → eqc_models-0.13.0.data}/platlib/eqc_models/solvers/__init__.py +10 -6
- eqc_models-0.13.0.data/platlib/eqc_models/solvers/eqcdirect.py +76 -0
- eqc_models-0.13.0.data/platlib/eqc_models/solvers/mip.py +115 -0
- {eqc_models-0.11.1.data → eqc_models-0.13.0.data}/platlib/eqc_models/solvers/qciclient.py +11 -22
- eqc_models-0.13.0.data/platlib/eqc_models/solvers/responselog.py +47 -0
- {eqc_models-0.11.1.dist-info → eqc_models-0.13.0.dist-info}/METADATA +7 -9
- eqc_models-0.13.0.dist-info/RECORD +69 -0
- {eqc_models-0.11.1.dist-info → eqc_models-0.13.0.dist-info}/WHEEL +1 -1
- eqc_models-0.11.1.data/platlib/eqc_models/base/polyeval.cpython-310-darwin.so +0 -0
- eqc_models-0.11.1.data/platlib/eqc_models/graph/__init__.py +0 -6
- eqc_models-0.11.1.dist-info/RECORD +0 -63
- {eqc_models-0.11.1.data → eqc_models-0.13.0.data}/platlib/compile_extensions.py +0 -0
- {eqc_models-0.11.1.data → eqc_models-0.13.0.data}/platlib/eqc_models/__init__.py +0 -0
- {eqc_models-0.11.1.data → eqc_models-0.13.0.data}/platlib/eqc_models/algorithms/__init__.py +0 -0
- {eqc_models-0.11.1.data → eqc_models-0.13.0.data}/platlib/eqc_models/algorithms/base.py +0 -0
- {eqc_models-0.11.1.data → eqc_models-0.13.0.data}/platlib/eqc_models/algorithms/penaltymultiplier.py +0 -0
- {eqc_models-0.11.1.data → eqc_models-0.13.0.data}/platlib/eqc_models/allocation/__init__.py +0 -0
- {eqc_models-0.11.1.data → eqc_models-0.13.0.data}/platlib/eqc_models/allocation/allocation.py +0 -0
- {eqc_models-0.11.1.data → eqc_models-0.13.0.data}/platlib/eqc_models/allocation/portbase.py +0 -0
- {eqc_models-0.11.1.data → eqc_models-0.13.0.data}/platlib/eqc_models/allocation/portmomentum.py +0 -0
- {eqc_models-0.11.1.data → eqc_models-0.13.0.data}/platlib/eqc_models/assignment/__init__.py +0 -0
- {eqc_models-0.11.1.data → eqc_models-0.13.0.data}/platlib/eqc_models/assignment/qap.py +0 -0
- {eqc_models-0.11.1.data → eqc_models-0.13.0.data}/platlib/eqc_models/assignment/resource.py +0 -0
- {eqc_models-0.11.1.data → eqc_models-0.13.0.data}/platlib/eqc_models/assignment/setpartition.py +0 -0
- {eqc_models-0.11.1.data → eqc_models-0.13.0.data}/platlib/eqc_models/base/__init__.py +0 -0
- {eqc_models-0.11.1.data → eqc_models-0.13.0.data}/platlib/eqc_models/base/operators.py +0 -0
- {eqc_models-0.11.1.data → eqc_models-0.13.0.data}/platlib/eqc_models/base/polyeval.pyx +0 -0
- {eqc_models-0.11.1.data → eqc_models-0.13.0.data}/platlib/eqc_models/combinatorics/__init__.py +0 -0
- {eqc_models-0.11.1.data → eqc_models-0.13.0.data}/platlib/eqc_models/combinatorics/setcover.py +0 -0
- {eqc_models-0.11.1.data → eqc_models-0.13.0.data}/platlib/eqc_models/combinatorics/setpartition.py +0 -0
- {eqc_models-0.11.1.data → eqc_models-0.13.0.data}/platlib/eqc_models/decoding.py +0 -0
- {eqc_models-0.11.1.data → eqc_models-0.13.0.data}/platlib/eqc_models/graph/hypergraph.py +0 -0
- {eqc_models-0.11.1.data → eqc_models-0.13.0.data}/platlib/eqc_models/graph/maxcut.py +0 -0
- {eqc_models-0.11.1.data → eqc_models-0.13.0.data}/platlib/eqc_models/graph/maxkcut.py +0 -0
- {eqc_models-0.11.1.data → eqc_models-0.13.0.data}/platlib/eqc_models/graph/partition.py +0 -0
- {eqc_models-0.11.1.data → eqc_models-0.13.0.data}/platlib/eqc_models/ml/__init__.py +0 -0
- {eqc_models-0.11.1.data → eqc_models-0.13.0.data}/platlib/eqc_models/ml/cvqboost_hamiltonian.pyx +0 -0
- {eqc_models-0.11.1.data → eqc_models-0.13.0.data}/platlib/eqc_models/ml/cvqboost_hamiltonian_c_func.c +0 -0
- {eqc_models-0.11.1.data → eqc_models-0.13.0.data}/platlib/eqc_models/ml/cvqboost_hamiltonian_c_func.h +0 -0
- {eqc_models-0.11.1.data → eqc_models-0.13.0.data}/platlib/eqc_models/ml/forecast.py +0 -0
- {eqc_models-0.11.1.data → eqc_models-0.13.0.data}/platlib/eqc_models/ml/forecastbase.py +0 -0
- {eqc_models-0.11.1.data → eqc_models-0.13.0.data}/platlib/eqc_models/ml/regressor.py +0 -0
- {eqc_models-0.11.1.data → eqc_models-0.13.0.data}/platlib/eqc_models/ml/reservoir.py +0 -0
- {eqc_models-0.11.1.data → eqc_models-0.13.0.data}/platlib/eqc_models/sequence/__init__.py +0 -0
- {eqc_models-0.11.1.data → eqc_models-0.13.0.data}/platlib/eqc_models/sequence/tsp.py +0 -0
- {eqc_models-0.11.1.data → eqc_models-0.13.0.data}/platlib/eqc_models/utilities/__init__.py +0 -0
- {eqc_models-0.11.1.data → eqc_models-0.13.0.data}/platlib/eqc_models/utilities/fileio.py +0 -0
- {eqc_models-0.11.1.data → eqc_models-0.13.0.data}/platlib/eqc_models/utilities/polynomial.py +0 -0
- {eqc_models-0.11.1.data → eqc_models-0.13.0.data}/platlib/eqc_models/utilities/qplib.py +0 -0
- {eqc_models-0.11.1.dist-info → eqc_models-0.13.0.dist-info}/licenses/LICENSE.txt +0 -0
- {eqc_models-0.11.1.dist-info → eqc_models-0.13.0.dist-info}/top_level.txt +0 -0
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
# (C) Quantum Computing Inc., 2024.
|
|
2
2
|
from typing import Tuple, Union, List
|
|
3
|
+
import logging
|
|
3
4
|
import numpy as np
|
|
4
5
|
from eqc_models.base.base import EqcModel
|
|
5
6
|
from eqc_models.base.operators import Polynomial, QUBO, OperatorNotAvailableError
|
|
6
7
|
from eqc_models.base.constraints import ConstraintsMixIn
|
|
7
8
|
|
|
9
|
+
log = logging.getLogger(name=__name__)
|
|
10
|
+
|
|
8
11
|
class PolynomialMixin:
|
|
9
12
|
"""This class provides an instance method and property that
|
|
10
13
|
manage polynomial models.
|
|
@@ -46,7 +49,12 @@ class PolynomialMixin:
|
|
|
46
49
|
|
|
47
50
|
"""
|
|
48
51
|
|
|
49
|
-
|
|
52
|
+
coefficients, indices = self.H
|
|
53
|
+
log.debug("Coefficients (%d): %s", len(coefficients), coefficients)
|
|
54
|
+
log.debug("Indices (%d): %s", len(indices), indices)
|
|
55
|
+
polynomial = self.polynomial
|
|
56
|
+
log.debug("Polynomial: %s", polynomial)
|
|
57
|
+
value = polynomial.evaluate(np.array(solution))
|
|
50
58
|
|
|
51
59
|
return value
|
|
52
60
|
|
|
@@ -107,8 +115,19 @@ class PolynomialModel(PolynomialMixin, EqcModel):
|
|
|
107
115
|
"""
|
|
108
116
|
|
|
109
117
|
def __init__(self, coefficients : Union[List, np.ndarray], indices : Union[List, np.ndarray]) -> None:
|
|
118
|
+
# ensure that the coefficients are ordered and not duplicated
|
|
119
|
+
new_coefficients = {}
|
|
120
|
+
for idx, v in zip(indices, coefficients):
|
|
121
|
+
idx = tuple(idx)
|
|
122
|
+
if idx in new_coefficients:
|
|
123
|
+
new_coefficients[idx] += v
|
|
124
|
+
else:
|
|
125
|
+
new_coefficients[idx] = v
|
|
126
|
+
new_indices = [idx for idx in new_coefficients]
|
|
127
|
+
new_indices.sort()
|
|
128
|
+
coefficients = [new_coefficients[idx] for idx in new_indices]
|
|
110
129
|
self.coefficients = coefficients
|
|
111
|
-
self.indices =
|
|
130
|
+
self.indices = new_indices
|
|
112
131
|
|
|
113
132
|
@property
|
|
114
133
|
def polynomial(self) -> Polynomial:
|
|
@@ -245,6 +245,6 @@ class ConstrainedQuadraticModel(ConstraintsMixIn, QuadraticModel):
|
|
|
245
245
|
return self.lhs, self.rhs
|
|
246
246
|
|
|
247
247
|
def evaluateObjective(self, solution: np.ndarray) -> float:
|
|
248
|
-
J = self.quad_objective
|
|
249
|
-
C = self.linear_objective
|
|
248
|
+
J = np.array(self.quad_objective)
|
|
249
|
+
C = np.array(self.linear_objective)
|
|
250
250
|
return np.squeeze(C.T @ solution + solution.T@J@solution)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import dataclasses
|
|
2
|
+
from typing import Dict
|
|
2
3
|
import warnings
|
|
3
4
|
import numpy as np
|
|
4
5
|
|
|
@@ -41,6 +42,14 @@ class SolutionResults:
|
|
|
41
42
|
device : str
|
|
42
43
|
String that represents the device used to solve the model.
|
|
43
44
|
|
|
45
|
+
raw_solutions : np.ndarray
|
|
46
|
+
Numpy array of the solutions as returned by the solver device.
|
|
47
|
+
|
|
48
|
+
calibration_time : float
|
|
49
|
+
Total time spend during job exectution where the device performed calibration.
|
|
50
|
+
The calibration is not directly affected by the job submission and the time
|
|
51
|
+
is not included in run_time.
|
|
52
|
+
|
|
44
53
|
time_units : str
|
|
45
54
|
String indicator of the unit of time reported in the metrics. Only
|
|
46
55
|
ns is supported at this time.
|
|
@@ -56,6 +65,7 @@ class SolutionResults:
|
|
|
56
65
|
postprocessing_time : np.ndarray
|
|
57
66
|
penalties : np.ndarray = None
|
|
58
67
|
device : str = None
|
|
68
|
+
raw_solutions : np.ndarray = None
|
|
59
69
|
time_units : str = "ns"
|
|
60
70
|
|
|
61
71
|
@property
|
|
@@ -95,7 +105,22 @@ class SolutionResults:
|
|
|
95
105
|
|
|
96
106
|
@classmethod
|
|
97
107
|
def from_cloud_response(cls, model, response, solver):
|
|
98
|
-
"""
|
|
108
|
+
"""
|
|
109
|
+
Fill in the details from the cloud
|
|
110
|
+
|
|
111
|
+
Parameters
|
|
112
|
+
------------
|
|
113
|
+
|
|
114
|
+
model : eqc_models.base.EqcModel
|
|
115
|
+
EqcModel object describing the problem solved in response
|
|
116
|
+
|
|
117
|
+
response : Dict
|
|
118
|
+
Dictionary of the repsonse from the solver device.
|
|
119
|
+
|
|
120
|
+
solver : eqc_models.base.ModelSolver
|
|
121
|
+
ModelSolver object which is used to obtain job metrics.
|
|
122
|
+
|
|
123
|
+
"""
|
|
99
124
|
|
|
100
125
|
solutions = np.array(response["results"]["solutions"])
|
|
101
126
|
if model.machine_slacks > 0:
|
|
@@ -164,3 +189,58 @@ class SolutionResults:
|
|
|
164
189
|
device=device_type, time_units="ns")
|
|
165
190
|
|
|
166
191
|
return results
|
|
192
|
+
|
|
193
|
+
@classmethod
|
|
194
|
+
def from_eqcdirect_response(cls, model, response, solver):
|
|
195
|
+
"""
|
|
196
|
+
Fill in details from the response dictionary and possibly the solver device.
|
|
197
|
+
|
|
198
|
+
Parameters
|
|
199
|
+
------------
|
|
200
|
+
|
|
201
|
+
model : eqc_models.base.EqcModel
|
|
202
|
+
EqcModel object describing the problem solved in response
|
|
203
|
+
|
|
204
|
+
response : Dict
|
|
205
|
+
Dictionary of the repsonse from the solver device.
|
|
206
|
+
|
|
207
|
+
solver : eqc_models.base.ModelSolver
|
|
208
|
+
ModelSolver object which is used to obtain device information.
|
|
209
|
+
|
|
210
|
+
"""
|
|
211
|
+
solutions = np.array(response["solution"])
|
|
212
|
+
if model.machine_slacks > 0:
|
|
213
|
+
solutions = solutions[:,:-model.machine_slacks]
|
|
214
|
+
energies = np.array(response["energy"])
|
|
215
|
+
# interrogate to determine the device type
|
|
216
|
+
info_dict = solver.client.system_info()
|
|
217
|
+
device_type = info_dict["device_type"]
|
|
218
|
+
if hasattr(model, "evaluateObjective"):
|
|
219
|
+
objectives = np.zeros((solutions.shape[0],), dtype=np.float32)
|
|
220
|
+
for i in range(solutions.shape[0]):
|
|
221
|
+
try:
|
|
222
|
+
objective = model.evaluateObjective(solutions[i])
|
|
223
|
+
except NotImplementedError:
|
|
224
|
+
warnings.warn(f"Cannot set objective value in results for {model.__class__}")
|
|
225
|
+
objectives = None
|
|
226
|
+
break
|
|
227
|
+
objectives[i] = objective
|
|
228
|
+
else:
|
|
229
|
+
objectives = None
|
|
230
|
+
if hasattr(model, "evaluatePenalties"):
|
|
231
|
+
penalties = np.zeros((solutions.shape[0],), dtype=np.float32)
|
|
232
|
+
for i in range(solutions.shape[0]):
|
|
233
|
+
penalties[i] = model.evaluatePenalties(solutions[i]) + model.offset
|
|
234
|
+
else:
|
|
235
|
+
penalties = None
|
|
236
|
+
counts = np.ones(solutions.shape[0])
|
|
237
|
+
runtime = response["runtime"]
|
|
238
|
+
post = response["postprocessing_time"]
|
|
239
|
+
pre = response["preprocessing_time"]
|
|
240
|
+
results = SolutionResults(solutions, energies, counts, objectives,
|
|
241
|
+
runtime, pre, post, penalties=penalties,
|
|
242
|
+
device=device_type, time_units="s")
|
|
243
|
+
|
|
244
|
+
return results
|
|
245
|
+
|
|
246
|
+
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# (C) Quantum Computing Inc., 2024.
|
|
2
|
+
|
|
3
|
+
from .base import EdgeMixin, EdgeModel, GraphModel, NodeModel
|
|
4
|
+
from .maxcut import MaxCutModel
|
|
5
|
+
from .partition import GraphPartitionModel
|
|
6
|
+
from .shortestpath import ShortestPathModel
|
|
7
|
+
from .rcshortestpath import RCShortestPathModel
|
|
8
|
+
|
|
9
|
+
__all__ = ["MaxCutModel", "GraphPartitionModel",
|
|
10
|
+
"EdgeMixin", "EdgeModel", "GraphModel",
|
|
11
|
+
"NodeModel"]
|
|
@@ -7,7 +7,7 @@ class GraphModel(QuadraticModel):
|
|
|
7
7
|
""" """
|
|
8
8
|
def __init__(self, G : nx.Graph):
|
|
9
9
|
self.G = G
|
|
10
|
-
super().__init__(*self.costFunction())
|
|
10
|
+
super(GraphModel, self).__init__(*self.costFunction())
|
|
11
11
|
|
|
12
12
|
@property
|
|
13
13
|
def linear_objective(self):
|
|
@@ -63,12 +63,16 @@ class TwoPartitionModel(NodeModel):
|
|
|
63
63
|
|
|
64
64
|
"""
|
|
65
65
|
|
|
66
|
-
class
|
|
67
|
-
""" Create a model where the variables are edge-based """
|
|
66
|
+
class EdgeMixin:
|
|
68
67
|
|
|
69
68
|
@property
|
|
70
69
|
def variables(self) -> List[str]:
|
|
71
70
|
""" Provide a variable name to index lookup; order enforced by sorting the list before returning """
|
|
72
|
-
names = [
|
|
71
|
+
names = [(u, v) for u, v in self.G.edges]
|
|
73
72
|
names.sort()
|
|
74
73
|
return names
|
|
74
|
+
|
|
75
|
+
class EdgeModel(EdgeMixin, GraphModel):
|
|
76
|
+
""" Create a model where the variables are edge-based """
|
|
77
|
+
|
|
78
|
+
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import (Any, Dict, Tuple, List)
|
|
3
|
+
import networkx as nx
|
|
4
|
+
import numpy as np
|
|
5
|
+
from eqc_models.graph.shortestpath import ShortestPathModel
|
|
6
|
+
|
|
7
|
+
log = logging.getLogger(name=__name__)
|
|
8
|
+
|
|
9
|
+
class RCShortestPathModel(ShortestPathModel):
|
|
10
|
+
"""
|
|
11
|
+
Model for resource constrained shortest path problems.
|
|
12
|
+
Use a shortest path base model to implement the routing
|
|
13
|
+
constraints and objective function. Add resource constraints.
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, G : nx.DiGraph, s : Any, t : Any, T : int, resource_key : str="resource"):
|
|
18
|
+
if T <= 0:
|
|
19
|
+
raise ValueError("T must be positive")
|
|
20
|
+
elif round(T, 0) != T:
|
|
21
|
+
raise ValueError("T must be integer-valued")
|
|
22
|
+
self.T = T
|
|
23
|
+
self.resource_key = resource_key
|
|
24
|
+
# determine shortest path by weight
|
|
25
|
+
nx_path = nx.shortest_path(G, s, t)
|
|
26
|
+
path_length = len(nx_path)
|
|
27
|
+
self.resource_mult = resource_mult = 2 * path_length / self.T
|
|
28
|
+
super(RCShortestPathModel, self).__init__(G, s, t)
|
|
29
|
+
upper_bound = np.ones(len(self.variables))
|
|
30
|
+
upper_bound[-1] = np.ceil(resource_mult * T)
|
|
31
|
+
self.upper_bound = upper_bound
|
|
32
|
+
is_discrete = [True for i in range(upper_bound.shape[0])]
|
|
33
|
+
is_discrete[-1] = False
|
|
34
|
+
self.is_discrete = is_discrete
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def variables(self):
|
|
38
|
+
variables = super(RCShortestPathModel, self).variables
|
|
39
|
+
return variables + ["resource_slack"]
|
|
40
|
+
|
|
41
|
+
def buildConstraints(self) -> Tuple[np.ndarray,np.ndarray]:
|
|
42
|
+
lhs, rhs = super(RCShortestPathModel, self).buildConstraints()
|
|
43
|
+
# add a single constraint
|
|
44
|
+
G = self.G
|
|
45
|
+
n = len(self.variables)
|
|
46
|
+
resource_lhs = np.zeros((1, n), dtype=np.float32)
|
|
47
|
+
resource_mult = self.resource_mult
|
|
48
|
+
log.debug("Resource multiplier %f", resource_mult)
|
|
49
|
+
for i in range(len(G.edges)):
|
|
50
|
+
(u, v) = self.variables[i]
|
|
51
|
+
# find the time to traverse the arc
|
|
52
|
+
resource_cost = G.edges[(u, v)][self.resource_key]
|
|
53
|
+
resource_cost *= resource_mult
|
|
54
|
+
log.debug(f"Adding resource %s for edge %s", resource_cost, (u, v))
|
|
55
|
+
resource_lhs[0, i] = resource_cost
|
|
56
|
+
resource_lhs[0, -1] = 1
|
|
57
|
+
lhs = self._stackLHS(lhs, resource_lhs)
|
|
58
|
+
rhs = np.hstack([rhs, [self.T*resource_mult]])
|
|
59
|
+
log.debug("LHS shape %s RHS shape %s", lhs.shape, rhs.shape)
|
|
60
|
+
return lhs, rhs
|
|
61
|
+
|
|
62
|
+
def pathCost(self, path):
|
|
63
|
+
""" sum the cost of all legs in the path """
|
|
64
|
+
assert self.s in path
|
|
65
|
+
G = self.G
|
|
66
|
+
node = self.s
|
|
67
|
+
max_len = len(self.G.nodes) - 1
|
|
68
|
+
path_len = 0
|
|
69
|
+
path_cost = 0
|
|
70
|
+
path_resources = 0
|
|
71
|
+
while node != t:
|
|
72
|
+
edge = (node, path[node])
|
|
73
|
+
if edge not in G.edges:
|
|
74
|
+
raise ValueError(f"Edge {edge} not found")
|
|
75
|
+
path_len += 1
|
|
76
|
+
path_cost += G.edges[edge]["weight"]
|
|
77
|
+
path_resources += G.edges[edge][self.resource_key]
|
|
78
|
+
if path_len > max_len:
|
|
79
|
+
raise ValueError("Invalid path. Describes a cycle.")
|
|
80
|
+
return path_cost, path_resources
|
|
81
|
+
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
r"""
|
|
2
|
+
MIP Shortest Path implementation
|
|
3
|
+
|
|
4
|
+
Given a graph $G$ and nodes $s$ and $t$, find the shortest path
|
|
5
|
+
by edge weight between $s$ and $t$.
|
|
6
|
+
|
|
7
|
+
$$
|
|
8
|
+
\min sum_ij w_ij x_ij
|
|
9
|
+
$$
|
|
10
|
+
subject to
|
|
11
|
+
$$
|
|
12
|
+
\sum_{(u,v)\in E} x_{u,v} - \sum{(v,u)\in E} x_{v,u} = 0 \forall u\in N\\{s,t}
|
|
13
|
+
$$
|
|
14
|
+
and
|
|
15
|
+
$$
|
|
16
|
+
\sum_{(s,v)\in E} x_{s,v} = 1
|
|
17
|
+
$$
|
|
18
|
+
and
|
|
19
|
+
$$
|
|
20
|
+
\sum_{(u,t)\in E} x_{u,t} = 1
|
|
21
|
+
$$
|
|
22
|
+
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from typing import Any, Dict, Tuple
|
|
26
|
+
import logging
|
|
27
|
+
import numpy as np
|
|
28
|
+
import networkx as nx
|
|
29
|
+
from eqc_models.graph import EdgeMixin
|
|
30
|
+
from eqc_models.base.quadratic import ConstrainedQuadraticModel
|
|
31
|
+
|
|
32
|
+
log = logging.getLogger(name=__name__)
|
|
33
|
+
|
|
34
|
+
class ShortestPathModel(EdgeMixin, ConstrainedQuadraticModel):
|
|
35
|
+
"""
|
|
36
|
+
ShortestPathModel describes the MIP formulation for the
|
|
37
|
+
shortest path problem.
|
|
38
|
+
|
|
39
|
+
Parameters
|
|
40
|
+
-------------
|
|
41
|
+
|
|
42
|
+
G : nx.DiGraph
|
|
43
|
+
A directed graph which is assumed to be connected. A graph
|
|
44
|
+
with disconnected subgraphs may reveal a solution if $s$ and $t$
|
|
45
|
+
are in the same subgraph, but testing for the existence of a path
|
|
46
|
+
between s and t using this model is not recommended. This is
|
|
47
|
+
due to the difficulty posed by selecting a penalty multiplier
|
|
48
|
+
large enough to enforce the panalties, which DNE in the infeasible
|
|
49
|
+
case.
|
|
50
|
+
s : Any
|
|
51
|
+
This is the label for the start node.
|
|
52
|
+
t : Any
|
|
53
|
+
This is the label for the end node.
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def __init__(self, G: nx.DiGraph, s : Any, t : Any):
|
|
59
|
+
self.G = G
|
|
60
|
+
self.s = s
|
|
61
|
+
self.t = t
|
|
62
|
+
self.lhs, self.rhs = self.buildConstraints()
|
|
63
|
+
C, J = self.buildObjective()
|
|
64
|
+
super(ShortestPathModel, self).__init__(C, J, self.lhs, self.rhs)
|
|
65
|
+
self.upper_bound = np.ones(self.lhs.shape[1])
|
|
66
|
+
self.is_discrete = [True for i in range(self.lhs.shape[1])]
|
|
67
|
+
self.machine_slacks = 0
|
|
68
|
+
|
|
69
|
+
def buildConstraints(self) -> Tuple[np.ndarray,np.ndarray]:
|
|
70
|
+
"""
|
|
71
|
+
Constraints:
|
|
72
|
+
$$
|
|
73
|
+
sum_j x[i,l] - sum_j x[j,l] = c for all l
|
|
74
|
+
$$
|
|
75
|
+
$c$ is -1, 1 or 0 for $i=t$, $s$ or all others
|
|
76
|
+
|
|
77
|
+
"""
|
|
78
|
+
log.debug("Building constraints to find path from %s to %s", self.s, self.t)
|
|
79
|
+
variables = self.variables
|
|
80
|
+
nodes = [n for n in self.G.nodes]
|
|
81
|
+
m = len(nodes)
|
|
82
|
+
n = len(variables)
|
|
83
|
+
_cons = np.zeros((m, n), dtype=np.int8)
|
|
84
|
+
_rhs = np.zeros((m, 1), dtype=np.int8)
|
|
85
|
+
for node_index, k in enumerate(nodes):
|
|
86
|
+
if k == self.s:
|
|
87
|
+
_rhs[node_index, 0] = 1
|
|
88
|
+
elif k == self.t:
|
|
89
|
+
_rhs[node_index, 0] = -1
|
|
90
|
+
for l in range(len(self.G.edges)): # don't enumerate the edges because the order could change
|
|
91
|
+
(i, j) = self.variables[l]
|
|
92
|
+
if i == j:
|
|
93
|
+
# self loops are not allowed
|
|
94
|
+
raise ValueError("Self loops are not allowed in ShortestPathModel")
|
|
95
|
+
# # ignore these edges because we can't go back to s or leave t
|
|
96
|
+
elif j == self.s:
|
|
97
|
+
continue
|
|
98
|
+
elif i == self.t:
|
|
99
|
+
continue
|
|
100
|
+
i_index = nodes.index(i)
|
|
101
|
+
j_index = nodes.index(j)
|
|
102
|
+
_cons[i_index, l] = 1
|
|
103
|
+
_cons[j_index, l] = -1
|
|
104
|
+
log.debug("LHS shape %s RHS shape %s", _cons.shape, _rhs.shape)
|
|
105
|
+
log.debug("checksum %f min %f", np.sum(_cons), np.min(_cons))
|
|
106
|
+
assert np.sum(_rhs) == 0
|
|
107
|
+
return _cons, np.squeeze(_rhs)
|
|
108
|
+
|
|
109
|
+
def buildObjective(self) -> Tuple[np.ndarray, np.ndarray]:
|
|
110
|
+
r"""
|
|
111
|
+
Objective:
|
|
112
|
+
$\min sum_ij w_ij x_ij$
|
|
113
|
+
|
|
114
|
+
"""
|
|
115
|
+
variables = self.variables
|
|
116
|
+
G = self.G
|
|
117
|
+
nodes = G.nodes
|
|
118
|
+
m, n = len(nodes), len(variables)
|
|
119
|
+
_obj = [0 for i in range(n)]
|
|
120
|
+
for index, name in enumerate(variables):
|
|
121
|
+
if type(name) == type((1,2)):
|
|
122
|
+
i, j = name
|
|
123
|
+
_obj[index] = v = G.get_edge_data(i, j)["weight"]
|
|
124
|
+
assert not np.isnan(v), f"Got a NaN at {i, j}"
|
|
125
|
+
J = np.zeros((n, n))
|
|
126
|
+
return np.array(_obj), J
|
|
127
|
+
|
|
128
|
+
def decode(self, solution : np.ndarray) -> Dict:
|
|
129
|
+
"""
|
|
130
|
+
Convert a solution to this model into a path, which is
|
|
131
|
+
a dictionary with each edge described by key, value pairs.
|
|
132
|
+
|
|
133
|
+
"""
|
|
134
|
+
variables = self.variables[:len(self.G.edges)]
|
|
135
|
+
# log.debug("Using variables %s", variables)
|
|
136
|
+
|
|
137
|
+
lhs, rhs = self.constraints
|
|
138
|
+
log.debug("LHS shape %s RHS shape %s", lhs.shape, rhs.shape)
|
|
139
|
+
upper_thresh = max(solution[:len(variables)])
|
|
140
|
+
lower_thresh = 0
|
|
141
|
+
got_path = None
|
|
142
|
+
while upper_thresh - lower_thresh > 1e-6:
|
|
143
|
+
log.debug("Lower Value: %f Upper Value %f", lower_thresh, upper_thresh)
|
|
144
|
+
thresh = (lower_thresh + upper_thresh) / 2
|
|
145
|
+
nx_path = None
|
|
146
|
+
G = nx.DiGraph()
|
|
147
|
+
for (i, j), value in zip(variables, solution):
|
|
148
|
+
if value > thresh:
|
|
149
|
+
G.add_edge(i, j)
|
|
150
|
+
edges = [e for e in G.edges]
|
|
151
|
+
log.debug("Resulting edge count %s", len(edges))
|
|
152
|
+
self.alt_g = G
|
|
153
|
+
path = {}
|
|
154
|
+
try:
|
|
155
|
+
nx_path = nx.shortest_path(G, self.s, self.t)
|
|
156
|
+
lower_thresh = thresh
|
|
157
|
+
got_path = nx_path
|
|
158
|
+
log.debug("Got path size %d", len(got_path))
|
|
159
|
+
except (nx.exception.NodeNotFound, nx.NetworkXAlgorithmError) as err:
|
|
160
|
+
upper_thresh = thresh
|
|
161
|
+
if got_path is None:
|
|
162
|
+
raise RuntimeError(f"Solution does not describe path from {self.s} to {self.t}")
|
|
163
|
+
# path = {}
|
|
164
|
+
# log.debug("Translating path to dictionary")
|
|
165
|
+
# for i, v in enumerate(got_path):
|
|
166
|
+
# path[got_path[i-1]] = v
|
|
167
|
+
# log.debug("Updated path %s", path)
|
|
168
|
+
# if self.t in path:
|
|
169
|
+
# log.debug("Removing %s from path keys.", self.t)
|
|
170
|
+
# del path[self.t]
|
|
171
|
+
return got_path
|
|
172
|
+
|
|
173
|
+
def pathCost(self, path):
|
|
174
|
+
""" sum the cost of all legs in the path """
|
|
175
|
+
assert self.s in path
|
|
176
|
+
G = self.G
|
|
177
|
+
node = self.s
|
|
178
|
+
max_len = len(self.G.nodes) - 1
|
|
179
|
+
path_len = 0
|
|
180
|
+
path_cost = 0
|
|
181
|
+
while node != t:
|
|
182
|
+
edge = (node, path[node])
|
|
183
|
+
if edge not in G.edges:
|
|
184
|
+
raise ValueError(f"Edge {edge} not found")
|
|
185
|
+
path_len += 1
|
|
186
|
+
path_cost += G.edges[edge]["weight"]
|
|
187
|
+
if path_len > max_len:
|
|
188
|
+
raise ValueError("Invalid path. Describes a cycle.")
|
|
189
|
+
return path_cost
|
|
190
|
+
|
|
@@ -11,6 +11,7 @@ import numpy as np
|
|
|
11
11
|
|
|
12
12
|
from eqc_models import QuadraticModel
|
|
13
13
|
from eqc_models.solvers.qciclient import Dirac3CloudSolver
|
|
14
|
+
from eqc_models.solvers.eqcdirect import Dirac3DirectSolver
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
class ClassifierBase(QuadraticModel):
|
|
@@ -18,12 +19,25 @@ class ClassifierBase(QuadraticModel):
|
|
|
18
19
|
self,
|
|
19
20
|
relaxation_schedule=2,
|
|
20
21
|
num_samples=1,
|
|
22
|
+
solver_access="cloud",
|
|
23
|
+
ip_addr=None,
|
|
24
|
+
port=None,
|
|
21
25
|
):
|
|
22
26
|
|
|
23
27
|
super(self).__init__(None, None, None)
|
|
24
28
|
|
|
25
29
|
self.relaxation_schedule = relaxation_schedule
|
|
26
30
|
self.num_samples = num_samples
|
|
31
|
+
|
|
32
|
+
assert solver_access in ["cloud", "direct"]
|
|
33
|
+
|
|
34
|
+
if solver_access == "direct":
|
|
35
|
+
assert ip_addr is not None, "ip_addr should be set when using direct solver!"
|
|
36
|
+
assert port is not None,"port should be set when using direct solver!"
|
|
37
|
+
|
|
38
|
+
self.solver_access = solver_access
|
|
39
|
+
self.ip_addr = ip_addr
|
|
40
|
+
self.port = port
|
|
27
41
|
self.params = None
|
|
28
42
|
self.X_train = None
|
|
29
43
|
self.y_train = None
|
|
@@ -53,18 +67,29 @@ class ClassifierBase(QuadraticModel):
|
|
|
53
67
|
return
|
|
54
68
|
|
|
55
69
|
def solve(self):
|
|
56
|
-
|
|
70
|
+
|
|
71
|
+
if self.solver_access == "direct":
|
|
72
|
+
solver = Dirac3DirectSolver()
|
|
73
|
+
solver.connect(self.ip_addr, self.port)
|
|
74
|
+
else:
|
|
75
|
+
solver = Dirac3CloudSolver()
|
|
76
|
+
|
|
57
77
|
response = solver.solve(
|
|
58
78
|
self,
|
|
59
79
|
sum_constraint=self._sum_constraint,
|
|
60
80
|
relaxation_schedule=self.relaxation_schedule,
|
|
61
|
-
solution_precision=1,
|
|
62
81
|
num_samples=self.num_samples,
|
|
63
82
|
)
|
|
64
83
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
84
|
+
if self.solver_access == "cloud":
|
|
85
|
+
energies = response["results"]["energies"]
|
|
86
|
+
solutions = response["results"]["solutions"]
|
|
87
|
+
elif self.solver_access == "direct":
|
|
88
|
+
energies = response["energy"]
|
|
89
|
+
solutions = response["solution"]
|
|
90
|
+
|
|
91
|
+
min_id = np.argmin(energies)
|
|
92
|
+
sol = solutions[min_id]
|
|
68
93
|
|
|
69
94
|
print(response)
|
|
70
95
|
|
|
@@ -94,6 +94,12 @@ class QBoostClassifier(ClassifierBase):
|
|
|
94
94
|
|
|
95
95
|
num_samples: Number of samples used by Dirac-3; default: 1.
|
|
96
96
|
|
|
97
|
+
solver_access: Solver access type: cloud or direct; default: cloud.
|
|
98
|
+
|
|
99
|
+
ip_addr: IP address of the device when direct access is used; default: None.
|
|
100
|
+
|
|
101
|
+
port: Port number of the device when direct access is used; default: None.
|
|
102
|
+
|
|
97
103
|
lambda_coef: A penalty multiplier; default: 0.
|
|
98
104
|
|
|
99
105
|
weak_cls_schedule: Weak classifier schedule. Is either 1, 2,
|
|
@@ -155,6 +161,9 @@ class QBoostClassifier(ClassifierBase):
|
|
|
155
161
|
self,
|
|
156
162
|
relaxation_schedule=2,
|
|
157
163
|
num_samples=1,
|
|
164
|
+
solver_access="cloud",
|
|
165
|
+
ip_addr=None,
|
|
166
|
+
port=None,
|
|
158
167
|
lambda_coef=0,
|
|
159
168
|
weak_cls_schedule=2,
|
|
160
169
|
weak_cls_type="lg",
|
|
@@ -172,9 +181,13 @@ class QBoostClassifier(ClassifierBase):
|
|
|
172
181
|
"multi_processing_shm",
|
|
173
182
|
"sequential",
|
|
174
183
|
]
|
|
175
|
-
|
|
184
|
+
assert solver_access in ["cloud", "direct"]
|
|
185
|
+
|
|
176
186
|
self.relaxation_schedule = relaxation_schedule
|
|
177
187
|
self.num_samples = num_samples
|
|
188
|
+
self.solver_access = solver_access
|
|
189
|
+
self.ip_addr = ip_addr
|
|
190
|
+
self.port = port
|
|
178
191
|
self.lambda_coef = lambda_coef
|
|
179
192
|
self.weak_cls_schedule = weak_cls_schedule
|
|
180
193
|
self.weak_cls_type = weak_cls_type
|
|
@@ -24,6 +24,12 @@ class QSVMClassifier(ClassifierBase):
|
|
|
24
24
|
|
|
25
25
|
num_samples: Number of samples used by Dirac-3; default: 1.
|
|
26
26
|
|
|
27
|
+
solver_access: Solver access type: cloud or direct; default: cloud.
|
|
28
|
+
|
|
29
|
+
ip_addr: IP address of the device when direct access is used; default: None.
|
|
30
|
+
|
|
31
|
+
port: Port number of the device when direct access is used; default: None.
|
|
32
|
+
|
|
27
33
|
lambda_coef: The penalty multipler
|
|
28
34
|
|
|
29
35
|
Examples
|
|
@@ -67,12 +73,20 @@ class QSVMClassifier(ClassifierBase):
|
|
|
67
73
|
self,
|
|
68
74
|
relaxation_schedule=1,
|
|
69
75
|
num_samples=1,
|
|
76
|
+
solver_access="cloud",
|
|
77
|
+
ip_addr=None,
|
|
78
|
+
port=None,
|
|
70
79
|
lambda_coef=1.0,
|
|
71
80
|
):
|
|
72
81
|
super(QSVMClassifier).__init__()
|
|
73
82
|
|
|
83
|
+
assert solver_access in ["cloud", "direct"]
|
|
84
|
+
|
|
74
85
|
self.relaxation_schedule = relaxation_schedule
|
|
75
86
|
self.num_samples = num_samples
|
|
87
|
+
self.solver_access = solver_access
|
|
88
|
+
self.ip_addr = ip_addr
|
|
89
|
+
self.port = port
|
|
76
90
|
self.lambda_coef = lambda_coef
|
|
77
91
|
self.fea_scaler = MinMaxScaler(feature_range=(-1, 1))
|
|
78
92
|
|
|
@@ -199,6 +213,12 @@ class QSVMClassifierDual(ClassifierBase):
|
|
|
199
213
|
|
|
200
214
|
num_samples: Number of samples used by Dirac-3; default: 1.
|
|
201
215
|
|
|
216
|
+
solver_access: Solver access type: cloud or direct; default: cloud.
|
|
217
|
+
|
|
218
|
+
ip_addr: IP address of the device when direct access is used; default: None.
|
|
219
|
+
|
|
220
|
+
port: Port number of the device when direct access is used; default: None.
|
|
221
|
+
|
|
202
222
|
upper_limit: Coefficient upper limit; a regularization parameter;
|
|
203
223
|
default: 1.0.
|
|
204
224
|
|
|
@@ -253,6 +273,9 @@ class QSVMClassifierDual(ClassifierBase):
|
|
|
253
273
|
self,
|
|
254
274
|
relaxation_schedule=2,
|
|
255
275
|
num_samples=1,
|
|
276
|
+
solver_access="cloud",
|
|
277
|
+
ip_addr=None,
|
|
278
|
+
port=None,
|
|
256
279
|
upper_limit=1.0,
|
|
257
280
|
gamma=1.0,
|
|
258
281
|
eta=1.0,
|
|
@@ -260,8 +283,13 @@ class QSVMClassifierDual(ClassifierBase):
|
|
|
260
283
|
):
|
|
261
284
|
super(QSVMClassifierDual).__init__()
|
|
262
285
|
|
|
286
|
+
assert solver_access in ["cloud", "direct"]
|
|
287
|
+
|
|
263
288
|
self.relaxation_schedule = relaxation_schedule
|
|
264
289
|
self.num_samples = num_samples
|
|
290
|
+
self.solver_access = solver_access
|
|
291
|
+
self.ip_addr = ip_addr
|
|
292
|
+
self.port = port
|
|
265
293
|
self.upper_limit = upper_limit
|
|
266
294
|
self.gamma = gamma
|
|
267
295
|
self.eta = eta
|