tsam 2.2.2__py3-none-any.whl → 2.3.4__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.
@@ -1,234 +1,239 @@
1
- # -*- coding: utf-8 -*-
2
-
3
- import numpy as np
4
-
5
- from sklearn.base import BaseEstimator, ClusterMixin, TransformerMixin
6
- from sklearn.metrics.pairwise import PAIRWISE_DISTANCE_FUNCTIONS
7
- from sklearn.utils import check_array
8
- import pyomo.environ as pyomo
9
- import pyomo.opt as opt
10
- from pyomo.contrib import appsi
11
-
12
-
13
- class KMedoids(BaseEstimator, ClusterMixin, TransformerMixin):
14
- """
15
- k-medoids class.
16
-
17
- :param n_clusters: How many medoids. Must be positive. optional, default: 8
18
- :type n_clusters: integer
19
-
20
- :param distance_metric: What distance metric to use. optional, default: 'euclidean'
21
- :type distance_metric: string
22
-
23
- :param timelimit: Specify the time limit of the solver. optional, default: 100
24
- :type timelimit: integer
25
-
26
- :param threads: Threads to use by the optimization solver. optional, default: 7
27
- :type threads: integer
28
-
29
- :param solver: Specifies the solver. optional, default: 'highs'
30
- :type solver: string
31
- """
32
-
33
- def __init__(
34
- self,
35
- n_clusters=8,
36
- distance_metric="euclidean",
37
- timelimit=100,
38
- threads=7,
39
- solver="highs",
40
- ):
41
-
42
- self.n_clusters = n_clusters
43
-
44
- self.distance_metric = distance_metric
45
-
46
- self.solver = solver
47
-
48
- self.timelimit = timelimit
49
-
50
- self.threads = threads
51
-
52
- def _check_init_args(self):
53
-
54
- # Check n_clusters
55
- if (
56
- self.n_clusters is None
57
- or self.n_clusters <= 0
58
- or not isinstance(self.n_clusters, int)
59
- ):
60
- raise ValueError("n_clusters has to be nonnegative integer")
61
-
62
- # Check distance_metric
63
- if callable(self.distance_metric):
64
- self.distance_func = self.distance_metric
65
- elif self.distance_metric in PAIRWISE_DISTANCE_FUNCTIONS:
66
- self.distance_func = PAIRWISE_DISTANCE_FUNCTIONS[self.distance_metric]
67
- else:
68
- raise ValueError(
69
- "distance_metric needs to be "
70
- + "callable or one of the "
71
- + "following strings: "
72
- + "{}".format(PAIRWISE_DISTANCE_FUNCTIONS.keys())
73
- + ". Instead, '{}' ".format(self.distance_metric)
74
- + "was given."
75
- )
76
-
77
- def fit(self, X, y=None):
78
- """Fit K-Medoids to the provided data.
79
-
80
- :param X: shape=(n_samples, n_features)
81
- :type X: array-like or sparse matrix
82
-
83
- :returns: self
84
- """
85
-
86
- self._check_init_args()
87
-
88
- # check that the array is good and attempt to convert it to
89
- # Numpy array if possible
90
- X = self._check_array(X)
91
-
92
- # apply distance metric to get the distance matrix
93
- D = self.distance_func(X)
94
-
95
- # run exact optimization
96
- r_y, r_x, best_inertia = self._k_medoids_exact(D, self.n_clusters)
97
-
98
- labels_raw = r_x.argmax(axis=0)
99
-
100
- count = 0
101
- translator = {}
102
- cluster_centers_ = []
103
- for ix, val in enumerate(r_y):
104
- if val > 0:
105
- translator[ix] = count
106
- cluster_centers_.append(X[ix])
107
- count += 1
108
- labels_ = []
109
- for label in labels_raw:
110
- labels_.append(translator[label])
111
-
112
- self.labels_ = labels_
113
- self.cluster_centers_ = cluster_centers_
114
-
115
- return self
116
-
117
- def _check_array(self, X):
118
-
119
- X = check_array(X)
120
-
121
- # Check that the number of clusters is less than or equal to
122
- # the number of samples
123
- if self.n_clusters > X.shape[0]:
124
- raise ValueError(
125
- "The number of medoids "
126
- + "({}) ".format(self.n_clusters)
127
- + "must be larger than the number "
128
- + "of samples ({})".format(X.shape[0])
129
- )
130
-
131
- return X
132
-
133
- def _k_medoids_exact(self, distances, n_clusters):
134
- """
135
- Parameters
136
- ----------
137
- distances : int, required
138
- Pairwise distances between each row.
139
- n_clusters : int, required
140
- Number of clusters.
141
- """
142
-
143
- # Create pyomo model
144
- M = _setup_k_medoids(distances, n_clusters)
145
-
146
- # And solve
147
- r_x, r_y, r_obj = _solve_given_pyomo_model(M, solver=self.solver)
148
-
149
- return (r_y, r_x.T, r_obj)
150
-
151
-
152
- def _setup_k_medoids(distances, n_clusters):
153
- """Define the k-medoids model with pyomo.
154
- In the spatial aggregation community, it is referred to as Hess Model for political districting
155
- with an additional constraint of cluster-sizes/populations.
156
- (W Hess, JB Weaver, HJ Siegfeldt, JN Whelan, and PA Zitlau. Nonpartisan political redistricting by computer. Operations Research, 13(6):998–1006, 1965.)
157
- """
158
- # Create model
159
- M = pyomo.ConcreteModel()
160
-
161
- # get distance matrix
162
- M.d = distances
163
-
164
- # set number of clusters
165
- M.no_k = n_clusters
166
-
167
- # Distances is a symmetrical matrix, extract its length
168
- length = distances.shape[0]
169
-
170
- # get indices
171
- M.i = [j for j in range(length)]
172
- M.j = [j for j in range(length)]
173
-
174
- # initialize vars
175
- # Decision every candidate to every possible other candidate as cluster center
176
- M.z = pyomo.Var(M.i, M.j, within=pyomo.Binary)
177
-
178
- # get objective
179
- # Minimize the distance of every candidate to the cluster center
180
- def objRule(M):
181
- return sum(sum(M.d[i, j] * M.z[i, j] for j in M.j) for i in M.i)
182
-
183
- M.obj = pyomo.Objective(rule=objRule)
184
-
185
- # s.t.
186
- # Assign all candidates to one clusters
187
- def candToClusterRule(M, j):
188
- return sum(M.z[i, j] for i in M.i) == 1
189
-
190
- M.candToClusterCon = pyomo.Constraint(M.j, rule=candToClusterRule)
191
-
192
- # Predefine the number of clusters
193
- def noClustersRule(M):
194
- return sum(M.z[i, i] for i in M.i) == M.no_k
195
-
196
- M.noClustersCon = pyomo.Constraint(rule=noClustersRule)
197
-
198
- # Describe the choice of a candidate to a cluster
199
- def clusterRelationRule(M, i, j):
200
- return M.z[i, j] <= M.z[i, i]
201
-
202
- M.clusterRelationCon = pyomo.Constraint(M.i, M.j, rule=clusterRelationRule)
203
- return M
204
-
205
-
206
- def _solve_given_pyomo_model(M, solver="highs"):
207
- """Solves a given pyomo model clustering model an returns the clusters
208
-
209
- Args:
210
- M (pyomo.ConcreteModel): Concrete model instance that gets solved.
211
- solver (str, optional): solver, defines the solver for the pyomo model. Defaults to "highs".
212
-
213
- Raises:
214
- ValueError: [description]
215
-
216
- Returns:
217
- [type]: [description]
218
- """
219
- # create optimization problem
220
- if solver == "highs":
221
- solver_instance = appsi.solvers.Highs()
222
- else:
223
- solver_instance = opt.SolverFactory(solver)
224
- results = solver_instance.solve(M)
225
- # check that it does not fail
226
-
227
- # Get results
228
- r_x = np.array([[round(M.z[i, j].value) for i in M.i] for j in M.j])
229
-
230
- r_y = np.array([round(M.z[j, j].value) for j in M.j])
231
-
232
- r_obj = pyomo.value(M.obj)
233
-
234
- return (r_x, r_y, r_obj)
1
+ # -*- coding: utf-8 -*-
2
+
3
+ import numpy as np
4
+
5
+ from sklearn.base import BaseEstimator, ClusterMixin, TransformerMixin
6
+ from sklearn.metrics.pairwise import PAIRWISE_DISTANCE_FUNCTIONS
7
+ from sklearn.utils import check_array
8
+
9
+ # switch to numpy 2.0
10
+ np.float_ = np.float64
11
+ np.complex_=np.complex128
12
+
13
+ import pyomo.environ as pyomo
14
+ import pyomo.opt as opt
15
+ from pyomo.contrib import appsi
16
+
17
+
18
+ class KMedoids(BaseEstimator, ClusterMixin, TransformerMixin):
19
+ """
20
+ k-medoids class.
21
+
22
+ :param n_clusters: How many medoids. Must be positive. optional, default: 8
23
+ :type n_clusters: integer
24
+
25
+ :param distance_metric: What distance metric to use. optional, default: 'euclidean'
26
+ :type distance_metric: string
27
+
28
+ :param timelimit: Specify the time limit of the solver. optional, default: 100
29
+ :type timelimit: integer
30
+
31
+ :param threads: Threads to use by the optimization solver. optional, default: 7
32
+ :type threads: integer
33
+
34
+ :param solver: Specifies the solver. optional, default: 'highs'
35
+ :type solver: string
36
+ """
37
+
38
+ def __init__(
39
+ self,
40
+ n_clusters=8,
41
+ distance_metric="euclidean",
42
+ timelimit=100,
43
+ threads=7,
44
+ solver="highs",
45
+ ):
46
+
47
+ self.n_clusters = n_clusters
48
+
49
+ self.distance_metric = distance_metric
50
+
51
+ self.solver = solver
52
+
53
+ self.timelimit = timelimit
54
+
55
+ self.threads = threads
56
+
57
+ def _check_init_args(self):
58
+
59
+ # Check n_clusters
60
+ if (
61
+ self.n_clusters is None
62
+ or self.n_clusters <= 0
63
+ or not isinstance(self.n_clusters, int)
64
+ ):
65
+ raise ValueError("n_clusters has to be nonnegative integer")
66
+
67
+ # Check distance_metric
68
+ if callable(self.distance_metric):
69
+ self.distance_func = self.distance_metric
70
+ elif self.distance_metric in PAIRWISE_DISTANCE_FUNCTIONS:
71
+ self.distance_func = PAIRWISE_DISTANCE_FUNCTIONS[self.distance_metric]
72
+ else:
73
+ raise ValueError(
74
+ "distance_metric needs to be "
75
+ + "callable or one of the "
76
+ + "following strings: "
77
+ + "{}".format(PAIRWISE_DISTANCE_FUNCTIONS.keys())
78
+ + ". Instead, '{}' ".format(self.distance_metric)
79
+ + "was given."
80
+ )
81
+
82
+ def fit(self, X, y=None):
83
+ """Fit K-Medoids to the provided data.
84
+
85
+ :param X: shape=(n_samples, n_features)
86
+ :type X: array-like or sparse matrix
87
+
88
+ :returns: self
89
+ """
90
+
91
+ self._check_init_args()
92
+
93
+ # check that the array is good and attempt to convert it to
94
+ # Numpy array if possible
95
+ X = self._check_array(X)
96
+
97
+ # apply distance metric to get the distance matrix
98
+ D = self.distance_func(X)
99
+
100
+ # run exact optimization
101
+ r_y, r_x, best_inertia = self._k_medoids_exact(D, self.n_clusters)
102
+
103
+ labels_raw = r_x.argmax(axis=0)
104
+
105
+ count = 0
106
+ translator = {}
107
+ cluster_centers_ = []
108
+ for ix, val in enumerate(r_y):
109
+ if val > 0:
110
+ translator[ix] = count
111
+ cluster_centers_.append(X[ix])
112
+ count += 1
113
+ labels_ = []
114
+ for label in labels_raw:
115
+ labels_.append(translator[label])
116
+
117
+ self.labels_ = labels_
118
+ self.cluster_centers_ = cluster_centers_
119
+
120
+ return self
121
+
122
+ def _check_array(self, X):
123
+
124
+ X = check_array(X)
125
+
126
+ # Check that the number of clusters is less than or equal to
127
+ # the number of samples
128
+ if self.n_clusters > X.shape[0]:
129
+ raise ValueError(
130
+ "The number of medoids "
131
+ + "({}) ".format(self.n_clusters)
132
+ + "must be larger than the number "
133
+ + "of samples ({})".format(X.shape[0])
134
+ )
135
+
136
+ return X
137
+
138
+ def _k_medoids_exact(self, distances, n_clusters):
139
+ """
140
+ Parameters
141
+ ----------
142
+ distances : int, required
143
+ Pairwise distances between each row.
144
+ n_clusters : int, required
145
+ Number of clusters.
146
+ """
147
+
148
+ # Create pyomo model
149
+ M = _setup_k_medoids(distances, n_clusters)
150
+
151
+ # And solve
152
+ r_x, r_y, r_obj = _solve_given_pyomo_model(M, solver=self.solver)
153
+
154
+ return (r_y, r_x.T, r_obj)
155
+
156
+
157
+ def _setup_k_medoids(distances, n_clusters):
158
+ """Define the k-medoids model with pyomo.
159
+ In the spatial aggregation community, it is referred to as Hess Model for political districting
160
+ with an additional constraint of cluster-sizes/populations.
161
+ (W Hess, JB Weaver, HJ Siegfeldt, JN Whelan, and PA Zitlau. Nonpartisan political redistricting by computer. Operations Research, 13(6):998–1006, 1965.)
162
+ """
163
+ # Create model
164
+ M = pyomo.ConcreteModel()
165
+
166
+ # get distance matrix
167
+ M.d = distances
168
+
169
+ # set number of clusters
170
+ M.no_k = n_clusters
171
+
172
+ # Distances is a symmetrical matrix, extract its length
173
+ length = distances.shape[0]
174
+
175
+ # get indices
176
+ M.i = [j for j in range(length)]
177
+ M.j = [j for j in range(length)]
178
+
179
+ # initialize vars
180
+ # Decision every candidate to every possible other candidate as cluster center
181
+ M.z = pyomo.Var(M.i, M.j, within=pyomo.Binary)
182
+
183
+ # get objective
184
+ # Minimize the distance of every candidate to the cluster center
185
+ def objRule(M):
186
+ return sum(sum(M.d[i, j] * M.z[i, j] for j in M.j) for i in M.i)
187
+
188
+ M.obj = pyomo.Objective(rule=objRule)
189
+
190
+ # s.t.
191
+ # Assign all candidates to one clusters
192
+ def candToClusterRule(M, j):
193
+ return sum(M.z[i, j] for i in M.i) == 1
194
+
195
+ M.candToClusterCon = pyomo.Constraint(M.j, rule=candToClusterRule)
196
+
197
+ # Predefine the number of clusters
198
+ def noClustersRule(M):
199
+ return sum(M.z[i, i] for i in M.i) == M.no_k
200
+
201
+ M.noClustersCon = pyomo.Constraint(rule=noClustersRule)
202
+
203
+ # Describe the choice of a candidate to a cluster
204
+ def clusterRelationRule(M, i, j):
205
+ return M.z[i, j] <= M.z[i, i]
206
+
207
+ M.clusterRelationCon = pyomo.Constraint(M.i, M.j, rule=clusterRelationRule)
208
+ return M
209
+
210
+
211
+ def _solve_given_pyomo_model(M, solver="highs"):
212
+ """Solves a given pyomo model clustering model an returns the clusters
213
+
214
+ Args:
215
+ M (pyomo.ConcreteModel): Concrete model instance that gets solved.
216
+ solver (str, optional): solver, defines the solver for the pyomo model. Defaults to "highs".
217
+
218
+ Raises:
219
+ ValueError: [description]
220
+
221
+ Returns:
222
+ [type]: [description]
223
+ """
224
+ # create optimization problem
225
+ if solver == "highs":
226
+ solver_instance = appsi.solvers.Highs()
227
+ else:
228
+ solver_instance = opt.SolverFactory(solver)
229
+ results = solver_instance.solve(M)
230
+ # check that it does not fail
231
+
232
+ # Get results
233
+ r_x = np.array([[round(M.z[i, j].value) for i in M.i] for j in M.j])
234
+
235
+ r_y = np.array([round(M.z[j, j].value) for j in M.j])
236
+
237
+ r_obj = pyomo.value(M.obj)
238
+
239
+ return (r_x, r_y, r_obj)