eqc-models 0.12.0__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.
Files changed (71) hide show
  1. {eqc_models-0.12.0.data → eqc_models-0.13.0.data}/platlib/eqc_models/base/base.py +11 -0
  2. eqc_models-0.13.0.data/platlib/eqc_models/base/binaries.py +33 -0
  3. {eqc_models-0.12.0.data → eqc_models-0.13.0.data}/platlib/eqc_models/base/constraints.py +39 -0
  4. {eqc_models-0.12.0.data → eqc_models-0.13.0.data}/platlib/eqc_models/base/polyeval.c +443 -267
  5. eqc_models-0.13.0.data/platlib/eqc_models/base/polyeval.cpython-310-darwin.so +0 -0
  6. {eqc_models-0.12.0.data → eqc_models-0.13.0.data}/platlib/eqc_models/base/polynomial.py +21 -2
  7. {eqc_models-0.12.0.data → eqc_models-0.13.0.data}/platlib/eqc_models/base/results.py +81 -1
  8. {eqc_models-0.12.0.data → eqc_models-0.13.0.data}/platlib/eqc_models/graph/__init__.py +2 -0
  9. eqc_models-0.13.0.data/platlib/eqc_models/graph/rcshortestpath.py +81 -0
  10. {eqc_models-0.12.0.data → eqc_models-0.13.0.data}/platlib/eqc_models/graph/shortestpath.py +52 -19
  11. {eqc_models-0.12.0.data → eqc_models-0.13.0.data}/platlib/eqc_models/ml/classifierbase.py +0 -1
  12. {eqc_models-0.12.0.data → eqc_models-0.13.0.data}/platlib/eqc_models/ml/clusteringbase.py +0 -1
  13. {eqc_models-0.12.0.data → eqc_models-0.13.0.data}/platlib/eqc_models/ml/decomposition.py +0 -1
  14. {eqc_models-0.12.0.data → eqc_models-0.13.0.data}/platlib/eqc_models/ml/regressorbase.py +0 -1
  15. {eqc_models-0.12.0.data → eqc_models-0.13.0.data}/platlib/eqc_models/solvers/__init__.py +9 -1
  16. {eqc_models-0.12.0.data → eqc_models-0.13.0.data}/platlib/eqc_models/solvers/eqcdirect.py +9 -4
  17. eqc_models-0.13.0.data/platlib/eqc_models/solvers/mip.py +115 -0
  18. {eqc_models-0.12.0.data → eqc_models-0.13.0.data}/platlib/eqc_models/solvers/qciclient.py +11 -22
  19. eqc_models-0.13.0.data/platlib/eqc_models/solvers/responselog.py +47 -0
  20. {eqc_models-0.12.0.dist-info → eqc_models-0.13.0.dist-info}/METADATA +7 -8
  21. eqc_models-0.13.0.dist-info/RECORD +69 -0
  22. eqc_models-0.12.0.data/platlib/eqc_models/base/polyeval.cpython-310-darwin.so +0 -0
  23. eqc_models-0.12.0.dist-info/RECORD +0 -65
  24. {eqc_models-0.12.0.data → eqc_models-0.13.0.data}/platlib/compile_extensions.py +0 -0
  25. {eqc_models-0.12.0.data → eqc_models-0.13.0.data}/platlib/eqc_models/__init__.py +0 -0
  26. {eqc_models-0.12.0.data → eqc_models-0.13.0.data}/platlib/eqc_models/algorithms/__init__.py +0 -0
  27. {eqc_models-0.12.0.data → eqc_models-0.13.0.data}/platlib/eqc_models/algorithms/base.py +0 -0
  28. {eqc_models-0.12.0.data → eqc_models-0.13.0.data}/platlib/eqc_models/algorithms/penaltymultiplier.py +0 -0
  29. {eqc_models-0.12.0.data → eqc_models-0.13.0.data}/platlib/eqc_models/allocation/__init__.py +0 -0
  30. {eqc_models-0.12.0.data → eqc_models-0.13.0.data}/platlib/eqc_models/allocation/allocation.py +0 -0
  31. {eqc_models-0.12.0.data → eqc_models-0.13.0.data}/platlib/eqc_models/allocation/portbase.py +0 -0
  32. {eqc_models-0.12.0.data → eqc_models-0.13.0.data}/platlib/eqc_models/allocation/portmomentum.py +0 -0
  33. {eqc_models-0.12.0.data → eqc_models-0.13.0.data}/platlib/eqc_models/assignment/__init__.py +0 -0
  34. {eqc_models-0.12.0.data → eqc_models-0.13.0.data}/platlib/eqc_models/assignment/qap.py +0 -0
  35. {eqc_models-0.12.0.data → eqc_models-0.13.0.data}/platlib/eqc_models/assignment/resource.py +0 -0
  36. {eqc_models-0.12.0.data → eqc_models-0.13.0.data}/platlib/eqc_models/assignment/setpartition.py +0 -0
  37. {eqc_models-0.12.0.data → eqc_models-0.13.0.data}/platlib/eqc_models/base/__init__.py +0 -0
  38. {eqc_models-0.12.0.data → eqc_models-0.13.0.data}/platlib/eqc_models/base/operators.py +0 -0
  39. {eqc_models-0.12.0.data → eqc_models-0.13.0.data}/platlib/eqc_models/base/polyeval.pyx +0 -0
  40. {eqc_models-0.12.0.data → eqc_models-0.13.0.data}/platlib/eqc_models/base/quadratic.py +0 -0
  41. {eqc_models-0.12.0.data → eqc_models-0.13.0.data}/platlib/eqc_models/combinatorics/__init__.py +0 -0
  42. {eqc_models-0.12.0.data → eqc_models-0.13.0.data}/platlib/eqc_models/combinatorics/setcover.py +0 -0
  43. {eqc_models-0.12.0.data → eqc_models-0.13.0.data}/platlib/eqc_models/combinatorics/setpartition.py +0 -0
  44. {eqc_models-0.12.0.data → eqc_models-0.13.0.data}/platlib/eqc_models/decoding.py +0 -0
  45. {eqc_models-0.12.0.data → eqc_models-0.13.0.data}/platlib/eqc_models/graph/base.py +0 -0
  46. {eqc_models-0.12.0.data → eqc_models-0.13.0.data}/platlib/eqc_models/graph/hypergraph.py +0 -0
  47. {eqc_models-0.12.0.data → eqc_models-0.13.0.data}/platlib/eqc_models/graph/maxcut.py +0 -0
  48. {eqc_models-0.12.0.data → eqc_models-0.13.0.data}/platlib/eqc_models/graph/maxkcut.py +0 -0
  49. {eqc_models-0.12.0.data → eqc_models-0.13.0.data}/platlib/eqc_models/graph/partition.py +0 -0
  50. {eqc_models-0.12.0.data → eqc_models-0.13.0.data}/platlib/eqc_models/ml/__init__.py +0 -0
  51. {eqc_models-0.12.0.data → eqc_models-0.13.0.data}/platlib/eqc_models/ml/classifierqboost.py +0 -0
  52. {eqc_models-0.12.0.data → eqc_models-0.13.0.data}/platlib/eqc_models/ml/classifierqsvm.py +0 -0
  53. {eqc_models-0.12.0.data → eqc_models-0.13.0.data}/platlib/eqc_models/ml/clustering.py +0 -0
  54. {eqc_models-0.12.0.data → eqc_models-0.13.0.data}/platlib/eqc_models/ml/cvqboost_hamiltonian.pyx +0 -0
  55. {eqc_models-0.12.0.data → eqc_models-0.13.0.data}/platlib/eqc_models/ml/cvqboost_hamiltonian_c_func.c +0 -0
  56. {eqc_models-0.12.0.data → eqc_models-0.13.0.data}/platlib/eqc_models/ml/cvqboost_hamiltonian_c_func.h +0 -0
  57. {eqc_models-0.12.0.data → eqc_models-0.13.0.data}/platlib/eqc_models/ml/forecast.py +0 -0
  58. {eqc_models-0.12.0.data → eqc_models-0.13.0.data}/platlib/eqc_models/ml/forecastbase.py +0 -0
  59. {eqc_models-0.12.0.data → eqc_models-0.13.0.data}/platlib/eqc_models/ml/regressor.py +0 -0
  60. {eqc_models-0.12.0.data → eqc_models-0.13.0.data}/platlib/eqc_models/ml/reservoir.py +0 -0
  61. {eqc_models-0.12.0.data → eqc_models-0.13.0.data}/platlib/eqc_models/process/base.py +0 -0
  62. {eqc_models-0.12.0.data → eqc_models-0.13.0.data}/platlib/eqc_models/process/mpc.py +0 -0
  63. {eqc_models-0.12.0.data → eqc_models-0.13.0.data}/platlib/eqc_models/sequence/__init__.py +0 -0
  64. {eqc_models-0.12.0.data → eqc_models-0.13.0.data}/platlib/eqc_models/sequence/tsp.py +0 -0
  65. {eqc_models-0.12.0.data → eqc_models-0.13.0.data}/platlib/eqc_models/utilities/__init__.py +0 -0
  66. {eqc_models-0.12.0.data → eqc_models-0.13.0.data}/platlib/eqc_models/utilities/fileio.py +0 -0
  67. {eqc_models-0.12.0.data → eqc_models-0.13.0.data}/platlib/eqc_models/utilities/polynomial.py +0 -0
  68. {eqc_models-0.12.0.data → eqc_models-0.13.0.data}/platlib/eqc_models/utilities/qplib.py +0 -0
  69. {eqc_models-0.12.0.dist-info → eqc_models-0.13.0.dist-info}/WHEEL +0 -0
  70. {eqc_models-0.12.0.dist-info → eqc_models-0.13.0.dist-info}/licenses/LICENSE.txt +0 -0
  71. {eqc_models-0.12.0.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
- value = self.polynomial.evaluate(np.array(solution))
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 = indices
130
+ self.indices = new_indices
112
131
 
113
132
  @property
114
133
  def polynomial(self) -> Polynomial:
@@ -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
- """ Fill in the details from the cloud """
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
+
@@ -3,6 +3,8 @@
3
3
  from .base import EdgeMixin, EdgeModel, GraphModel, NodeModel
4
4
  from .maxcut import MaxCutModel
5
5
  from .partition import GraphPartitionModel
6
+ from .shortestpath import ShortestPathModel
7
+ from .rcshortestpath import RCShortestPathModel
6
8
 
7
9
  __all__ = ["MaxCutModel", "GraphPartitionModel",
8
10
  "EdgeMixin", "EdgeModel", "GraphModel",
@@ -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
+
@@ -62,6 +62,9 @@ class ShortestPathModel(EdgeMixin, ConstrainedQuadraticModel):
62
62
  self.lhs, self.rhs = self.buildConstraints()
63
63
  C, J = self.buildObjective()
64
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
65
68
 
66
69
  def buildConstraints(self) -> Tuple[np.ndarray,np.ndarray]:
67
70
  """
@@ -72,7 +75,7 @@ class ShortestPathModel(EdgeMixin, ConstrainedQuadraticModel):
72
75
  $c$ is -1, 1 or 0 for $i=t$, $s$ or all others
73
76
 
74
77
  """
75
- log.info("Building constraints to find path from %s to %s", self.s, self.t)
78
+ log.debug("Building constraints to find path from %s to %s", self.s, self.t)
76
79
  variables = self.variables
77
80
  nodes = [n for n in self.G.nodes]
78
81
  m = len(nodes)
@@ -84,7 +87,8 @@ class ShortestPathModel(EdgeMixin, ConstrainedQuadraticModel):
84
87
  _rhs[node_index, 0] = 1
85
88
  elif k == self.t:
86
89
  _rhs[node_index, 0] = -1
87
- for l, (i, j) in enumerate(variables):
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]
88
92
  if i == j:
89
93
  # self loops are not allowed
90
94
  raise ValueError("Self loops are not allowed in ShortestPathModel")
@@ -97,8 +101,8 @@ class ShortestPathModel(EdgeMixin, ConstrainedQuadraticModel):
97
101
  j_index = nodes.index(j)
98
102
  _cons[i_index, l] = 1
99
103
  _cons[j_index, l] = -1
100
- log.info("LHS shape %s RHS shape %s", _cons.shape, _rhs.shape)
101
- log.info("checksum %f min %f", np.sum(_cons), np.min(_cons))
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))
102
106
  assert np.sum(_rhs) == 0
