eqc-models 0.9.8__py3-none-any.whl → 0.10.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 (68) hide show
  1. eqc_models-0.10.0.data/platlib/compile_extensions.py +67 -0
  2. {eqc_models-0.9.8.data → eqc_models-0.10.0.data}/platlib/eqc_models/assignment/setpartition.py +8 -29
  3. {eqc_models-0.9.8.data → eqc_models-0.10.0.data}/platlib/eqc_models/base/polyeval.c +127 -123
  4. {eqc_models-0.9.8.data → eqc_models-0.10.0.data}/platlib/eqc_models/base/polyeval.cpython-310-darwin.so +0 -0
  5. {eqc_models-0.9.8.data → eqc_models-0.10.0.data}/platlib/eqc_models/base/polynomial.py +84 -1
  6. eqc_models-0.10.0.data/platlib/eqc_models/base.py +115 -0
  7. eqc_models-0.10.0.data/platlib/eqc_models/combinatorics/setcover.py +93 -0
  8. eqc_models-0.10.0.data/platlib/eqc_models/communitydetection.py +25 -0
  9. eqc_models-0.10.0.data/platlib/eqc_models/eqcdirectsolver.py +61 -0
  10. {eqc_models-0.9.8.data → eqc_models-0.10.0.data}/platlib/eqc_models/graph/base.py +28 -17
  11. eqc_models-0.10.0.data/platlib/eqc_models/graph/partition.py +148 -0
  12. eqc_models-0.10.0.data/platlib/eqc_models/graphs.py +28 -0
  13. eqc_models-0.10.0.data/platlib/eqc_models/maxcut.py +113 -0
  14. eqc_models-0.10.0.data/platlib/eqc_models/maxkcut.py +185 -0
  15. eqc_models-0.10.0.data/platlib/eqc_models/ml/classifierqboost.py +628 -0
  16. eqc_models-0.10.0.data/platlib/eqc_models/ml/cvqboost_hamiltonian.pyx +83 -0
  17. eqc_models-0.10.0.data/platlib/eqc_models/ml/cvqboost_hamiltonian_c_func.c +68 -0
  18. eqc_models-0.10.0.data/platlib/eqc_models/ml/cvqboost_hamiltonian_c_func.h +14 -0
  19. eqc_models-0.10.0.data/platlib/eqc_models/quadraticmodel.py +131 -0
  20. {eqc_models-0.9.8.data → eqc_models-0.10.0.data}/platlib/eqc_models/sequence/tsp.py +38 -34
  21. eqc_models-0.10.0.data/platlib/eqc_models/solvers/eqcdirect.py +160 -0
  22. {eqc_models-0.9.8.data → eqc_models-0.10.0.data}/platlib/eqc_models/solvers/qciclient.py +46 -11
  23. {eqc_models-0.9.8.data → eqc_models-0.10.0.data}/platlib/eqc_models/utilities/polynomial.py +11 -0
  24. {eqc_models-0.9.8.dist-info → eqc_models-0.10.0.dist-info}/METADATA +3 -2
  25. eqc_models-0.10.0.dist-info/RECORD +65 -0
  26. {eqc_models-0.9.8.dist-info → eqc_models-0.10.0.dist-info}/WHEEL +1 -1
  27. eqc_models-0.9.8.data/platlib/compile_extensions.py +0 -23
  28. eqc_models-0.9.8.data/platlib/eqc_models/ml/classifierqboost.py +0 -423
  29. eqc_models-0.9.8.dist-info/RECORD +0 -52
  30. {eqc_models-0.9.8.data → eqc_models-0.10.0.data}/platlib/eqc_models/__init__.py +0 -0
  31. {eqc_models-0.9.8.data → eqc_models-0.10.0.data}/platlib/eqc_models/algorithms/__init__.py +0 -0
  32. {eqc_models-0.9.8.data → eqc_models-0.10.0.data}/platlib/eqc_models/algorithms/base.py +0 -0
  33. {eqc_models-0.9.8.data → eqc_models-0.10.0.data}/platlib/eqc_models/algorithms/penaltymultiplier.py +0 -0
  34. {eqc_models-0.9.8.data → eqc_models-0.10.0.data}/platlib/eqc_models/allocation/__init__.py +0 -0
  35. {eqc_models-0.9.8.data → eqc_models-0.10.0.data}/platlib/eqc_models/allocation/allocation.py +0 -0
  36. {eqc_models-0.9.8.data → eqc_models-0.10.0.data}/platlib/eqc_models/allocation/portbase.py +0 -0
  37. {eqc_models-0.9.8.data → eqc_models-0.10.0.data}/platlib/eqc_models/allocation/portmomentum.py +0 -0
  38. {eqc_models-0.9.8.data → eqc_models-0.10.0.data}/platlib/eqc_models/assignment/__init__.py +0 -0
  39. {eqc_models-0.9.8.data → eqc_models-0.10.0.data}/platlib/eqc_models/assignment/qap.py +0 -0
  40. {eqc_models-0.9.8.data → eqc_models-0.10.0.data}/platlib/eqc_models/base/__init__.py +0 -0
  41. {eqc_models-0.9.8.data → eqc_models-0.10.0.data}/platlib/eqc_models/base/base.py +0 -0
  42. {eqc_models-0.9.8.data → eqc_models-0.10.0.data}/platlib/eqc_models/base/constraints.py +0 -0
  43. {eqc_models-0.9.8.data → eqc_models-0.10.0.data}/platlib/eqc_models/base/operators.py +0 -0
  44. {eqc_models-0.9.8.data → eqc_models-0.10.0.data}/platlib/eqc_models/base/polyeval.pyx +0 -0
  45. {eqc_models-0.9.8.data → eqc_models-0.10.0.data}/platlib/eqc_models/base/quadratic.py +0 -0
  46. {eqc_models-0.9.8.data → eqc_models-0.10.0.data}/platlib/eqc_models/decoding.py +0 -0
  47. {eqc_models-0.9.8.data → eqc_models-0.10.0.data}/platlib/eqc_models/graph/__init__.py +0 -0
  48. {eqc_models-0.9.8.data → eqc_models-0.10.0.data}/platlib/eqc_models/graph/hypergraph.py +0 -0
  49. {eqc_models-0.9.8.data → eqc_models-0.10.0.data}/platlib/eqc_models/graph/maxcut.py +0 -0
  50. {eqc_models-0.9.8.data → eqc_models-0.10.0.data}/platlib/eqc_models/graph/maxkcut.py +0 -0
  51. {eqc_models-0.9.8.data → eqc_models-0.10.0.data}/platlib/eqc_models/ml/__init__.py +0 -0
  52. {eqc_models-0.9.8.data → eqc_models-0.10.0.data}/platlib/eqc_models/ml/classifierbase.py +0 -0
  53. {eqc_models-0.9.8.data → eqc_models-0.10.0.data}/platlib/eqc_models/ml/classifierqsvm.py +0 -0
  54. {eqc_models-0.9.8.data → eqc_models-0.10.0.data}/platlib/eqc_models/ml/clustering.py +0 -0
  55. {eqc_models-0.9.8.data → eqc_models-0.10.0.data}/platlib/eqc_models/ml/clusteringbase.py +0 -0
  56. {eqc_models-0.9.8.data → eqc_models-0.10.0.data}/platlib/eqc_models/ml/decomposition.py +0 -0
  57. {eqc_models-0.9.8.data → eqc_models-0.10.0.data}/platlib/eqc_models/ml/forecast.py +0 -0
  58. {eqc_models-0.9.8.data → eqc_models-0.10.0.data}/platlib/eqc_models/ml/forecastbase.py +0 -0
  59. {eqc_models-0.9.8.data → eqc_models-0.10.0.data}/platlib/eqc_models/ml/regressor.py +0 -0
  60. {eqc_models-0.9.8.data → eqc_models-0.10.0.data}/platlib/eqc_models/ml/regressorbase.py +0 -0
  61. {eqc_models-0.9.8.data → eqc_models-0.10.0.data}/platlib/eqc_models/ml/reservoir.py +0 -0
  62. {eqc_models-0.9.8.data → eqc_models-0.10.0.data}/platlib/eqc_models/sequence/__init__.py +0 -0
  63. {eqc_models-0.9.8.data → eqc_models-0.10.0.data}/platlib/eqc_models/solvers/__init__.py +0 -0
  64. {eqc_models-0.9.8.data → eqc_models-0.10.0.data}/platlib/eqc_models/utilities/__init__.py +0 -0
  65. {eqc_models-0.9.8.data → eqc_models-0.10.0.data}/platlib/eqc_models/utilities/fileio.py +0 -0
  66. {eqc_models-0.9.8.data → eqc_models-0.10.0.data}/platlib/eqc_models/utilities/qplib.py +0 -0
  67. {eqc_models-0.9.8.dist-info → eqc_models-0.10.0.dist-info}/LICENSE.txt +0 -0
  68. {eqc_models-0.9.8.dist-info → eqc_models-0.10.0.dist-info}/top_level.txt +0 -0
