eqc-models 0.9.9__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.
- eqc_models-0.10.0.data/platlib/compile_extensions.py +67 -0
- {eqc_models-0.9.9.data → eqc_models-0.10.0.data}/platlib/eqc_models/base/polyeval.c +127 -123
- {eqc_models-0.9.9.data → eqc_models-0.10.0.data}/platlib/eqc_models/base/polyeval.cpython-310-darwin.so +0 -0
- eqc_models-0.10.0.data/platlib/eqc_models/base.py +115 -0
- eqc_models-0.10.0.data/platlib/eqc_models/combinatorics/setcover.py +93 -0
- eqc_models-0.10.0.data/platlib/eqc_models/communitydetection.py +25 -0
- eqc_models-0.10.0.data/platlib/eqc_models/eqcdirectsolver.py +61 -0
- {eqc_models-0.9.9.data → eqc_models-0.10.0.data}/platlib/eqc_models/graph/base.py +28 -17
- eqc_models-0.10.0.data/platlib/eqc_models/graph/partition.py +148 -0
- eqc_models-0.10.0.data/platlib/eqc_models/graphs.py +28 -0
- eqc_models-0.10.0.data/platlib/eqc_models/maxcut.py +113 -0
- eqc_models-0.10.0.data/platlib/eqc_models/maxkcut.py +185 -0
- eqc_models-0.10.0.data/platlib/eqc_models/ml/classifierqboost.py +628 -0
- eqc_models-0.10.0.data/platlib/eqc_models/ml/cvqboost_hamiltonian.pyx +83 -0
- eqc_models-0.10.0.data/platlib/eqc_models/ml/cvqboost_hamiltonian_c_func.c +68 -0
- eqc_models-0.10.0.data/platlib/eqc_models/ml/cvqboost_hamiltonian_c_func.h +14 -0
- eqc_models-0.10.0.data/platlib/eqc_models/quadraticmodel.py +131 -0
- eqc_models-0.10.0.data/platlib/eqc_models/solvers/eqcdirect.py +160 -0
- {eqc_models-0.9.9.data → eqc_models-0.10.0.data}/platlib/eqc_models/utilities/polynomial.py +11 -0
- {eqc_models-0.9.9.dist-info → eqc_models-0.10.0.dist-info}/METADATA +2 -1
- eqc_models-0.10.0.dist-info/RECORD +65 -0
- eqc_models-0.9.9.data/platlib/compile_extensions.py +0 -23
- eqc_models-0.9.9.data/platlib/eqc_models/ml/classifierqboost.py +0 -423
- eqc_models-0.9.9.dist-info/RECORD +0 -52
- {eqc_models-0.9.9.data → eqc_models-0.10.0.data}/platlib/eqc_models/__init__.py +0 -0
- {eqc_models-0.9.9.data → eqc_models-0.10.0.data}/platlib/eqc_models/algorithms/__init__.py +0 -0
- {eqc_models-0.9.9.data → eqc_models-0.10.0.data}/platlib/eqc_models/algorithms/base.py +0 -0
- {eqc_models-0.9.9.data → eqc_models-0.10.0.data}/platlib/eqc_models/algorithms/penaltymultiplier.py +0 -0
- {eqc_models-0.9.9.data → eqc_models-0.10.0.data}/platlib/eqc_models/allocation/__init__.py +0 -0
- {eqc_models-0.9.9.data → eqc_models-0.10.0.data}/platlib/eqc_models/allocation/allocation.py +0 -0
- {eqc_models-0.9.9.data → eqc_models-0.10.0.data}/platlib/eqc_models/allocation/portbase.py +0 -0
- {eqc_models-0.9.9.data → eqc_models-0.10.0.data}/platlib/eqc_models/allocation/portmomentum.py +0 -0
- {eqc_models-0.9.9.data → eqc_models-0.10.0.data}/platlib/eqc_models/assignment/__init__.py +0 -0
- {eqc_models-0.9.9.data → eqc_models-0.10.0.data}/platlib/eqc_models/assignment/qap.py +0 -0
- {eqc_models-0.9.9.data → eqc_models-0.10.0.data}/platlib/eqc_models/assignment/setpartition.py +0 -0
- {eqc_models-0.9.9.data → eqc_models-0.10.0.data}/platlib/eqc_models/base/__init__.py +0 -0
- {eqc_models-0.9.9.data → eqc_models-0.10.0.data}/platlib/eqc_models/base/base.py +0 -0
- {eqc_models-0.9.9.data → eqc_models-0.10.0.data}/platlib/eqc_models/base/constraints.py +0 -0
- {eqc_models-0.9.9.data → eqc_models-0.10.0.data}/platlib/eqc_models/base/operators.py +0 -0
- {eqc_models-0.9.9.data → eqc_models-0.10.0.data}/platlib/eqc_models/base/polyeval.pyx +0 -0
- {eqc_models-0.9.9.data → eqc_models-0.10.0.data}/platlib/eqc_models/base/polynomial.py +0 -0
- {eqc_models-0.9.9.data → eqc_models-0.10.0.data}/platlib/eqc_models/base/quadratic.py +0 -0
- {eqc_models-0.9.9.data → eqc_models-0.10.0.data}/platlib/eqc_models/decoding.py +0 -0
- {eqc_models-0.9.9.data → eqc_models-0.10.0.data}/platlib/eqc_models/graph/__init__.py +0 -0
- {eqc_models-0.9.9.data → eqc_models-0.10.0.data}/platlib/eqc_models/graph/hypergraph.py +0 -0
- {eqc_models-0.9.9.data → eqc_models-0.10.0.data}/platlib/eqc_models/graph/maxcut.py +0 -0
- {eqc_models-0.9.9.data → eqc_models-0.10.0.data}/platlib/eqc_models/graph/maxkcut.py +0 -0
- {eqc_models-0.9.9.data → eqc_models-0.10.0.data}/platlib/eqc_models/ml/__init__.py +0 -0
- {eqc_models-0.9.9.data → eqc_models-0.10.0.data}/platlib/eqc_models/ml/classifierbase.py +0 -0
- {eqc_models-0.9.9.data → eqc_models-0.10.0.data}/platlib/eqc_models/ml/classifierqsvm.py +0 -0
- {eqc_models-0.9.9.data → eqc_models-0.10.0.data}/platlib/eqc_models/ml/clustering.py +0 -0
- {eqc_models-0.9.9.data → eqc_models-0.10.0.data}/platlib/eqc_models/ml/clusteringbase.py +0 -0
- {eqc_models-0.9.9.data → eqc_models-0.10.0.data}/platlib/eqc_models/ml/decomposition.py +0 -0
- {eqc_models-0.9.9.data → eqc_models-0.10.0.data}/platlib/eqc_models/ml/forecast.py +0 -0
- {eqc_models-0.9.9.data → eqc_models-0.10.0.data}/platlib/eqc_models/ml/forecastbase.py +0 -0
- {eqc_models-0.9.9.data → eqc_models-0.10.0.data}/platlib/eqc_models/ml/regressor.py +0 -0
- {eqc_models-0.9.9.data → eqc_models-0.10.0.data}/platlib/eqc_models/ml/regressorbase.py +0 -0
- {eqc_models-0.9.9.data → eqc_models-0.10.0.data}/platlib/eqc_models/ml/reservoir.py +0 -0
- {eqc_models-0.9.9.data → eqc_models-0.10.0.data}/platlib/eqc_models/sequence/__init__.py +0 -0
- {eqc_models-0.9.9.data → eqc_models-0.10.0.data}/platlib/eqc_models/sequence/tsp.py +0 -0
- {eqc_models-0.9.9.data → eqc_models-0.10.0.data}/platlib/eqc_models/solvers/__init__.py +0 -0
- {eqc_models-0.9.9.data → eqc_models-0.10.0.data}/platlib/eqc_models/solvers/qciclient.py +0 -0
- {eqc_models-0.9.9.data → eqc_models-0.10.0.data}/platlib/eqc_models/utilities/__init__.py +0 -0
- {eqc_models-0.9.9.data → eqc_models-0.10.0.data}/platlib/eqc_models/utilities/fileio.py +0 -0
- {eqc_models-0.9.9.data → eqc_models-0.10.0.data}/platlib/eqc_models/utilities/qplib.py +0 -0
- {eqc_models-0.9.9.dist-info → eqc_models-0.10.0.dist-info}/LICENSE.txt +0 -0
- {eqc_models-0.9.9.dist-info → eqc_models-0.10.0.dist-info}/WHEEL +0 -0
- {eqc_models-0.9.9.dist-info → eqc_models-0.10.0.dist-info}/top_level.txt +0 -0
|
Binary file
|
|
@@ -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
|
-
|
|
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
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import networkx as nx
|
|
2
|
+
import numpy as np
|
|
3
|
+
from .graphs import TwoPartitionModel
|
|
4
|
+
|
|
5
|
+
class MaxCutModel(TwoPartitionModel):
|
|
6
|
+
|
|
7
|
+
def build(self):
|
|
8
|
+
variables = self.variables
|
|
9
|
+
n = len(variables)
|
|
10
|
+
self.domains = np.ones((n,))
|
|
11
|
+
|
|
12
|
+
J = np.zeros((n+1, n+1), dtype=np.float32)
|
|
13
|
+
h = np.zeros((n+1,1), dtype=np.float32)
|
|
14
|
+
for u, v in G.edges:
|
|
15
|
+
J[u, v] += 1
|
|
16
|
+
J[v, u] += 1
|
|
17
|
+
J[u, u] = 1
|
|
18
|
+
J[v, v] = 1
|
|
19
|
+
h[u] -= 1
|
|
20
|
+
h[v] -= 1
|
|
21
|
+
J *= 1/t**2
|
|
22
|
+
h *= 1/t
|
|
23
|
+
H = np.hstack([h, J])
|
|
24
|
+
return H
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def J(self) -> np.ndarray:
|
|
28
|
+
if getattr(self, "_J", None) is None:
|
|
29
|
+
self.build()
|
|
30
|
+
return self._J
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def C(self) -> np.ndarray:
|
|
34
|
+
if getattr(self, "C", None) is None:
|
|
35
|
+
self.build()
|
|
36
|
+
return self._C
|
|
37
|
+
|
|
38
|
+
def get_graph(n, d):
|
|
39
|
+
""" Produce a repeatable graph with parameters n and d """
|
|
40
|
+
|
|
41
|
+
seed = n * d
|
|
42
|
+
return nx.random_graphs.random_regular_graph(d, n, seed)
|
|
43
|
+
|
|
44
|
+
def get_partition_graph(G, solution):
|
|
45
|
+
"""
|
|
46
|
+
Build the partitioned graph, counting cut size
|
|
47
|
+
|
|
48
|
+
:parameters: G : nx.DiGraph, solution : np.ndarray
|
|
49
|
+
:returns: nx.DiGraph, int
|
|
50
|
+
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
cut_size = 0
|
|
54
|
+
Gprime = nx.DiGraph()
|
|
55
|
+
Gprime.add_nodes_from(G.nodes)
|
|
56
|
+
for i, j in G.edges:
|
|
57
|
+
if solution[i] != solution[j]:
|
|
58
|
+
cut_size+=1
|
|
59
|
+
else:
|
|
60
|
+
Gprime.add_edge(i, j)
|
|
61
|
+
return Gprime, cut_size
|
|
62
|
+
|
|
63
|
+
def determine_solution(G, solution):
|
|
64
|
+
"""
|
|
65
|
+
Use a simple bisection method to determine the binary solution. Uses
|
|
66
|
+
the cut size as the metric.
|
|
67
|
+
|
|
68
|
+
Returns the partitioned graph and solution.
|
|
69
|
+
|
|
70
|
+
:parameters: G : nx.DiGraph, solution : np.ndarray
|
|
71
|
+
:returns: nx.DiGraph, np.ndarray
|
|
72
|
+
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
solution = np.array(solution)
|
|
76
|
+
lower = np.min(solution)
|
|
77
|
+
upper = np.max(solution)
|
|
78
|
+
best_cut_size = 0
|
|
79
|
+
best_graph = G
|
|
80
|
+
best_solution = None
|
|
81
|
+
while upper > lower + 0.0001:
|
|
82
|
+
middle = (lower + upper) / 2
|
|
83
|
+
test_solution = (solution>=middle).astype(np.int32)
|
|
84
|
+
Gprime, cut_size = get_partition_graph(G, test_solution)
|
|
85
|
+
if cut_size > best_cut_size:
|
|
86
|
+
best_cut_size = cut_size
|
|
87
|
+
lower = middle
|
|
88
|
+
best_solution = test_solution
|
|
89
|
+
best_graph = Gprime
|
|
90
|
+
else:
|
|
91
|
+
upper = middle
|
|
92
|
+
return best_graph, best_solution
|
|
93
|
+
|
|
94
|
+
def get_maxcut_H(G, t):
|
|
95
|
+
"""
|
|
96
|
+
Return a Hamiltonian representing the Maximum Cut Problem. Scale the problem using `t`.
|
|
97
|
+
Automatically adds a slack qudit.
|
|
98
|
+
|
|
99
|
+
"""
|
|
100
|
+
n = len(G.nodes)
|
|
101
|
+
J = np.zeros((n+1, n+1), dtype=np.float32)
|
|
102
|
+
h = np.zeros((n+1,1), dtype=np.float32)
|
|
103
|
+
for u, v in G.edges:
|
|
104
|
+
J[u, v] += 1
|
|
105
|
+
J[v, u] += 1
|
|
106
|
+
J[u, u] = 1
|
|
107
|
+
J[v, v] = 1
|
|
108
|
+
h[u] -= 1
|
|
109
|
+
h[v] -= 1
|
|
110
|
+
J *= 1/t**2
|
|
111
|
+
h *= 1/t
|
|
112
|
+
H = np.hstack([h, J])
|
|
113
|
+
return H
|