103
107
  return _cons, np.squeeze(_rhs)
104
108
 
@@ -114,9 +118,10 @@ class ShortestPathModel(EdgeMixin, ConstrainedQuadraticModel):
114
118
  m, n = len(nodes), len(variables)
115
119
  _obj = [0 for i in range(n)]
116
120
  for index, name in enumerate(variables):
117
- i, j = name
118
- _obj[index] = v = G.get_edge_data(i, j)["weight"]
119
- assert not np.isnan(v), f"Got a NaN at {i, j}"
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}"
120
125
  J = np.zeros((n, n))
121
126
  return np.array(_obj), J
122
127
 
@@ -126,32 +131,60 @@ class ShortestPathModel(EdgeMixin, ConstrainedQuadraticModel):
126
131
  a dictionary with each edge described by key, value pairs.
127
132
 
128
133
  """
129
- variables = self.variables
134
+ variables = self.variables[:len(self.G.edges)]
135
+ # log.debug("Using variables %s", variables)
130
136
 
131
137
  lhs, rhs = self.constraints
132
- upper_thresh = max(solution)
138
+ log.debug("LHS shape %s RHS shape %s", lhs.shape, rhs.shape)
139
+ upper_thresh = max(solution[:len(variables)])
133
140
  lower_thresh = 0
141
+ got_path = None
134
142
  while upper_thresh - lower_thresh > 1e-6:
135
- log.info("Lower Value: %f Upper Value %f", lower_thresh, upper_thresh)
143
+ log.debug("Lower Value: %f Upper Value %f", lower_thresh, upper_thresh)
136
144
  thresh = (lower_thresh + upper_thresh) / 2
137
145
  nx_path = None
138
146
  G = nx.DiGraph()
139
147
  for (i, j), value in zip(variables, solution):
140
148
  if value > thresh:
141
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
142
153
  path = {}
143
154
  try:
144
155
  nx_path = nx.shortest_path(G, self.s, self.t)
145
- upper_thresh = thresh
146
156
  lower_thresh = thresh
157
+ got_path = nx_path
158
+ log.debug("Got path size %d", len(got_path))
147
159
  except (nx.exception.NodeNotFound, nx.NetworkXAlgorithmError) as err:
148
- lower_thresh = thresh
149
- if nx_path is None:
160
+ upper_thresh = thresh
161
+ if got_path is None:
150
162
  raise RuntimeError(f"Solution does not describe path from {self.s} to {self.t}")
151
- path = {}
152
- for i, v in enumerate(nx_path):
153
- path[nx_path[i-1]] = v
154
- if self.t in path:
155
- del path[self.t]
156
- return path
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
157
190
 
@@ -78,7 +78,6 @@ class ClassifierBase(QuadraticModel):
78
78
  self,
79
79
  sum_constraint=self._sum_constraint,
80
80
  relaxation_schedule=self.relaxation_schedule,
81
- solution_precision=None,
82
81
  num_samples=self.num_samples,
83
82
  )
84
83
 
@@ -68,7 +68,6 @@ class ClusteringBase(QuadraticModel):
68
68
  self,
69
69
  sum_constraint=self._sum_constraint,
70
70
  relaxation_schedule=self.relaxation_schedule,
71
- solution_precision=1,
72
71
  num_samples=self.num_samples,
73
72
  )
74
73
 
@@ -66,7 +66,6 @@ class DecompBase(QuadraticModel):
66
66
  response = solver.solve(
67
67
  self,
68
68
  relaxation_schedule=self.relaxation_schedule,
69
- solution_precision=1,
70
69
  sum_constraint=self._sum_constraint,
71
70
  num_samples=self.num_samples,
72
71
  )
@@ -56,7 +56,6 @@ class RegressorBase(QuadraticModel):
56
56
  self,
57
57
  sum_constraint=self._sum_constraint,
58
58
  relaxation_schedule=self.relaxation_schedule,
59
- solution_precision=1,
60
59
  num_samples=self.num_samples,
61
60
  )
62
61
 
@@ -2,7 +2,15 @@
2
2
  from .eqcdirect import Dirac3DirectSolver
3
3
  from .qciclient import (Dirac1CloudSolver, Dirac3CloudSolver, QciClientSolver,
4
4
  Dirac3IntegerCloudSolver, Dirac3ContinuousCloudSolver)
5
+ from .mip import MIPMixin
6
+
7
+ class Dirac3MIPCloudSolver(MIPMixin, Dirac3ContinuousCloudSolver):
8
+ pass
9
+
10
+ class Dirac3MIPDirectSolver(MIPMixin, Dirac3DirectSolver):
11
+ pass
5
12
 
6
13
  __all__ = ["Dirac3DirectSolver", "Dirac1CloudSolver", "Dirac3CloudSolver",
7
14
  "EqcDirectSolver", "QciClientSolver", "Dirac3IntegerCloudSolver",
8
- "Dirac3ContinuousCloudSolver"]
15
+ "Dirac3ContinuousCloudSolver", "MILPMixin",
16
+ "Dirac3MIPCloudSolver", "Dirac3MIPDirectSolver"]
@@ -1,8 +1,9 @@
1
1
  # (C) Quantum Computing Inc., 2025.
2
2
  import logging
3
3
  import numpy as np
4
+ from typing import Dict
4
5
  from eqc_direct.client import EqcClient
5
- from eqc_models.base.base import ModelSolver
6
+ from eqc_models.base.base import EqcModel, ModelSolver
6
7
  from eqc_models.base.results import SolutionResults
7
8
 
8
9
  log = logging.getLogger(name=__name__)
@@ -19,7 +20,6 @@ class Dirac3DirectSolver(ModelSolver):
19
20
  sum_constraint,
20
21
  relaxation_schedule=1,
21
22
  num_samples=1,
22
- solution_precision=None,
23
23
  mean_photon_number=None,
24
24
  quantum_fluctuation_coefficient=None,
25
25
  ):
@@ -30,7 +30,8 @@ class Dirac3DirectSolver(ModelSolver):
30
30
  for i in range(len(indices)):
31
31
  if max(indices[i]) > num_variables:
32
32
  num_variables = max(indices[i])
33
-
33
+ # add machine slacks
34
+ num_variables += model.machine_slacks
34
35
  print("Num variables:", num_variables)
35
36
 
36
37
  client = self.client
@@ -49,7 +50,6 @@ class Dirac3DirectSolver(ModelSolver):
49
50
  lock_id=lock_id,
50
51
  relaxation_schedule=relaxation_schedule,
51
52
  sum_constraint=sum_constraint,
52
- solution_precision=solution_precision,
53
53
  mean_photon_number=mean_photon_number,
54
54
  quantum_fluctuation_coefficient=quantum_fluctuation_coefficient,
55
55
  )
@@ -69,3 +69,8 @@ class Dirac3DirectSolver(ModelSolver):
69
69
  @property
70
70
  def client(self) -> EqcClient:
71
71
  return EqcClient(self.ip_addr, self.port, cert_file=self.cert_file)
72
+
73
+ def makeResults(self, model : EqcModel, response : Dict):
74
+ """ Builds the results object """
75
+
76
+ return SolutionResults.from_eqcdirect_response(model, response, self)
@@ -0,0 +1,115 @@
1
+ import logging
2
+ import numpy as np
3
+ from eqc_models.base.base import EqcModel
4
+ from eqc_models.base.binaries import make_binary_penalty
5
+ from eqc_models.base.polynomial import PolynomialModel
6
+
7
+ log = logging.getLogger(name=__name__)
8
+
9
+ class MIPMixin:
10
+ """
11
+ Implements a solve method which intercepts the operator and builds a new
12
+ model with added terms for restriction of discrete variables with an
13
+ upper bound of 1 to take on only values 0 or 1 at minima.
14
+
15
+ Following the submission of a new model with the added penalties,
16
+ the solutions are updated to exclude added slack variables.
17
+
18
+ This is only supported with continuous-capable devices.
19
+
20
+ """
21
+
22
+ def solve(self, model : EqcModel, *args, **kwargs):
23
+ if model.is_discrete is None:
24
+ raise ValueError("Model solved with an MIP solver must have certain variables labeled discrete")
25
+ elif len([b for b in model.is_discrete if b])==0:
26
+ raise ValueError("Model solved with an MIP solver must have certain variables labeled discrete")
27
+ if kwargs.get("sum_constraint", None) is None:
28
+ raise ValueError("sum_constraint must be specified for MIP model sampling")
29
+ # get a polynomial
30
+ poly = model.polynomial
31
+ if hasattr(poly.coefficients, "tolist"):
32
+ coefficients = poly.coefficients.tolist()
33
+ else:
34
+ coefficients = list(poly.coefficients)
35
+ indices = poly.indices
36
+ old_n = model.n
37
+ log.debug("Model coefficients %d", len(coefficients))
38
+ log.debug("Model indices %d", len(indices))
39
+ log.debug("Model size %d", old_n)
40
+ if "penalty_multiplier" in kwargs:
41
+ penalty_multiplier = kwargs["penalty_multiplier"]
42
+ del kwargs["penalty_multiplier"]
43
+ else:
44
+ penalty_multiplier = getattr(model, "penalty_multiplier", 1)
45
+ log.debug("Binary enforcement penalty multiplier %f", penalty_multiplier)
46
+ bin_slacks = []
47
+ offset = 0
48
+ variables = model.variables
49
+ addtl_upper_bound = []
50
+ for i, v in enumerate(model.variables):
51
+ if model.is_discrete[i]:
52
+ bin_slacks.append(f"bin_slacks_{v}")
53
+ penalty_coeff, penalty_indices, penalty_offset = make_binary_penalty(i+1, len(variables)+len(bin_slacks), penalty_multiplier=penalty_multiplier)
54
+ log.debug("Adding penalties coeff: %s indices %s offset %s", penalty_coeff, penalty_indices, penalty_offset)
55
+ coefficients += penalty_coeff
56
+ indices += penalty_indices
57
+ offset += penalty_offset
58
+ addtl_upper_bound.append(1)
59
+ assert len(coefficients) == len(indices)
60
+ variables = variables + bin_slacks
61
+ log.debug("New model binary penalty slacks: %s", bin_slacks)
62
+ new_model = PolynomialModel(coefficients, indices)
63
+ new_model.variables = variables
64
+ log.debug("New model variables: %s", variables)
65
+ new_model.upper_bound = np.array(model.upper_bound.tolist() + addtl_upper_bound)
66
+ new_model.machine_slacks = model.machine_slacks
67
+ coefficients, indices = new_model.H
68
+ log.debug("New model coefficients %d", len(coefficients))
69
+ log.debug("New model indices %d", len(indices))
70
+ log.debug("New model size: %d", new_model.n)
71
+ response = super().solve(new_model, *args, **kwargs)
72
+ # translate the response into results
73
+ results = self.makeResults(new_model, response)
74
+ log.debug(results)
75
+ # update the results to relect the original model
76
+ solutions = results.solutions
77
+ for i in range(len(solutions)):
78
+ log.debug("SolutionResults solution: %s", solutions[i])
79
+ if hasattr(model, "evaluateObjective"):
80
+ new_objectives = []
81
+ else:
82
+ new_objectives = None
83
+ if hasattr(model, "evaluatePenalties"):
84
+ new_penalties = []
85
+ else:
86
+ new_penalties = None
87
+ new_solutions = []
88
+ new_energies = []
89
+ num_vars = len(solutions[0]) - len(bin_slacks)
90
+ log.debug("Num_vars %f", num_vars)
91
+ machine_slacks = model.machine_slacks
92
+ for i, solution in enumerate(solutions):
93
+ log.debug("%d - Raw solution %s", i, solution)
94
+ new_sol = [v for v in solution[:num_vars]]
95
+ new_sol = np.array(new_sol)
96
+ if machine_slacks > 0:
97
+ new_sol[-machine_slacks:] = solution[-machine_slacks:]
98
+ log.debug("%d - New solution %s", i, new_sol)
99
+ if new_objectives is not None:
100
+ try:
101
+ new_objectives.append(model.evaluateObjective(new_sol))
102
+ except NotImplementedError as err:
103
+ pass
104
+ if new_penalties is not None:
105
+ try:
106
+ new_penalties.append(model.evaluatePenalties(new_sol))
107
+ except NotImplementedError as err:
108
+ pass
109
+ new_solutions.append(new_sol)
110
+ new_energies.append(model.evaluate(new_sol))
111
+ results.solutions = new_solutions
112
+ results.penalties = new_penalties
113
+ results.objectives = new_objectives
114
+ results.energies = new_energies
115
+ return results