@@ -2,7 +2,7 @@
2
2
  from typing import Tuple, Union, List
3
3
  import numpy as np
4
4
  from eqc_models.base.base import EqcModel
5
- from eqc_models.base.operators import Polynomial
5
+ from eqc_models.base.operators import Polynomial, QUBO, OperatorNotAvailableError
6
6
  from eqc_models.base.constraints import ConstraintsMixIn
7
7
 
8
8
  class PolynomialMixin:
@@ -114,6 +114,89 @@ class PolynomialModel(PolynomialMixin, EqcModel):
114
114
  def polynomial(self) -> Polynomial:
115
115
  coefficients, indices = self.H
116
116
  return Polynomial(coefficients=coefficients, indices=indices)
117
+
118
+ @property
119
+ def qubo(self) -> QUBO:
120
+ try:
121
+ if np.all([len(self.polynomial.indices[i]) == 2 for i in range(len(self.polynomial.indices))]):
122
+ bin_n = 0
123
+ bits = []
124
+ C, J = self._quadratic_polynomial_to_qubo_coefficients(self.polynomial.coefficients,
125
+ self.polynomial.indices, self.n)
126
+ # upper_bound is an array of the maximum values each variable can take
127
+ upper_bound = self.upper_bound
128
+ if np.sum(upper_bound) != upper_bound.shape[0]:
129
+ for i in range(upper_bound.shape[0]):
130
+ bits.append(1 + np.floor(np.log2(upper_bound[i])))
131
+ bin_n += bits[-1]
132
+ bin_n = int(bin_n)
133
+ Q = np.zeros((bin_n, bin_n), dtype=np.float32)
134
+ powers = [2 ** np.arange(bit_count) for bit_count in bits]
135
+ blocks = []
136
+ linear_blocks = []
137
+ for i in range(len(powers)):
138
+ # add the linear terms to the diagonal
139
+ linear_blocks.append(C[i] * powers[i])
140
+ row = []
141
+ for j in range(len(powers)):
142
+ mult = J[i, j]
143
+ block = np.outer(powers[i], powers[j])
144
+ block *= mult
145
+ row.append(block)
146
+ blocks.append(row)
147
+ Q[:, :] = np.block(blocks)
148
+ linear_operator = np.hstack(linear_blocks)
149
+ Q += np.diag(linear_operator)
150
+ else:
151
+ # in this case, the fomulation already has only binary variables
152
+ Q = np.zeros_like(J)
153
+ Q[:, :] = J
154
+ Q += np.diag(np.squeeze(C))
155
+
156
+ return QUBO(Q)
157
+ else:
158
+ raise OperatorNotAvailableError("QUBO operator not available")
159
+ except OperatorNotAvailableError as e:
160
+ print(e)
161
+
162
+ def _quadratic_polynomial_to_qubo_coefficients(self, coefficients, indices, num_variables):
163
+ """
164
+ Transform polynomial into linear and quadratic qubo coefficient arrays.
165
+
166
+ Parameters
167
+ ----------
168
+ coefficients : List[float]
169
+ Coefficients of the polynomial terms, sorted according to the assumed format.
170
+ indices : List[Tuple[int, int]]
171
+ Sorted list of variable indices for the polynomial terms.
172
+ num_variables : int
173
+ Total number of variables in the polynomial.
174
+
175
+ Returns
176
+ -------
177
+ linear_coefficients : np.ndarray
178
+ 1D array of linear coefficients.
179
+ quadratic_coefficients : np.ndarray
180
+ 2D array of quadratic coefficients.
181
+ """
182
+ # Initialize arrays
183
+ linear_coefficients = np.zeros(num_variables, dtype=np.float32)
184
+ quadratic_coefficients = np.zeros((num_variables, num_variables), dtype=np.float32)
185
+
186
+ # Populate arrays
187
+ for coeff, index in zip(coefficients, indices):
188
+ if index[0] == 0: # Linear term
189
+ linear_coefficients[index[1] - 1] += coeff
190
+ else: # Quadratic term
191
+ i, j = index
192
+ if i == j:
193
+ quadratic_coefficients[i - 1, j - 1] += coeff
194
+ elif i != j: # Symmetric terms
195
+ quadratic_coefficients[i - 1, j - 1] += coeff / 2
196
+ quadratic_coefficients[j - 1, i - 1] += coeff / 2
197
+
198
+ return linear_coefficients, quadratic_coefficients
199
+
117
200
 
