tsam 2.3.9__py3-none-any.whl → 3.0.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.
@@ -1,239 +1,232 @@
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)
1
+ import numpy as np
2
+ from sklearn.base import BaseEstimator, ClusterMixin, TransformerMixin
3
+ from sklearn.metrics.pairwise import PAIRWISE_DISTANCE_FUNCTIONS
4
+ from sklearn.utils import check_array
5
+
6
+ # switch to numpy 2.0 (restore deprecated aliases for backward compatibility)
7
+ np.float_ = np.float64 # type: ignore[attr-defined]
8
+ np.complex_ = np.complex128 # type: ignore[attr-defined]
9
+
10
+ import pyomo.environ as pyomo
11
+ import pyomo.opt as opt
12
+ from pyomo.contrib import appsi
13
+
14
+
15
+ class KMedoids(BaseEstimator, ClusterMixin, TransformerMixin):
16
+ """
17
+ k-medoids class.
18
+
19
+ :param n_clusters: How many medoids. Must be positive. optional, default: 8
20
+ :type n_clusters: integer
21
+
22
+ :param distance_metric: What distance metric to use. optional, default: 'euclidean'
23
+ :type distance_metric: string
24
+
25
+ :param timelimit: Specify the time limit of the solver. optional, default: 100
26
+ :type timelimit: integer
27
+
28
+ :param threads: Threads to use by the optimization solver. optional, default: 7
29
+ :type threads: integer
30
+
31
+ :param solver: Specifies the solver. optional, default: 'highs'
32
+ :type solver: string
33
+ """
34
+
35
+ def __init__(
36
+ self,
37
+ n_clusters=8,
38
+ distance_metric="euclidean",
39
+ timelimit=100,
40
+ threads=7,
41
+ solver="highs",
42
+ ):
43
+ self.n_clusters = n_clusters
44
+
45
+ self.distance_metric = distance_metric
46
+
47
+ self.solver = solver
48
+
49
+ self.timelimit = timelimit
50
+
51
+ self.threads = threads
52
+
53
+ def _check_init_args(self):
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
+ + f"{PAIRWISE_DISTANCE_FUNCTIONS.keys()}"
73
+ + f". Instead, '{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
+ X = check_array(X)
119
+
120
+ # Check that the number of clusters is less than or equal to
121
+ # the number of samples
122
+ if self.n_clusters > X.shape[0]:
123
+ raise ValueError(
124
+ "The number of medoids "
125
+ + f"({self.n_clusters}) "
126
+ + "must be larger than the number "
127
+ + f"of samples ({X.shape[0]})"
128
+ )
129
+
130
+ return X
131
+
132
+ def _k_medoids_exact(self, distances, n_clusters):
133
+ """
134
+ Parameters
135
+ ----------
136
+ distances : int, required
137
+ Pairwise distances between each row.
138
+ n_clusters : int, required
139
+ Number of clusters.
140
+ """
141
+
142
+ # Create pyomo model
143
+ M = _setup_k_medoids(distances, n_clusters)
144
+
145
+ # And solve
146
+ r_x, r_y, r_obj = _solve_given_pyomo_model(M, solver=self.solver)
147
+
148
+ return (r_y, r_x.T, r_obj)
149
+
150
+
151
+ def _setup_k_medoids(distances, n_clusters):
152
+ """Define the k-medoids model with pyomo.
153
+ In the spatial aggregation community, it is referred to as Hess Model for political districting
154
+ with an additional constraint of cluster-sizes/populations.
155
+ (W Hess, JB Weaver, HJ Siegfeldt, JN Whelan, and PA Zitlau. Nonpartisan political redistricting by computer. Operations Research, 13(6):998–1006, 1965.)
156
+ """
157
+ # Create model
158
+ M = pyomo.ConcreteModel()
159
+
160
+ # get distance matrix
161
+ M.d = distances
162
+
163
+ # set number of clusters
164
+ M.no_k = n_clusters
165
+
166
+ # Distances is a symmetrical matrix, extract its length
167
+ length = distances.shape[0]
168
+
169
+ # get indices
170
+ M.i = [j for j in range(length)]
171
+ M.j = [j for j in range(length)]
172
+
173
+ # initialize vars
174
+ # Decision every candidate to every possible other candidate as cluster center
175
+ M.z = pyomo.Var(M.i, M.j, within=pyomo.Binary)
176
+
177
+ # get objective
178
+ # Minimize the distance of every candidate to the cluster center
179
+ def objRule(M):
180
+ return sum(sum(M.d[i, j] * M.z[i, j] for j in M.j) for i in M.i)
181
+
182
+ M.obj = pyomo.Objective(rule=objRule)
183
+
184
+ # s.t.
185
+ # Assign all candidates to one clusters
186
+ def candToClusterRule(M, j):
187
+ return sum(M.z[i, j] for i in M.i) == 1
188
+
189
+ M.candToClusterCon = pyomo.Constraint(M.j, rule=candToClusterRule)
190
+
191
+ # Predefine the number of clusters
192
+ def noClustersRule(M):
193
+ return sum(M.z[i, i] for i in M.i) == M.no_k
194
+
195
+ M.noClustersCon = pyomo.Constraint(rule=noClustersRule)
196
+
197
+ # Describe the choice of a candidate to a cluster
198
+ def clusterRelationRule(M, i, j):
199
+ return M.z[i, j] <= M.z[i, i]
200
+
201
+ M.clusterRelationCon = pyomo.Constraint(M.i, M.j, rule=clusterRelationRule)
202
+ return M
203
+
204
+
205
+ def _solve_given_pyomo_model(M, solver="highs"):
206
+ """Solves a given pyomo model clustering model an returns the clusters
207
+
208
+ Args:
209
+ M (pyomo.ConcreteModel): Concrete model instance that gets solved.
210
+ solver (str, optional): solver, defines the solver for the pyomo model. Defaults to "highs".
211
+
212
+ Raises:
213
+ ValueError: [description]
214
+
215
+ Returns:
216
+ [type]: [description]
217
+ """
218
+ # create optimization problem
219
+ if solver == "highs":
220
+ solver_instance = appsi.solvers.Highs()
221
+ else:
222
+ solver_instance = opt.SolverFactory(solver)
223
+ _results = solver_instance.solve(M) # results checked via model state
224
+
225
+ # Get results
226
+ r_x = np.array([[round(M.z[i, j].value) for i in M.i] for j in M.j])
227
+
228
+ r_y = np.array([round(M.z[j, j].value) for j in M.j])
229
+
230
+ r_obj = pyomo.value(M.obj)
231
+
232
+ return (r_x, r_y, r_obj)