118
201
  class ConstrainedPolynomialModel(ConstraintsMixIn, PolynomialModel):
119
202
  """
@@ -0,0 +1,115 @@
1
+ import os
2
+ import logging
3
+ from typing import (Dict, List, Tuple)
4
+ import numpy as np
5
+ from eqc_direct.client import EqcClient
6
+
7
+ log = logging.getLogger(name=__name__)
8
+
9
+ # base class
10
+ class EqcModel:
11
+ """ EqcModel subclasses must provide these properties/methods.
12
+
13
+ :decode: takes a raw solution and translates it into the original problem
14
+ formulation
15
+ :H: property which returns a Hamiltonian operator
16
+ :levels: property to set the number of levels in each qudit
17
+ :qudit_limits: maximjm value permitted for each qudit """
18
+
19
+ _levels = 100
20
+ _domains = None
21
+ _H = None
22
+ _machine_slacks = 0
23
+
24
+ def decode(self, solution : np.ndarray) -> np.ndarray:
25
+ """ Interpret the solution given the norm value and domains """
26
+
27
+ # ignore any slacks that may have been added during encoding
28
+ solution = solution[:self.n]
29
+ if self._domains is not None:
30
+ multipliers = self.domains / self.sum_constraint
31
+ else:
32
+ multipliers = self.sum_constraint / np.sum(solution)
33
+
34
+ return multipliers * solution
35
+
36
+ def encode(self, norm_value:float=1000) -> np.ndarray:
37
+ """ Encode Hamiltonian into the domain of the device """
38
+
39
+ raise NotImplementedError()
40
+
41
+ def encode_sum_constraint(self, levels):
42
+ new_sc = self.n * (levels-1) * self.sum_constraint / (np.sum(self.domains))
43
+ return new_sc
44
+
45
+ @property
46
+ def domains(self) -> np.array:
47
+ return self._domains
48
+
49
+ @domains.setter
50
+ def domains(self, value):
51
+ self._domains = value
52
+
53
+ @property
54
+ def n(self) -> int:
55
+ return int(max(self.domains.shape))
56
+
57
+ def processH(self, H : np.ndarray) -> np.ndarray:
58
+ """ By default, do nothing to H """
59
+
60
+ return H
61
+
62
+ @property
63
+ def H(self) -> Dict[str, np.ndarray]:
64
+ """ Matrix of a quadratic operator with the first column containing
65
+ the linear terms and the remaining columns containing a symmetric
66
+ quadratic matrix"""
67
+ return self._H
68
+
69
+ @H.setter
70
+ def H(self, value : Dict[str, np.ndarray]):
71
+ """ The H setter ensures that matrices order 2 and above are symmetric """
72
+
73
+ H = self.processH(value)
74
+ self._H = H
75
+
76
+ @property
77
+ def sparse(self) -> Tuple[np.ndarray, np.ndarray]:
78
+ H = self.H
79
+ coeff = []
80
+ idx = []
81
+ poly_orders = {"C": 1, "J": 2, "T": 3, "Q": 4, "P": 5}
82
+ key_len = max([poly_orders[k] for k, v in H.items() if v is not None])
83
+
84
+
85
+ @property
86
+ def machine_slacks(self):
87
+ """ Number of slack qudits to add to the model """
88
+ return self._machine_slacks
89
+
90
+ @machine_slacks.setter
91
+ def machine_slacks(self, value:int):
92
+ assert int(value) == value, "value not integer"
93
+ self._machine_slacks = value
94
+
95
+ class ModelSolver:
96
+ """ Provide a common interface for solver implementations.
97
+ Store a model, implement a solve method."""
98
+
99
+ def __init__(self, model : EqcModel, levels : int = 200):
100
+ self.model = model
101
+ self._levels = levels
102
+
103
+ def solve(self, *args, **kwargs) -> Dict:
104
+ raise NotImplementedError()
105
+
106
+ @property
107
+ def levels(self) -> int:
108
+ """ This integer value indicates the number of distinct
109
+ states each qudit can represent. These levels are separated
110
+ by some constant value with the first level taking the value 0. """
111
+ return self._levels
112
+
113
+ @levels.setter
114
+ def levels(self, value : int):
115
+ self._levels = value
@@ -0,0 +1,93 @@
1
+ r"""
2
+ SetCoverModel solves the mathematical programming problem
3
+
4
+ $$
5
+ \mathrm{minimize}_x \sum_{x_i:X_i \in X} c_i x_i
6
+ $$
7
+
8
+ Subject to
9
+
10
+ $$
11
+ \sum_{i:a\in X_i} x_j \geq 1 \, \forall a \in A}
12
+ $$$
13
+
14
+ and
15
+
16
+ $$
17
+ x_i \in \{0, 1\} \forall {x_i: X_i \in X}
18
+ $$
19
+
20
+ Where $S$ is a set of all elements, $X$ is a collection of sets $X_i$, and the union of all is equal to $S$.
21
+
22
+ """
23
+
24
+ from typing import List
25
+ import numpy as np
26
+ from eqc_models.base import ConstrainedQuadraticModel
27
+
28
+ class SetCoverModel(ConstrainedQuadraticModel):
29
+ """
30
+ Parameters
31
+ -------------
32
+
33
+ subsets : List
34
+ List of sets where the union of all sets is S
35
+
36
+ weights : List
37
+ List of weights where each weight is the cost of choosing the subset
38
+ corresponding to the index of the weight.
39
+
40
+ >>> X = [set(['A', 'B']), set(['B', 'C']), set(['C'])]
41
+ >>> weights = [2, 2, 1]
42
+ >>> model = SetCoverModel(X, weights)
43
+ >>> model.penalty_multiplier = 2
44
+ >>> from eqc_models.solvers import Dirac3IntegerCloudSolver
45
+ >>> solver = Dirac3IntegerCloudSolver()
46
+ >>> response = solver.solve(model, relaxation_schedule=1, num_samples=5) #doctest: +ELLIPSIS
47
+ 20...
48
+ >>> solutions = response["results"]["solutions"]
49
+ >>> solutions[0]
50
+ [1, 0, 1, 0, 0, 0]
51
+ >>> model.decode(solutions[0])
52
+ [{'B', 'A'}, {'C'}]
53
+
54
+ """
55
+
56
+ def __init__(self, subsets, weights):
57
+ # ensure that X is ordered
58
+ self.X = X = list(subsets)
59
+ self.S = S = set()
60
+
61
+ for x in subsets:
62
+ S = S.union(x)
63
+ # elements is sorted to maintain consistent output
64
+ elements = [a for a in S]
65
+ elements.sort()
66
+ # constraints
67
+ A = []
68
+ b = []
69
+ variables = [f'x_{i}' for i in range(len(X))]
70
+ pos = 0
71
+ for a in elements:
72
+ variables.append(f"s_{pos}")
73
+ constraint = [1 if a in X[i] else 0 for i in range(len(X))]
74
+ slacks = [0 for i in range(len(S))]
75
+ slacks[pos] = -1
76
+ A.append(constraint + slacks)
77
+ pos += 1
78
+ b.append(1)
79
+ n = len(variables)
80
+ J = np.zeros((n, n))
81
+ h = np.zeros((n, ))
82
+ h[:len(weights)] = weights
83
+ # call the superclass constructor with the objective and constraints
84
+ super(SetCoverModel, self).__init__(h, J, np.array(A), np.array(b))
85
+ # set upper bound on the variables to be 1 for x_i and the length of X minus 1 for the slacks
86
+ self.upper_bound = np.array([1 for i in range(len(weights))] + [len(X)-1 for i in range(n-len(weights))])
87
+
88
+ def decode(self, solution) -> List:
89
+ xbar = []
90
+ for i in range(len(self.X)):
91
+ if solution[i] > 0.5:
92
+ xbar.append(self.X[i])
93
+ return xbar
@@ -0,0 +1,25 @@
1
+ # (C) Quantum Computing Inc., 2024.
2
+ import numpy as np
3
+ import networkx as nx
4
+ from .graphs import GraphModel
5
+
6
+ class CommunityDetectionModel(GraphModel):
7
+ """
8
+ This model is the generic n-community model, which requires enforcing
9
+ membership to a single community
10
+
11
+ """
12
+
13
+ def __init__(self, G : nx.Graph, num_communities : int):
14
+ super(CommunityDetectionModel, self).__init__(G)
15
+ self.cnum_communities = num_communities
16
+
17
+ def build(self):
18
+ # num_nodes = len(self.G.nodes)
19
+ # num_variables = num_nodes * self.num_communities
20
+ # lhs = np.zeros((num_nodes, num_variables), dtype=np.int32)
21
+ # rhs = np.ones((num_nodes, 1), dtype=np.int32)
22
+ # for i in range(num_nodes):
23
+ # lhs[i, 3*i:3*(i+1)] = 1
24
+ # self.constraints = lhs, rhs
25
+ raise NotImplementedError("Community Detection is not implemented yet")
@@ -0,0 +1,61 @@
1
+ from typing import Dict
2
+ import logging
3
+ import numpy as np
4
+ from eqc_direct.client import EqcClient
5
+ from .base import ModelSolver
6
+
7
+ log = logging.getLogger(name=__name__)
8
+
9
+ class EqcDirectMixin:
10
+
11
+ ip_addr = None
12
+ port = None
13
+
14
+ def connect(self, ip_addr : str, port : str):
15
+ """ Explicitly set device address, if environment is configured with the connection, this call is not required """
16
+ self.ip_addr = ip_addr
17
+ self.port = port
18
+
19
+ @property
20
+ def client(self):
21
+
22
+ params = {}
23
+ if self.ip_addr is not None:
24
+ params["ip_address"] = self.ip_addr
25
+ if self.port is not None:
26
+ params["port"] = self.port
27
+ return EqcClient(**params)
28
+
29
+ class EqcDirectSolver(ModelSolver, EqcDirectMixin):
30
+
31
+ def solve(self, relaxation_schedule:int=2, precision : float = 1.0) -> Dict:
32
+ model = self.model
33
+ poly_coefficients, poly_indices = model.sparse
34
+ scval = model.encode_sum_constraint(self.levels)
35
+
36
+ client = self.client
37
+ lock_id, start_ts, end_ts = client.wait_for_lock()
38
+ log.debug("Got device lock id %s. Wait time %f", lock_id, end_ts - start_ts)
39
+ resp = None
40
+ try:
41
+ log.debug("Calling device with parameters relaxation_schedule %d sum_constraint %s lock_id %s solution_precision %f",
42
+ relaxation_schedule, scval, lock_id, precision)
43
+ resp = client.process_job(poly_coefficients=poly_coefficients,
44
+ poly_indices=poly_indices,
45
+ relaxation_schedule=relaxation_schedule,
46
+ sum_constraint = scval,
47
+ lock_id = lock_id,
48
+ solution_precision=precision)
49
+ log.debug("Received response with status %s", resp["err_desc"])
50
+ log.debug("Runtime %f resulting in energy %f", resp["runtime"], resp["energy"])
51
+ log.debug("Distillation runtime %s resulting in energy %f", resp["distilled_runtime"], resp["distilled_energy"])
52
+ finally:
53
+ client.release_lock(lock_id=lock_id)
54
+ if resp is not None:
55
+ solution = resp["solution"]
56
+ energy = resp["energy"]
57
+ runtime = resp["runtime"]
58
+ dirac3_sol = np.array(solution)
59
+ else:
60
+ raise RuntimeError("FAILED TO GET RESPONSE")
61
+ return resp
@@ -7,7 +7,34 @@ class GraphModel(QuadraticModel):
7
7
  """ """
8
8
  def __init__(self, G : nx.Graph):
9
9
  self.G = G
10
- self.linear_objective, self.quad_objective = self.costFunction()
10
+ super().__init__(*self.costFunction())
11
+
12
+ @property
13
+ def linear_objective(self):
14
+ """Return linear terms as a vector."""
15
+ return self._H[0]
16
+
17
+ @property
18
+ def quad_objective(self):
19
+ """Return quadratic terms as a matrix."""
20
+ return self._H[1]
21
+
22
+ def costFunction(self):
23
+ """
24
+ Parameters
25
+ -------------
26
+
27
+ None
28
+
29
+ Returns
30
+ --------------
31
+
32
+ :C: linear operator (vector array of coefficients) for cost function
33
+ :J: quadratic operator (N by N matrix array of coefficients ) for cost function
34
+
35
+ """
36
+ raise NotImplementedError("GraphModel does not implement costFunction")
37
+
11
38
 
12
39
  class NodeModel(GraphModel):
13
40
  """
@@ -22,22 +49,6 @@ class NodeModel(GraphModel):
22
49
  names = [node for node in self.G.nodes]
23
50
  names.sort()
24
51
  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
52
 
42
53
  def modularity(self, partition : Set[Set]) -> float:
43
54
  """ Calculate modularity from a partition (set of communities) """
@@ -0,0 +1,148 @@
1
+ from typing import Tuple
2
+ import numpy as np
3
+ import scipy.sparse as sp
4
+ import networkx as nx
5
+ from math import modf
6
+ from eqc_models.graph.base import GraphModel
7
+
8
+
9
+ class GraphPartitionModel(GraphModel):
10
+ """
11
+ A model for graph partitioning into `k` parts with objective and constraints
12
+ derived from the Laplacian matrix and additional penalties for balance and constraints.
13
+ """
14
+
15
+ def __init__(self, G: nx.Graph, k: int = 2, weight: str = "weight", alpha: float = 1.0, beta_obj: float = 1.0,
16
+ gamma: float = 1.0):
17
+ """
18
+ Parameters:
19
+ -----------
20
+ G : nx.Graph
21
+ The graph to partition.
22
+ k : int
23
+ The number of partitions.
24
+ weight : str
25
+ The key for edge weights in the graph.
26
+ alpha : float
27
+ The penalty multiplier for balance constraints.
28
+ beta_obj : float
29
+ The penalty multiplier for minimizing edge cuts (Laplacian term).
30
+ gamma : float
31
+ The penalty multiplier for assignment constraints.
32
+ """
33
+ self._G = G
34
+ self._k = k
35
+ self._weight = weight
36
+ self._alpha = alpha
37
+ self._beta_obj = beta_obj
38
+ self._gamma = gamma
39
+ self._laplacian = nx.laplacian_matrix(G, weight=weight)
40
+ self._num_nodes = G.number_of_nodes()
41
+ self._sorted_nodes = sorted(G.nodes)
42
+ self._constraints_offset = 0
43
+ self._balanced_partition_offset = 0
44
+ self.set_and_validate_k()
45
+ self._objective_matrix = self.initialize_model()
46
+ super().__init__(self._G)
47
+
48
+ def set_and_validate_k(self):
49
+ """
50
+ Sets k and encoding length for a graph problem
51
+ """
52
+ # modf(x) = (fractional, integer) decomposition.
53
+ # Make sure fractional portion is zero. Convert to int if so.
54
+ assert modf(self._k)[0] == 0, "'k' must be an integer."
55
+
56
+ # it's an int, so set self.k
57
+ self._k = int(self._k)
58
+
59
+ # Verify k >= 2
60
+ assert self._k >= 2, f"ERROR, k={self._k}: k must be greater than or equal to 2."
61
+
62
+ # Verify that k makes sense
63
+ assert self._k <= self._num_nodes, (
64
+ f"ERROR, k={self._k}: k must be less than number of nodes or variables. k = {self._k} and "
65
+ f"number of nodes = {self._num_nodes}"
66
+ )
67
+
68
+ def initialize_model(self):
69
+ """
70
+ Build the objective matrix and constraints for the k-partition problem.
71
+ """
72
+ if self._k == 2:
73
+ # For 2 partitions, construct a simpler QUBO from the Laplacian matrix
74
+ return self.get_two_partition_qubo()
75
+ else:
76
+ # For k > 2, construct a block-diagonal Laplacian with balance and constraints
77
+ laplacian_blocks = 0.5 * sp.block_diag([self._laplacian] * self._k, format="csr")
78
+ balance_term = self.get_balanced_partition_term()
79
+ constraints = self.get_constraints()
80
+ return (
81
+ self._alpha * balance_term
82
+ + self._gamma * constraints
83
+ + self._beta_obj * laplacian_blocks
84
+ )
85
+
86
+ def get_balanced_partition_term(self) -> sp.spmatrix:
87
+ """
88
+ Construct the quadratic penalty term for balanced partitions.
89
+ """
90
+ I_k = sp.identity(self._k)
91
+ Ones_n = np.ones((self._num_nodes, self._num_nodes))
92
+ balanced_partition_term = sp.kron(I_k, Ones_n, format="csr")
93
+ balanced_partition_term -= (
94
+ 2 * self._num_nodes / self._k * sp.identity(balanced_partition_term.shape[0])
95
+ )
96
+ self._balanced_partition_offset = self._num_nodes**2 / self._k
97
+ return balanced_partition_term
98
+
99
+ def get_constraints(self) -> sp.spmatrix:
100
+ """
101
+ Construct the quadratic penalty term for assignment constraints.
102
+ """
103
+ I_n = sp.identity(self._num_nodes)
104
+ Ones_k = np.ones((self._k, self._k))
105
+ constraints = sp.kron(Ones_k, I_n, format="csr")
106
+ constraints -= 2 * sp.identity(constraints.shape[0])
107
+ self._constraints_offset = self._num_nodes
108
+ return constraints
109
+
110
+ def get_two_partition_qubo(self) -> sp.spmatrix:
111
+ """
112
+ Construct the QUBO matrix for two partitions using adjacency and penalties.
113
+ """
114
+ Garr = nx.to_scipy_sparse_matrix(self._G, weight=self._weight, nodelist=self._sorted_nodes)
115
+ Q = (
116
+ self._alpha * np.ones(Garr.shape, dtype=np.float32)
117
+ - self._beta_obj * Garr
118
+ )
119
+ degrees = Garr.sum(axis=1).A1 # Convert sparse matrix to 1D array
120
+ diag = self._beta_obj * degrees - self._alpha * (self._num_nodes - 1)
121
+ np.fill_diagonal(Q, diag)
122
+ return sp.csr_matrix(Q)
123
+
124
+ def evaluate(self, solution: np.ndarray) -> float:
125
+ """
126
+ Evaluate the objective function for a given solution.
127
+ """
128
+ assert len(solution) == self._objective_matrix.shape[0], "Solution size mismatch."
129
+ return float(solution.T @ self._objective_matrix @ solution)
130
+
131
+ def decode(self, solution: np.ndarray) -> dict:
132
+ """
133
+ Decode the solution vector into a partition assignment.
134
+ """
135
+ if self._k == 2:
136
+ return {node: int(solution[i]) for i, node in enumerate(self._sorted_nodes)}
137
+ else:
138
+ partitions, nodes = np.where(solution.reshape((self._k, self._num_nodes)) == 1)
139
+ return {self._sorted_nodes[node]: int(partition) for partition, node in zip(partitions, nodes)}
140
+
141
+ def costFunction(self) -> Tuple[np.ndarray, np.ndarray]:
142
+ """
143
+ Return the linear and quadratic components of the objective function.
144
+ """
145
+ Q = self._objective_matrix
146
+ h = Q.diagonal()
147
+ J = 2 * sp.triu(Q, k=1).tocsr() # Extract upper triangular part for quadratic terms
148
+ return h, J
@@ -0,0 +1,28 @@
1
+ from typing import List
2
+ import networkx as nx
3
+ from .quadraticmodel import QuadraticModel
4
+
5
+ class GraphModel(QuadraticModel):
6
+
7
+ def __init__(self, G : nx.Graph):
8
+ self.G = G
9
+
10
+ class TwoPartitionModel(GraphModel):
11
+ """ Create a model where the variables are node-based """
12
+
13
+ @property
14
+ def variables(self) -> List[str]:
15
+ """ Provide a variable name to index lookup, order enforced by sorting the list before returning """
16
+ names = [node.name for node in self.G.nodes]
17
+ names.sort()
18
+ return names
19
+
20
+ class EdgeModel(GraphModel):
21
+ """ Create a model where the variables are edge-based """
22
+
23
+ @property
24
+ def variables(self) -> List[str]:
25
+ """ Provide a variable name to index lookup, order enforced by sorting the list before returning """
26
+ names = [f"({u},{v})" for u, v in self.G.edges]
27
+ names.sort()
28
+ return names