hdim-opt 1.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.
hdim_opt/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# hdim_opt/__init__.py
|
|
2
|
+
|
|
3
|
+
# package version
|
|
4
|
+
__version__ = "1.0.0"
|
|
5
|
+
|
|
6
|
+
# import core components
|
|
7
|
+
from .quasar_optimization import optimize as quasar
|
|
8
|
+
from .hyperellipsoid_sampling import sample as hds
|
|
9
|
+
|
|
10
|
+
# list what is available for star-imports
|
|
11
|
+
__all__ = ['quasar', 'hds']
|
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pandas as pd
|
|
3
|
+
from scipy import stats
|
|
4
|
+
from joblib import Parallel, delayed
|
|
5
|
+
from sklearn.neighbors import BallTree
|
|
6
|
+
from sklearn.cluster import MiniBatchKMeans, AgglomerativeClustering
|
|
7
|
+
from sklearn.random_projection import GaussianRandomProjection
|
|
8
|
+
from sklearn.decomposition import PCA
|
|
9
|
+
import scipy.cluster.hierarchy as shc
|
|
10
|
+
import time
|
|
11
|
+
import warnings
|
|
12
|
+
|
|
13
|
+
# globals
|
|
14
|
+
warnings.filterwarnings('ignore', category=UserWarning)
|
|
15
|
+
epsilon = 1e-12
|
|
16
|
+
|
|
17
|
+
### misc helper functions
|
|
18
|
+
|
|
19
|
+
def sample_hypersphere(n_dimensions, radius, n_samples_in_sphere, radius_qmc_sequence):
|
|
20
|
+
'''
|
|
21
|
+
Objective:
|
|
22
|
+
- Samples unit hyperspheres using Marsaglia polar vectors scaled by a QMC sequence.
|
|
23
|
+
'''
|
|
24
|
+
|
|
25
|
+
# generate normal distribution (for angular direction)
|
|
26
|
+
samples = np.random.normal(size=(n_samples_in_sphere, n_dimensions))
|
|
27
|
+
|
|
28
|
+
# normalize vectors to get points on the surface of a unit sphere (direction)
|
|
29
|
+
squared_norms = np.sum(samples**2, axis=1)
|
|
30
|
+
inv_norms = 1.0 / (np.sqrt(squared_norms) + epsilon)
|
|
31
|
+
|
|
32
|
+
# efficiently apply directions (broadcasting faster than division)
|
|
33
|
+
samples = samples * inv_norms[:, np.newaxis]
|
|
34
|
+
|
|
35
|
+
# use input QMC sequence for radius scaling (r^(1/n))
|
|
36
|
+
random_radii_qmc = np.power(radius_qmc_sequence, (1.0 / n_dimensions))
|
|
37
|
+
|
|
38
|
+
# scale and apply final radius
|
|
39
|
+
samples = samples * (random_radii_qmc * radius)[:, np.newaxis]
|
|
40
|
+
|
|
41
|
+
return samples
|
|
42
|
+
|
|
43
|
+
def sample_hyperellipsoid(n_dimensions, n_samples_in_ellipsoid, origin, pca_components, pca_variances, scaling_factor, radius_qmc_sequence=None):
|
|
44
|
+
'''
|
|
45
|
+
Objective:
|
|
46
|
+
- Generates samples inside the hyperellipsoid.
|
|
47
|
+
- Calls the function to sample unit hyperspheres.
|
|
48
|
+
- Transforms the hyperspherical samples to the ellipsoid axes defined using the PCA variances.
|
|
49
|
+
'''
|
|
50
|
+
|
|
51
|
+
# generate samples in unit hypersphere
|
|
52
|
+
unit_sphere_samples = sample_hypersphere(n_dimensions, 1.0, n_samples_in_ellipsoid, radius_qmc_sequence)
|
|
53
|
+
|
|
54
|
+
# axis lengths: L = sqrt(variance + epsilon) * scaling_factor
|
|
55
|
+
axis_lengths = np.sqrt(pca_variances + epsilon) * scaling_factor
|
|
56
|
+
|
|
57
|
+
# scale samples in PCA space and rotates back to original parameter space
|
|
58
|
+
scaled_and_rotated = (unit_sphere_samples * axis_lengths) @ pca_components
|
|
59
|
+
|
|
60
|
+
# translate samples to cluster origin
|
|
61
|
+
ellipsoid_samples = scaled_and_rotated + origin
|
|
62
|
+
|
|
63
|
+
return ellipsoid_samples
|
|
64
|
+
|
|
65
|
+
def sample_in_voids(existing_samples, n_to_fill, bounds_min, bounds_max,
|
|
66
|
+
k_neighbors=5, spread_factor=0.5,
|
|
67
|
+
n_query_max=1000, n_tree_max=10000):
|
|
68
|
+
'''
|
|
69
|
+
Objective:
|
|
70
|
+
- Identify & fill voids in the sample space, using the out-of-bounds sample set.
|
|
71
|
+
- Uses BallTree K-NearestNeighbors to identify voids.
|
|
72
|
+
'''
|
|
73
|
+
|
|
74
|
+
# extract shape
|
|
75
|
+
n_existing, n_dimensions = existing_samples.shape
|
|
76
|
+
|
|
77
|
+
# if no samples to replace
|
|
78
|
+
if n_to_fill <= 0:
|
|
79
|
+
return np.zeros((0, n_dimensions))
|
|
80
|
+
|
|
81
|
+
# if number of neighbors exceeds number of existing samples
|
|
82
|
+
if n_existing < k_neighbors + 1:
|
|
83
|
+
return stats.uniform.rvs(loc=bounds_min, scale=bounds_max - bounds_min, size=(n_to_fill, n_dimensions))
|
|
84
|
+
|
|
85
|
+
# for number of points > 100,000, reduce size to 10,000 for speed
|
|
86
|
+
if n_existing > n_tree_max:
|
|
87
|
+
tree_indices = np.random.choice(n_existing, size=n_tree_max, replace=False)
|
|
88
|
+
tree_samples = existing_samples[tree_indices]
|
|
89
|
+
else:
|
|
90
|
+
tree_indices = np.arange(n_existing)
|
|
91
|
+
tree_samples = existing_samples
|
|
92
|
+
|
|
93
|
+
# recalculate n_existing for reduced set
|
|
94
|
+
n_existing_for_tree = tree_samples.shape[0]
|
|
95
|
+
|
|
96
|
+
# reduce dimensionality for speed
|
|
97
|
+
n_rp_components = min(max(10, int(2*np.log2(n_dimensions))), n_dimensions)
|
|
98
|
+
if n_rp_components < n_dimensions:
|
|
99
|
+
rp = GaussianRandomProjection(n_components=n_rp_components)
|
|
100
|
+
existing_samples_reduced = rp.fit_transform(tree_samples)
|
|
101
|
+
else:
|
|
102
|
+
existing_samples_reduced = tree_samples
|
|
103
|
+
|
|
104
|
+
# build BallTree
|
|
105
|
+
start_kdtree_build = time.time()
|
|
106
|
+
tree = BallTree(existing_samples_reduced)
|
|
107
|
+
|
|
108
|
+
# BallTree query on subset
|
|
109
|
+
k_to_query = k_neighbors + 1 # k-th neighbor distance
|
|
110
|
+
|
|
111
|
+
# select random subset of centers to calculate void probability for (query set)
|
|
112
|
+
# query set sampled from the reduced set (existing_samples_reduced)
|
|
113
|
+
if n_existing_for_tree > n_query_max:
|
|
114
|
+
query_subset_indices = np.random.choice(n_existing_for_tree, size=n_query_max, replace=False)
|
|
115
|
+
existing_samples_query = existing_samples_reduced[query_subset_indices]
|
|
116
|
+
else:
|
|
117
|
+
query_subset_indices = np.arange(n_existing_for_tree)
|
|
118
|
+
existing_samples_query = existing_samples_reduced
|
|
119
|
+
|
|
120
|
+
# query the subset against the reduced-sample tree
|
|
121
|
+
# returns distances first, then indices.
|
|
122
|
+
kth_nn_distances_subset, _ = tree.query(existing_samples_query, k=k_to_query,)
|
|
123
|
+
|
|
124
|
+
# extract distance to the k-th nearest neighbor
|
|
125
|
+
kth_nn_distances_subset = kth_nn_distances_subset[:, k_neighbors]
|
|
126
|
+
|
|
127
|
+
# identify void centers
|
|
128
|
+
probabilities = kth_nn_distances_subset / (kth_nn_distances_subset.sum() + epsilon)
|
|
129
|
+
void_center_query_indices = np.random.choice(
|
|
130
|
+
len(query_subset_indices),
|
|
131
|
+
size=n_to_fill,
|
|
132
|
+
p=probabilities,
|
|
133
|
+
replace=True
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# map back to indices in the tree_samples set
|
|
137
|
+
void_center_tree_indices = query_subset_indices[void_center_query_indices]
|
|
138
|
+
|
|
139
|
+
# map the indices back to the original full-sized existing_samples array
|
|
140
|
+
void_center_full_indices = tree_indices[void_center_tree_indices]
|
|
141
|
+
|
|
142
|
+
# extract centers and spreads from original, full-D data
|
|
143
|
+
chosen_centers = existing_samples[void_center_full_indices]
|
|
144
|
+
# kth_nn_distances_subset is still correct as it corresponds to the query points
|
|
145
|
+
chosen_kth_distances = kth_nn_distances_subset[void_center_query_indices]
|
|
146
|
+
|
|
147
|
+
# calculate spreads and generate samples (in full-D)
|
|
148
|
+
spreads = chosen_kth_distances[:, np.newaxis] * spread_factor + epsilon
|
|
149
|
+
a_params = (bounds_min - chosen_centers) / spreads
|
|
150
|
+
b_params = (bounds_max - chosen_centers) / spreads
|
|
151
|
+
new_samples = stats.truncnorm.rvs(a=a_params, b=b_params, loc=chosen_centers, scale=spreads)
|
|
152
|
+
|
|
153
|
+
return new_samples
|
|
154
|
+
|
|
155
|
+
def fit_pca_for_cluster(cluster_samples, current_origin, initial_samples_std, n_dimensions):
|
|
156
|
+
'''
|
|
157
|
+
Performs PCA on a single cluster's samples or returns a default,
|
|
158
|
+
called in parallel.
|
|
159
|
+
'''
|
|
160
|
+
|
|
161
|
+
# extract shape
|
|
162
|
+
n_cluster_samples = len(cluster_samples)
|
|
163
|
+
|
|
164
|
+
if n_cluster_samples > n_dimensions * 2 and n_cluster_samples > 0:
|
|
165
|
+
pca = PCA(n_components=n_dimensions)
|
|
166
|
+
pca.fit(cluster_samples)
|
|
167
|
+
|
|
168
|
+
return {'origin': current_origin,
|
|
169
|
+
'components': pca.components_.T,
|
|
170
|
+
'variances': pca.explained_variance_}
|
|
171
|
+
else:
|
|
172
|
+
# handle empty/too small clusters
|
|
173
|
+
fixed_variance = np.ones(n_dimensions) * initial_samples_std
|
|
174
|
+
|
|
175
|
+
return {'origin': current_origin,
|
|
176
|
+
'components': np.eye(n_dimensions),
|
|
177
|
+
'variances': fixed_variance}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
# main sample function
|
|
181
|
+
|
|
182
|
+
def sample(n_samples, bounds,
|
|
183
|
+
weights=None, normalize=False,
|
|
184
|
+
n_ellipsoids=None, n_initial_clusters=None, n_initial_qmc=None,
|
|
185
|
+
seed=None, verbose=False):
|
|
186
|
+
'''
|
|
187
|
+
Objective:
|
|
188
|
+
- Generates a Hyperellipsoid Density sample sequence over the specified parameter range.
|
|
189
|
+
Inputs:
|
|
190
|
+
- n_samples: Number of samples to generate.
|
|
191
|
+
- bounds: Bounds of the parameter range.
|
|
192
|
+
- weights: Gaussian weights to influence clusters and final sample locations.
|
|
193
|
+
- normalize: Boolean to scale samples to the original parameter space, or leave normalized from [0,1].
|
|
194
|
+
- n_ellipsoids: Number of hyperellipsoids to sample.
|
|
195
|
+
- Replaces and skips the Agglomerative Hierarchical Clustering (AHC) step.
|
|
196
|
+
- n_initial_clusters: Number of initial clusters to use in calculating number of hyperellipsoids.
|
|
197
|
+
- Redunant if n_ellipsoids is specified.
|
|
198
|
+
- n_initial_qmc: Number of initial QMC samples to use for cluster analysis.
|
|
199
|
+
- seed: Random seed.
|
|
200
|
+
- verbose: Boolean to display stats and plots.
|
|
201
|
+
Outputs:
|
|
202
|
+
- hds_samples: Hyperellipsoid Density sample sequence.
|
|
203
|
+
'''
|
|
204
|
+
|
|
205
|
+
# initialize misc parameters:
|
|
206
|
+
start_time = time.time()
|
|
207
|
+
if seed is None:
|
|
208
|
+
seed = time.time()
|
|
209
|
+
seed = int(round(seed))
|
|
210
|
+
np.random.seed(seed)
|
|
211
|
+
|
|
212
|
+
# initialize sampling parameters:
|
|
213
|
+
n_samples = int(n_samples)
|
|
214
|
+
bounds = np.array(bounds)
|
|
215
|
+
n_dimensions = bounds.shape[0]
|
|
216
|
+
|
|
217
|
+
# n_initial_clusters scaling for D > 100
|
|
218
|
+
if n_initial_clusters is None:
|
|
219
|
+
if n_dimensions <= 500:
|
|
220
|
+
n_initial_clusters = 100 # 100 clusters for <= 500D
|
|
221
|
+
elif n_dimensions < 1000:
|
|
222
|
+
n_initial_clusters = 50 # 50 clusters sees same stddev as 100 for > 500D
|
|
223
|
+
else:
|
|
224
|
+
n_initial_clusters = 25 # 25 clusters sees same stddev as 100 for > 1000D
|
|
225
|
+
n_initial_clusters = int(n_initial_clusters)
|
|
226
|
+
|
|
227
|
+
# number of qmc samples scaling
|
|
228
|
+
n_initial_qmc_max = 2**15
|
|
229
|
+
if n_initial_qmc is None:
|
|
230
|
+
min_qmc_dimensions = int(2**np.ceil(np.log2(n_dimensions*200)))
|
|
231
|
+
n_initial_qmc = min(min_qmc_dimensions, n_initial_qmc_max)
|
|
232
|
+
|
|
233
|
+
# keep data normalized (0 to 1) for clustering
|
|
234
|
+
bounds_min_orig = bounds[:, 0]
|
|
235
|
+
bounds_max_orig = bounds[:, 1]
|
|
236
|
+
working_bounds_min = np.zeros(n_dimensions)
|
|
237
|
+
working_bounds_max = np.ones(n_dimensions)
|
|
238
|
+
|
|
239
|
+
# generate initial QMC samples:
|
|
240
|
+
qmc_start_time = time.time()
|
|
241
|
+
initial_sobol_sampler = stats.qmc.Sobol(d=n_dimensions, seed=np.random.randint(0, 1000))
|
|
242
|
+
initial_samples_unit = initial_sobol_sampler.random(n=n_initial_qmc)
|
|
243
|
+
initial_samples = initial_samples_unit
|
|
244
|
+
|
|
245
|
+
# calculate sample weights based on input
|
|
246
|
+
sample_weights = np.ones(initial_samples.shape[0])
|
|
247
|
+
if weights:
|
|
248
|
+
initial_samples_denorm = stats.qmc.scale(initial_samples, bounds_min_orig, bounds_max_orig)
|
|
249
|
+
for dim, info in weights.items():
|
|
250
|
+
center = info['center']
|
|
251
|
+
std = info['std']
|
|
252
|
+
if not std > 0:
|
|
253
|
+
raise ValueError(f'Gaussian weight stddevs must be > 0.')
|
|
254
|
+
return None
|
|
255
|
+
dim_values = initial_samples_denorm[:, dim]
|
|
256
|
+
gaussian_weights = stats.norm.pdf(dim_values, loc=center, scale=std)
|
|
257
|
+
|
|
258
|
+
gaussian_weights += epsilon
|
|
259
|
+
sample_weights *= gaussian_weights
|
|
260
|
+
|
|
261
|
+
if verbose and n_ellipsoids is None:
|
|
262
|
+
print('Calculating ellipsoid density.')
|
|
263
|
+
|
|
264
|
+
# determine number of ellipsoids via agglomerative clustering
|
|
265
|
+
# KMeans to get stable sub-cluster centers
|
|
266
|
+
kmeans_initial = MiniBatchKMeans(n_clusters=n_initial_clusters, random_state=seed, n_init=6)
|
|
267
|
+
kmeans_initial.fit(initial_samples, sample_weight=sample_weights)
|
|
268
|
+
initial_centroids = kmeans_initial.cluster_centers_
|
|
269
|
+
|
|
270
|
+
# skip agglomerative clustering if number of initial clusters is provided
|
|
271
|
+
linkage_matrix = None
|
|
272
|
+
optimal_distance = None
|
|
273
|
+
if n_ellipsoids is not None and n_ellipsoids >= 1:
|
|
274
|
+
n_hyperellipsoids = n_ellipsoids
|
|
275
|
+
else:
|
|
276
|
+
# hierarchical clustering on the centroids to find natural grouping
|
|
277
|
+
linkage_matrix = shc.linkage(initial_centroids, method='ward')
|
|
278
|
+
|
|
279
|
+
# find optimal cut-off distance (d) based on largest jump
|
|
280
|
+
distances = linkage_matrix[:, 2]
|
|
281
|
+
optimal_distance = 0
|
|
282
|
+
if len(distances) > 2:
|
|
283
|
+
diffs = distances[1:] - distances[:-1]
|
|
284
|
+
max_diff_index = np.argmax(diffs)
|
|
285
|
+
optimal_distance = distances[max_diff_index + 1]
|
|
286
|
+
|
|
287
|
+
# in case initial sample clusters are uniform
|
|
288
|
+
if optimal_distance < 0.1:
|
|
289
|
+
optimal_distance = 0.5
|
|
290
|
+
|
|
291
|
+
# agglomerative clustering to determine k at the optimal distance
|
|
292
|
+
agg_model = AgglomerativeClustering(n_clusters=None, distance_threshold=optimal_distance, linkage='ward')
|
|
293
|
+
agg_model.fit(initial_centroids)
|
|
294
|
+
n_hyperellipsoids = agg_model.n_clusters_
|
|
295
|
+
|
|
296
|
+
else:
|
|
297
|
+
n_hyperellipsoids = 1
|
|
298
|
+
n_hyperellipsoids = max(1, n_hyperellipsoids)
|
|
299
|
+
|
|
300
|
+
# K-Means again with optimal n_hyperellipsoids to find final centers
|
|
301
|
+
kmeans = MiniBatchKMeans(n_clusters=n_hyperellipsoids, random_state=seed, n_init=6)
|
|
302
|
+
kmeans.fit(initial_samples, sample_weight=sample_weights)
|
|
303
|
+
origins = kmeans.cluster_centers_
|
|
304
|
+
cluster_labels = kmeans.labels_
|
|
305
|
+
|
|
306
|
+
# pre-calculate sample stddev
|
|
307
|
+
initial_samples_std = np.std(initial_samples)
|
|
308
|
+
|
|
309
|
+
# prepare inputs for the parallel loop
|
|
310
|
+
cluster_data_inputs = []
|
|
311
|
+
for i in range(n_hyperellipsoids):
|
|
312
|
+
cluster_samples = initial_samples[cluster_labels == i]
|
|
313
|
+
current_origin = origins[i]
|
|
314
|
+
|
|
315
|
+
# recenter origin if the cluster is empty
|
|
316
|
+
if len(cluster_samples) == 0:
|
|
317
|
+
current_origin = initial_samples[np.random.randint(len(initial_samples))]
|
|
318
|
+
|
|
319
|
+
cluster_data_inputs.append((cluster_samples, current_origin, initial_samples_std, n_dimensions))
|
|
320
|
+
|
|
321
|
+
# calculate hyperellipsoid shapes via PCA (parallelized)
|
|
322
|
+
if verbose:
|
|
323
|
+
print(f'Orienting axes.')
|
|
324
|
+
ellipsoid_params = Parallel(n_jobs=-1)(
|
|
325
|
+
delayed(fit_pca_for_cluster)(*args) for args in cluster_data_inputs
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
# recalculate cluster counts from labels, for proportional sampling
|
|
329
|
+
cluster_sample_counts = np.array([np.sum(cluster_labels == i) for i in range(n_hyperellipsoids)])
|
|
330
|
+
|
|
331
|
+
# distribute samples proportionally to cluster size
|
|
332
|
+
total_cluster_count = np.sum(cluster_sample_counts)
|
|
333
|
+
if total_cluster_count == 0:
|
|
334
|
+
n_samples_per_ellipsoid = np.ones(n_hyperellipsoids, dtype=int)
|
|
335
|
+
else:
|
|
336
|
+
n_samples_per_ellipsoid = np.round(n_samples * (cluster_sample_counts / total_cluster_count)).astype(int)
|
|
337
|
+
n_samples_per_ellipsoid[-1] += n_samples - np.sum(n_samples_per_ellipsoid)
|
|
338
|
+
n_samples_per_ellipsoid = np.maximum(0, n_samples_per_ellipsoid)
|
|
339
|
+
|
|
340
|
+
# generate hyperellipsoid samples (sobol distributed radius):
|
|
341
|
+
hds_samples_normalized = np.zeros((0, n_dimensions))
|
|
342
|
+
|
|
343
|
+
# radius scaling factor; scales with dimension
|
|
344
|
+
confidence_level = 0.9999 # captures 99.99% of cluster's samples
|
|
345
|
+
|
|
346
|
+
# critical value (the statistical radius squared)
|
|
347
|
+
chi2_critical_value = stats.chi2.ppf(confidence_level, df=n_dimensions)
|
|
348
|
+
baseline_factor = 0.55 - 0.01*np.log(n_dimensions) # empirically derived to resample out-of-bounds points
|
|
349
|
+
|
|
350
|
+
# square root as the scaling factor (Mahalanobis distance)
|
|
351
|
+
ellipsoid_scaling_factor = baseline_factor * np.sqrt(chi2_critical_value)
|
|
352
|
+
|
|
353
|
+
# QMC sequence for radius scaling
|
|
354
|
+
radius_qmc_sampler = stats.qmc.Sobol(d=1, seed=seed+1) # offset seed from initial qmc
|
|
355
|
+
radius_qmc_sequence_base = radius_qmc_sampler.random(n=int(n_samples * 2.5)) # generate extra samples
|
|
356
|
+
radius_start_idx = 0
|
|
357
|
+
|
|
358
|
+
# sequentially generate samples from each ellipsoid
|
|
359
|
+
collected_samples = []
|
|
360
|
+
for i, params in enumerate(ellipsoid_params):
|
|
361
|
+
n_to_generate = n_samples_per_ellipsoid[i] * 2
|
|
362
|
+
|
|
363
|
+
# select next chunk of QMC radius sequence
|
|
364
|
+
radius_end_idx = radius_start_idx + n_to_generate
|
|
365
|
+
|
|
366
|
+
# prevent index out of bounds
|
|
367
|
+
if radius_end_idx > len(radius_qmc_sequence_base):
|
|
368
|
+
# use the remainder
|
|
369
|
+
radius_qmc_chunk = radius_qmc_sequence_base[radius_start_idx:].flatten()
|
|
370
|
+
else:
|
|
371
|
+
radius_qmc_chunk = radius_qmc_sequence_base[radius_start_idx:radius_end_idx].flatten()
|
|
372
|
+
|
|
373
|
+
radius_start_idx = radius_end_idx
|
|
374
|
+
|
|
375
|
+
# prevent ValueError from empty array
|
|
376
|
+
if n_to_generate > 0 and radius_qmc_chunk.size == 0:
|
|
377
|
+
continue # skip ellipsoid if this chunk is empty
|
|
378
|
+
|
|
379
|
+
# generate samples inside current ellipsoid
|
|
380
|
+
ellipsoid_samples = sample_hyperellipsoid(n_dimensions,
|
|
381
|
+
n_to_generate,
|
|
382
|
+
params['origin'],
|
|
383
|
+
params['components'],
|
|
384
|
+
params['variances'],
|
|
385
|
+
scaling_factor=ellipsoid_scaling_factor,
|
|
386
|
+
radius_qmc_sequence=radius_qmc_chunk
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
# identify points outside boundaries ([0,1] hypercube)
|
|
390
|
+
in_bounds_mask = np.all(ellipsoid_samples >= 0, axis=1) & np.all(ellipsoid_samples <= 1, axis=1)
|
|
391
|
+
valid_samples = ellipsoid_samples[in_bounds_mask]
|
|
392
|
+
|
|
393
|
+
# add required number of valid samples
|
|
394
|
+
num_to_add = min(n_samples_per_ellipsoid[i], len(valid_samples))
|
|
395
|
+
collected_samples.append(valid_samples[:num_to_add])
|
|
396
|
+
|
|
397
|
+
# vstack
|
|
398
|
+
if collected_samples:
|
|
399
|
+
hds_samples_normalized = np.vstack(collected_samples)
|
|
400
|
+
else:
|
|
401
|
+
hds_samples_normalized = np.zeros((0, n_dimensions))
|
|
402
|
+
|
|
403
|
+
# identify number of points to resample
|
|
404
|
+
n_to_fill = n_samples - len(hds_samples_normalized)
|
|
405
|
+
if n_to_fill > 0:
|
|
406
|
+
if verbose:
|
|
407
|
+
print(f'Geometric void filling {n_to_fill} samples.')
|
|
408
|
+
|
|
409
|
+
# use the existing collected hds samples to find the voids
|
|
410
|
+
k_void_neighbors = min(max(5, int(n_dimensions / 10)), 10)
|
|
411
|
+
void_resamples = sample_in_voids(
|
|
412
|
+
existing_samples=hds_samples_normalized,
|
|
413
|
+
n_to_fill=n_to_fill,
|
|
414
|
+
bounds_min=working_bounds_min,
|
|
415
|
+
bounds_max=working_bounds_max,
|
|
416
|
+
k_neighbors=k_void_neighbors, # k scales with dimension
|
|
417
|
+
spread_factor=0.25 # stay local
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
# combine original hds samples with new void samples
|
|
421
|
+
hds_samples_normalized = np.vstack([hds_samples_normalized, void_resamples])
|
|
422
|
+
|
|
423
|
+
hds_samples_normalized = hds_samples_normalized[:n_samples]
|
|
424
|
+
|
|
425
|
+
if normalize:
|
|
426
|
+
# leave in [0,1] space
|
|
427
|
+
hds_sequence = hds_samples_normalized
|
|
428
|
+
else:
|
|
429
|
+
# scale samples to original bounds
|
|
430
|
+
hds_sequence = hds_samples_normalized * (bounds_max_orig - bounds_min_orig) + bounds_min_orig
|
|
431
|
+
|
|
432
|
+
### print & plot results:
|
|
433
|
+
if verbose:
|
|
434
|
+
end_time = time.time()
|
|
435
|
+
sample_generation_time = end_time - start_time
|
|
436
|
+
|
|
437
|
+
# visualization imports
|
|
438
|
+
import matplotlib.pyplot as plt
|
|
439
|
+
import seaborn as sns
|
|
440
|
+
from matplotlib.patches import Circle, Rectangle
|
|
441
|
+
sns.set_style('dark')
|
|
442
|
+
|
|
443
|
+
# print results
|
|
444
|
+
print('\nresults:')
|
|
445
|
+
print(' - number of samples:', len(hds_sequence))
|
|
446
|
+
print(f' - sample generation time: {sample_generation_time:.2f}')
|
|
447
|
+
print(f' - number of hyperellipsoids: {n_hyperellipsoids}')
|
|
448
|
+
print(f' - number of initial QMC: {n_initial_qmc}')
|
|
449
|
+
print(f' - number of initial clusters: {n_initial_clusters}')
|
|
450
|
+
if weights:
|
|
451
|
+
print(f' - gaussian weights: {weights}')
|
|
452
|
+
|
|
453
|
+
# generate a sobol sequence for comparison
|
|
454
|
+
sobol_sampler = stats.qmc.Sobol(d=n_dimensions, seed=seed+2) # offset seed to be different from initial qmc
|
|
455
|
+
sobol_samples_unit = sobol_sampler.random(n=n_samples)
|
|
456
|
+
if normalize:
|
|
457
|
+
sobol_samples = sobol_samples_unit
|
|
458
|
+
else:
|
|
459
|
+
sobol_samples = stats.qmc.scale(sobol_samples_unit, bounds_min_orig, bounds_max_orig)
|
|
460
|
+
|
|
461
|
+
# samples stats:
|
|
462
|
+
hds_mean = np.mean(hds_sequence)
|
|
463
|
+
sobol_mean = np.mean(sobol_samples)
|
|
464
|
+
hds_std = np.std(hds_sequence)
|
|
465
|
+
sobol_std = np.std(sobol_samples)
|
|
466
|
+
|
|
467
|
+
print('\nstats:')
|
|
468
|
+
print(f' - mean HDS: {hds_mean:.2f}')
|
|
469
|
+
print(f' - mean comparison QMC: {sobol_mean:.2f}')
|
|
470
|
+
print(f' - stdev HDS: {hds_std:.2f}')
|
|
471
|
+
print(f' - stdev comparison QMC: {sobol_std:.2f}\n')
|
|
472
|
+
|
|
473
|
+
# dendrogram of centroids
|
|
474
|
+
if linkage_matrix is not None:
|
|
475
|
+
plt.figure(figsize=(9, 7))
|
|
476
|
+
plt.title(f'Dendrogram of Initial Centroids: {n_dimensions}D')
|
|
477
|
+
|
|
478
|
+
# using pre-calculated linkage matrix
|
|
479
|
+
shc.dendrogram(linkage_matrix, color_threshold=optimal_distance, above_threshold_color='gray')
|
|
480
|
+
plt.axhline(y=optimal_distance, color='r', linestyle='--', label=f'Optimal Cutoff (k={n_hyperellipsoids})')
|
|
481
|
+
plt.ylabel('Dissimilarity Distance')
|
|
482
|
+
plt.xticks([])
|
|
483
|
+
|
|
484
|
+
plt.legend(loc='upper right')
|
|
485
|
+
plt.show()
|
|
486
|
+
|
|
487
|
+
# plot for 1d samples
|
|
488
|
+
if n_dimensions == 1:
|
|
489
|
+
plt.figure(figsize=(6,5))
|
|
490
|
+
plt.hist(hds_sequence, bins=30, color='deepskyblue', alpha=0.9, label='HDS Samples')
|
|
491
|
+
plt.hist(sobol_samples, bins=30, color='red', alpha=0.5, label='Sobol Samples')
|
|
492
|
+
plt.title('HDS Sample Distribution')
|
|
493
|
+
plt.xlabel('Value')
|
|
494
|
+
plt.ylabel('Frequency')
|
|
495
|
+
plt.legend()
|
|
496
|
+
plt.show()
|
|
497
|
+
return hds_sequence
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
### PCA for n_dim > 2:
|
|
501
|
+
|
|
502
|
+
if normalize:
|
|
503
|
+
data_to_plot_raw = initial_samples
|
|
504
|
+
else:
|
|
505
|
+
data_to_plot_raw = stats.qmc.scale(initial_samples, bounds_min_orig, bounds_max_orig)
|
|
506
|
+
|
|
507
|
+
if n_dimensions > 2:
|
|
508
|
+
pca = PCA(n_components=2)
|
|
509
|
+
data_to_plot = pca.fit_transform(data_to_plot_raw)
|
|
510
|
+
hds_sequence_plot = pca.transform(hds_sequence)
|
|
511
|
+
sobol_samples_plot = pca.transform(sobol_samples)
|
|
512
|
+
origins_plot = pca.transform(origins)
|
|
513
|
+
title_str = f'Parameter Space (PCA): n={n_samples}, D={n_dimensions}'
|
|
514
|
+
xlabel_str = f'Principal Component 1 ({pca.explained_variance_ratio_[0]:.1%})'
|
|
515
|
+
ylabel_str = f'Principal Component 2 ({pca.explained_variance_ratio_[1]:.1%})'
|
|
516
|
+
else:
|
|
517
|
+
data_to_plot = data_to_plot_raw
|
|
518
|
+
hds_sequence_plot = hds_sequence
|
|
519
|
+
sobol_samples_plot = sobol_samples
|
|
520
|
+
origins_plot = origins
|
|
521
|
+
title_str = f'Parameter Space: n={n_samples}, D={n_dimensions}'
|
|
522
|
+
xlabel_str = f'Dimension 0'
|
|
523
|
+
ylabel_str = f'Dimension 1'
|
|
524
|
+
|
|
525
|
+
# plot histograms
|
|
526
|
+
fig, ax = plt.subplots(1,2,figsize=(9,5))
|
|
527
|
+
ax[0].hist(hds_sequence.flatten(), bins=30, label='HDS Samples')
|
|
528
|
+
ax[0].set_title('HDS Distribution')
|
|
529
|
+
ax[0].set_ylabel('')
|
|
530
|
+
ax[0].set_yticks([])
|
|
531
|
+
ax[0].set_xticks([])
|
|
532
|
+
ax[0].set_xlabel('')
|
|
533
|
+
ax[1].hist(sobol_samples.flatten(), bins=30, label='Sobol Samples')
|
|
534
|
+
ax[1].set_title('Sobol Distribution')
|
|
535
|
+
ax[1].set_ylabel('')
|
|
536
|
+
ax[1].set_xlabel('')
|
|
537
|
+
ax[1].set_xticks([])
|
|
538
|
+
ax[1].set_yticks([])
|
|
539
|
+
|
|
540
|
+
plt.tight_layout()
|
|
541
|
+
plt.show()
|
|
542
|
+
|
|
543
|
+
# dark visualization parameters for better sample visuals
|
|
544
|
+
plt.rcParams['figure.facecolor'] = 'black'
|
|
545
|
+
plt.rcParams['axes.facecolor'] = 'black'
|
|
546
|
+
plt.rcParams['text.color'] = 'white'
|
|
547
|
+
plt.rcParams['axes.labelcolor'] = 'white'
|
|
548
|
+
plt.rcParams['xtick.color'] = 'white'
|
|
549
|
+
plt.rcParams['ytick.color'] = 'white'
|
|
550
|
+
plt.rcParams['axes.edgecolor'] = 'white'
|
|
551
|
+
plt.rcParams['grid.color'] = 'white'
|
|
552
|
+
plt.rcParams['lines.color'] = 'white'
|
|
553
|
+
|
|
554
|
+
# plot samples in PCA space
|
|
555
|
+
fig, ax = plt.subplots(figsize=(7, 6))
|
|
556
|
+
|
|
557
|
+
# samples
|
|
558
|
+
ax.scatter(data_to_plot[:, 0], data_to_plot[:, 1], zorder=0, color='deepskyblue', label='Initial QMC Data', alpha=0.5, s=1.5)
|
|
559
|
+
ax.scatter(hds_sequence_plot[:, 0], hds_sequence_plot[:, 1], color='yellow', s=5, zorder=5, label='HDS Samples')
|
|
560
|
+
ax.scatter(sobol_samples_plot[:, 0], sobol_samples_plot[:, 1], color='red', s=8, label='Sobol Samples')
|
|
561
|
+
|
|
562
|
+
# data hypercube boundary
|
|
563
|
+
min_plot = np.min(data_to_plot, axis=0)
|
|
564
|
+
max_plot = np.max(data_to_plot, axis=0)
|
|
565
|
+
width = max_plot[0] - min_plot[0]
|
|
566
|
+
height = max_plot[1] - min_plot[1]
|
|
567
|
+
hypercube_boundary = Rectangle((min_plot[0], min_plot[1]), width, height, fill=False, color='deepskyblue', linewidth=1.5, linestyle='--', label='Bounds', zorder=6)
|
|
568
|
+
|
|
569
|
+
ax.add_patch(hypercube_boundary)
|
|
570
|
+
ax.set_title(title_str, fontweight='bold', fontsize=14)
|
|
571
|
+
ax.set_xlabel(xlabel_str)
|
|
572
|
+
ax.set_ylabel(ylabel_str)
|
|
573
|
+
ax.axis(False)
|
|
574
|
+
ax.legend(loc=(0.7,0.75), fontsize=11)
|
|
575
|
+
|
|
576
|
+
plt.tight_layout()
|
|
577
|
+
plt.show()
|
|
578
|
+
|
|
579
|
+
return hds_sequence
|
|
@@ -0,0 +1,740 @@
|
|
|
1
|
+
# global imports
|
|
2
|
+
import numpy as np
|
|
3
|
+
from scipy import stats
|
|
4
|
+
epsilon = 1e-12 # small epsilon to prevent zero-point errors
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
### test functions, for local testing ###
|
|
8
|
+
|
|
9
|
+
def rastrigin(x, vectorized=False):
|
|
10
|
+
'''Rastrigin test function, for local testing.'''
|
|
11
|
+
|
|
12
|
+
A = 10 # rastrigin coefficient
|
|
13
|
+
|
|
14
|
+
# check if x is a matrix (2D) or a single vector (1D)
|
|
15
|
+
matrix_flag = x.ndim > 1
|
|
16
|
+
if matrix_flag:
|
|
17
|
+
# for x of (popsize, dimensions)
|
|
18
|
+
n = x.shape[1]
|
|
19
|
+
rastrigin_value = A * n + np.sum(x**2 - A * np.cos(2 * np.pi * x), axis=1)
|
|
20
|
+
else:
|
|
21
|
+
# for single solution vector x
|
|
22
|
+
n = x.shape[0]
|
|
23
|
+
rastrigin_value = A * n + np.sum(x**2 - A * np.cos(2 * np.pi * x))
|
|
24
|
+
|
|
25
|
+
return rastrigin_value
|
|
26
|
+
|
|
27
|
+
def ackley(x, vectorized=False):
|
|
28
|
+
'''Ackley test function, for local testing.'''
|
|
29
|
+
|
|
30
|
+
# check if x is a matrix (2D) or a single vector (1D)
|
|
31
|
+
matrix_flag = x.ndim > 1
|
|
32
|
+
if matrix_flag:
|
|
33
|
+
# for x of (popsize, dimensions)
|
|
34
|
+
n = x.shape[1]
|
|
35
|
+
arg1 = -0.2 * np.sqrt(1/n * np.sum(x**2, axis=1))
|
|
36
|
+
arg2 = 1/n * np.sum(np.cos(2 * np.pi * x), axis=1)
|
|
37
|
+
else:
|
|
38
|
+
# for single solution vector x
|
|
39
|
+
n = x.shape[0]
|
|
40
|
+
arg1 = -0.2 * np.sqrt(1/n * np.sum(x**2))
|
|
41
|
+
arg2 = 1/n * np.sum(np.cos(2 * np.pi * x))
|
|
42
|
+
|
|
43
|
+
ackley_val = -20 * np.exp(arg1) - np.exp(arg2) + 20 + np.exp(1)
|
|
44
|
+
|
|
45
|
+
return ackley_val
|
|
46
|
+
|
|
47
|
+
def sphere(x, vectorized=False):
|
|
48
|
+
'''Sphere test function, for local testing.'''
|
|
49
|
+
|
|
50
|
+
# check if x is a matrix (2D) or a single vector (1D)
|
|
51
|
+
matrix_flag = x.ndim > 1
|
|
52
|
+
if matrix_flag:
|
|
53
|
+
sphere_val = np.sum(x**2, axis=1)
|
|
54
|
+
else:
|
|
55
|
+
sphere_val = np.sum(x**2)
|
|
56
|
+
|
|
57
|
+
return sphere_val
|
|
58
|
+
|
|
59
|
+
def shifted_function(func, shift_vector, vectorized=False):
|
|
60
|
+
'''
|
|
61
|
+
Objective:
|
|
62
|
+
- Shifts the global optimum of the given test function.
|
|
63
|
+
|
|
64
|
+
Inputs:
|
|
65
|
+
- func: Original test function.
|
|
66
|
+
- shift_vector: 1D array of the new optimum.
|
|
67
|
+
|
|
68
|
+
Outputs:
|
|
69
|
+
- shifted_func: New function with the shifted optimum.
|
|
70
|
+
'''
|
|
71
|
+
|
|
72
|
+
def shifted_func(x, vectorized=False):
|
|
73
|
+
return func(x - shift_vector, vectorized=vectorized)
|
|
74
|
+
return shifted_func
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
### verbose plotting functions ###
|
|
78
|
+
|
|
79
|
+
def plot_mutations(n_points=100000):
|
|
80
|
+
'''
|
|
81
|
+
Plots the distribution of mutation factors for all mutation strategies.
|
|
82
|
+
'''
|
|
83
|
+
|
|
84
|
+
# ensure integer n_points
|
|
85
|
+
n_points = int(n_points)
|
|
86
|
+
|
|
87
|
+
# import matplotlib
|
|
88
|
+
import matplotlib.pyplot as plt
|
|
89
|
+
|
|
90
|
+
# mutation plot params
|
|
91
|
+
dimensions = 1 # for plotting
|
|
92
|
+
peak_loc = 0.5
|
|
93
|
+
initial_std_loc = 0.25
|
|
94
|
+
local_std = 0.33
|
|
95
|
+
|
|
96
|
+
loc_signs = np.random.choice([-1.0, 1.0], size=(n_points, 1), p=[0.5, 0.5])
|
|
97
|
+
locs = loc_signs * peak_loc
|
|
98
|
+
base_mutations = np.random.normal(loc=0.0, scale=initial_std_loc, size=(n_points, dimensions))
|
|
99
|
+
global_muts = base_mutations + locs
|
|
100
|
+
global_muts_flat = global_muts.flatten()
|
|
101
|
+
|
|
102
|
+
local_muts = np.random.normal(loc=0.0, scale=local_std, size=(n_points, dimensions))
|
|
103
|
+
local_muts_flat = local_muts.flatten()
|
|
104
|
+
|
|
105
|
+
try: # in case seaborn is not imported
|
|
106
|
+
import seaborn as sns
|
|
107
|
+
sns.set_style('dark')
|
|
108
|
+
sns.histplot(x=global_muts_flat, bins=50, edgecolor='black',stat='density',kde=True,color='deepskyblue',alpha=0.85,label='Global')
|
|
109
|
+
sns.histplot(x=local_muts_flat, bins=50, edgecolor='black',stat='density',kde=True,color='darkorange',alpha=0.85,label='Local')
|
|
110
|
+
except:
|
|
111
|
+
plt.rcParams['figure.facecolor'] = 'black'
|
|
112
|
+
plt.rcParams['axes.facecolor'] = 'black'
|
|
113
|
+
plt.rcParams['text.color'] = 'white'
|
|
114
|
+
plt.rcParams['axes.labelcolor'] = 'white'
|
|
115
|
+
plt.rcParams['xtick.color'] = 'white'
|
|
116
|
+
plt.rcParams['ytick.color'] = 'white'
|
|
117
|
+
plt.rcParams['axes.edgecolor'] = 'white'
|
|
118
|
+
plt.rcParams['grid.color'] = 'white'
|
|
119
|
+
plt.rcParams['lines.color'] = 'white'
|
|
120
|
+
plt.hist(global_muts_flat, bins=50, edgecolor='black',density=True,color='deepskyblue',alpha=0.85,label='Global')
|
|
121
|
+
plt.hist(local_muts_flat, bins=50, edgecolor='black',density=True,color='darkorange',alpha=0.85,label='Local')
|
|
122
|
+
finally:
|
|
123
|
+
plt.title('Mutation Factor Distribution',fontsize=16)
|
|
124
|
+
plt.xlabel('Mutation Factor',fontsize=15)
|
|
125
|
+
plt.ylabel('Frequency',fontsize=15)
|
|
126
|
+
plt.legend(fontsize=15)
|
|
127
|
+
|
|
128
|
+
plt.tight_layout()
|
|
129
|
+
plt.show()
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def plot_trajectories(obj_function, pop_history, best_history, bounds, num_to_plot):
|
|
133
|
+
'''
|
|
134
|
+
Plots the solution position history.
|
|
135
|
+
'''
|
|
136
|
+
|
|
137
|
+
from sklearn.decomposition import PCA
|
|
138
|
+
import matplotlib.pyplot as plt
|
|
139
|
+
|
|
140
|
+
# visualization params
|
|
141
|
+
plt.rcParams['figure.facecolor'] = 'black'
|
|
142
|
+
plt.rcParams['axes.facecolor'] = 'black'
|
|
143
|
+
plt.rcParams['text.color'] = 'white'
|
|
144
|
+
plt.rcParams['axes.labelcolor'] = 'white'
|
|
145
|
+
plt.rcParams['xtick.color'] = 'white'
|
|
146
|
+
plt.rcParams['ytick.color'] = 'white'
|
|
147
|
+
plt.rcParams['axes.edgecolor'] = 'white'
|
|
148
|
+
plt.rcParams['grid.color'] = 'white'
|
|
149
|
+
plt.rcParams['lines.color'] = 'white'
|
|
150
|
+
|
|
151
|
+
original_dims = bounds.shape[0]
|
|
152
|
+
|
|
153
|
+
# convert to arrays
|
|
154
|
+
plot_pop_history = np.array(pop_history)
|
|
155
|
+
plot_best_history = np.array(best_history)
|
|
156
|
+
|
|
157
|
+
# ensure bounds array has more than 2 dimensions
|
|
158
|
+
if original_dims > 2:
|
|
159
|
+
pca = PCA(n_components=2)
|
|
160
|
+
|
|
161
|
+
# reshape data to fit PCA
|
|
162
|
+
all_data = plot_pop_history.reshape(-1, original_dims)
|
|
163
|
+
|
|
164
|
+
# fit PCA on population history
|
|
165
|
+
pca.fit(all_data)
|
|
166
|
+
|
|
167
|
+
# reshape data
|
|
168
|
+
num_generations = plot_pop_history.shape[0]
|
|
169
|
+
popsize = plot_pop_history.shape[1]
|
|
170
|
+
|
|
171
|
+
# transform and reshape dadta
|
|
172
|
+
plot_pop_history = pca.transform(all_data).reshape(num_generations, popsize, 2)
|
|
173
|
+
plot_best_history = pca.transform(plot_best_history)
|
|
174
|
+
|
|
175
|
+
# adjust bounds
|
|
176
|
+
combined_transformed_data = np.concatenate([plot_pop_history.reshape(-1, 2), plot_best_history], axis=0)
|
|
177
|
+
|
|
178
|
+
# ensure combined data is not empty
|
|
179
|
+
if combined_transformed_data.size > 0:
|
|
180
|
+
min_vals = np.min(combined_transformed_data, axis=0)
|
|
181
|
+
max_vals = np.max(combined_transformed_data, axis=0)
|
|
182
|
+
x_min, x_max = min_vals[0], max_vals[0]
|
|
183
|
+
y_min, y_max = min_vals[1], max_vals[1]
|
|
184
|
+
else:
|
|
185
|
+
x_min, x_max = -1, 1
|
|
186
|
+
y_min, y_max = -1, 1
|
|
187
|
+
|
|
188
|
+
else:
|
|
189
|
+
x_min, x_max = bounds[0, 0], bounds[0, 1]
|
|
190
|
+
y_min, y_max = bounds[1, 0], bounds[1, 1]
|
|
191
|
+
|
|
192
|
+
plt.figure(figsize=(9, 7))
|
|
193
|
+
plt.xlabel('Principal Component 0')
|
|
194
|
+
plt.ylabel('Principal Component 1')
|
|
195
|
+
plt.title('Solution Trajectories')
|
|
196
|
+
|
|
197
|
+
# objective function contour plot
|
|
198
|
+
x = np.linspace(x_min, x_max, 100)
|
|
199
|
+
y = np.linspace(y_min, y_max, 100)
|
|
200
|
+
X, Y = np.meshgrid(x, y)
|
|
201
|
+
xy_coords = np.vstack([X.ravel(), Y.ravel()]).T
|
|
202
|
+
|
|
203
|
+
if plot_pop_history is not None:
|
|
204
|
+
indices_to_plot = np.random.choice(plot_pop_history.shape[1], min(num_to_plot, plot_pop_history.shape[1]), replace=False)
|
|
205
|
+
|
|
206
|
+
for i in indices_to_plot:
|
|
207
|
+
x_coords = plot_pop_history[:, i, 0]
|
|
208
|
+
y_coords = plot_pop_history[:, i, 1]
|
|
209
|
+
plt.plot(x_coords, y_coords, linestyle='-', marker='o', markersize=3, alpha=0.67, zorder=1)
|
|
210
|
+
|
|
211
|
+
# plot path of best solution
|
|
212
|
+
if plot_best_history is not None:
|
|
213
|
+
x_coords = plot_best_history[:, 0]
|
|
214
|
+
y_coords = plot_best_history[:, 1]
|
|
215
|
+
plt.plot(x_coords, y_coords, linestyle='-', marker='x', markersize=8, color='red', label='Best Solution Trajectory',
|
|
216
|
+
alpha=0.85, zorder=2)
|
|
217
|
+
plt.scatter(x_coords[0], y_coords[0], color='cyan', marker='d', s=300, label='Initial Best Solution', alpha=0.8, zorder=5)
|
|
218
|
+
plt.scatter(x_coords[-1], y_coords[-1], color='cyan', marker='X', s=350, label='Final Best Solution', alpha=0.8, zorder=5)
|
|
219
|
+
|
|
220
|
+
plt.legend(fontsize=10,markerscale=0.67)
|
|
221
|
+
plt.grid(False)
|
|
222
|
+
plt.show()
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
### evolution algorithm ###
|
|
226
|
+
|
|
227
|
+
def initialize_population(popsize, bounds, init, hds_weights, seed, verbose):
|
|
228
|
+
'''
|
|
229
|
+
Objective:
|
|
230
|
+
- Initializes a population using Sobol, Adaptive Hyperellipsoid, or a custom population.
|
|
231
|
+
'''
|
|
232
|
+
|
|
233
|
+
# misc extracts
|
|
234
|
+
n_dimensions = bounds.shape[0]
|
|
235
|
+
|
|
236
|
+
# if input is not a string assume it is the initial population
|
|
237
|
+
if isinstance(init, str):
|
|
238
|
+
init = init.lower() # ensure lowercase string
|
|
239
|
+
|
|
240
|
+
# generate adaptive hypersphere sequence
|
|
241
|
+
if init == 'hds':
|
|
242
|
+
# import hds
|
|
243
|
+
try:
|
|
244
|
+
from . import hyperellipsoid_sampling as hds
|
|
245
|
+
except ImportError:
|
|
246
|
+
import hyperellipsoid_sampling as hds
|
|
247
|
+
|
|
248
|
+
# generate samples
|
|
249
|
+
if verbose:
|
|
250
|
+
print('Initializing Hyperellipsoid vectors.')
|
|
251
|
+
initial_population = hds.sample(popsize, bounds, weights=hds_weights,
|
|
252
|
+
seed=seed, verbose=False)
|
|
253
|
+
|
|
254
|
+
# generate sobol sequence
|
|
255
|
+
elif init == 'sobol':
|
|
256
|
+
if verbose:
|
|
257
|
+
print('Initializing Sobol vectors.')
|
|
258
|
+
import warnings
|
|
259
|
+
warnings.filterwarnings('ignore', category=UserWarning) # ignore power-of-2 warning
|
|
260
|
+
sobol_sampler = stats.qmc.Sobol(d=n_dimensions, seed=seed)
|
|
261
|
+
sobol_samples_unit = sobol_sampler.random(n=popsize)
|
|
262
|
+
initial_population = stats.qmc.scale(sobol_samples_unit, bounds[:, 0], bounds[:, 1])
|
|
263
|
+
|
|
264
|
+
elif init == 'lhs':
|
|
265
|
+
if verbose:
|
|
266
|
+
print('Initializing Latin Hypercube vectors.')
|
|
267
|
+
lhs_sampler = stats.qmc.LatinHypercube(d=n_dimensions, seed=seed)
|
|
268
|
+
lhs_samples_unit = lhs_sampler.random(n=popsize)
|
|
269
|
+
initial_population = stats.qmc.scale(lhs_samples_unit, bounds[:, 0], bounds[:, 1])
|
|
270
|
+
|
|
271
|
+
elif init == 'random':
|
|
272
|
+
if verbose:
|
|
273
|
+
print('Initializing random vectors.')
|
|
274
|
+
initial_population = np.random.uniform(low=bounds[:, 0], high=bounds[:, 1], size=(popsize, n_dimensions))
|
|
275
|
+
else:
|
|
276
|
+
initial_population = init
|
|
277
|
+
if verbose:
|
|
278
|
+
print('Initializing custom population.')
|
|
279
|
+
|
|
280
|
+
return initial_population
|
|
281
|
+
|
|
282
|
+
def evolve_generation(obj_function, population, fitnesses, best_solution,
|
|
283
|
+
bounds, entangle_rate, generation, maxiter,
|
|
284
|
+
vectorized, *args):
|
|
285
|
+
'''
|
|
286
|
+
Objective:
|
|
287
|
+
- Evolves the population for the current generation.
|
|
288
|
+
- Dynamic crossover rate:
|
|
289
|
+
- Worst solution CR = 1.0, best solution CR = 0.33.
|
|
290
|
+
- Local & global mutation factor distributions.
|
|
291
|
+
- Can be displayed using the 'plot_mutations' function.
|
|
292
|
+
- Greedy selection:
|
|
293
|
+
- New solution vector is chosen as the better between of trial and current vectors.
|
|
294
|
+
- Covariance reinitialization is handled externally.
|
|
295
|
+
'''
|
|
296
|
+
|
|
297
|
+
# crossover parameters
|
|
298
|
+
base_crossover_proba = 1.0
|
|
299
|
+
min_crossover_proba = 0.33
|
|
300
|
+
|
|
301
|
+
if vectorized:
|
|
302
|
+
dimensions, popsize = population.shape
|
|
303
|
+
|
|
304
|
+
# select random entangled partners for all solutions
|
|
305
|
+
random_indices = np.random.randint(0, popsize, size=popsize)
|
|
306
|
+
entangled_partners = population[:, random_indices]
|
|
307
|
+
|
|
308
|
+
# global mutation factor:
|
|
309
|
+
global_std = 0.25
|
|
310
|
+
global_peaks = 0.5
|
|
311
|
+
is_positive_peak = np.random.choice([True, False], p=[0.5, 0.5])
|
|
312
|
+
global_mutation = np.random.normal(loc=global_peaks, scale=global_std,
|
|
313
|
+
size=(dimensions,1)) if is_positive_peak else np.random.normal(loc=-global_peaks,
|
|
314
|
+
scale=global_std, size=(dimensions,1))
|
|
315
|
+
|
|
316
|
+
# local mutation factor:
|
|
317
|
+
local_mutation_std = 0.33
|
|
318
|
+
local_mutation = np.random.normal(0.0, local_mutation_std, size=(dimensions,1))
|
|
319
|
+
|
|
320
|
+
# best solution as a column vector (dimensions, 1)
|
|
321
|
+
best_solution_broadcast = best_solution[:, np.newaxis]
|
|
322
|
+
|
|
323
|
+
# identify solution indices to use Spooky-Best strategy
|
|
324
|
+
local_indices = np.random.rand(1, popsize) < entangle_rate
|
|
325
|
+
|
|
326
|
+
# global mutations
|
|
327
|
+
mutant_vectors_current = population + global_mutation * (best_solution_broadcast - entangled_partners)
|
|
328
|
+
mutant_vectors_random = entangled_partners + global_mutation * (population - entangled_partners)
|
|
329
|
+
|
|
330
|
+
# 50% chance of using Spooky-Random, otherwise Spooky-Current
|
|
331
|
+
entangled_random_indices = np.random.rand(1, popsize) < 0.5
|
|
332
|
+
|
|
333
|
+
# select between the two global mutations
|
|
334
|
+
global_mutants = np.where(entangled_random_indices, mutant_vectors_random, mutant_vectors_current)
|
|
335
|
+
|
|
336
|
+
# select between local and global mutations
|
|
337
|
+
mutant_vectors = np.where(local_indices, best_solution_broadcast + local_mutation * (population - entangled_partners),
|
|
338
|
+
global_mutants)
|
|
339
|
+
|
|
340
|
+
# rank solutions by fitness
|
|
341
|
+
sorted_indices = np.argsort(fitnesses)
|
|
342
|
+
ranks = np.zeros_like(fitnesses)
|
|
343
|
+
ranks[sorted_indices] = np.arange(popsize)
|
|
344
|
+
max_rank = popsize - 1
|
|
345
|
+
|
|
346
|
+
# calculate adaptive crossover rates
|
|
347
|
+
relative_fitness = (max_rank - ranks) / max_rank
|
|
348
|
+
adaptive_crossover_proba_raw = (1 - base_crossover_proba) + base_crossover_proba * relative_fitness
|
|
349
|
+
adaptive_crossover_proba = np.maximum(adaptive_crossover_proba_raw, min_crossover_proba)
|
|
350
|
+
|
|
351
|
+
# apply crossover to create trial vectors
|
|
352
|
+
crossover_indices = np.random.rand(dimensions, popsize) < adaptive_crossover_proba
|
|
353
|
+
trial_vectors = np.where(crossover_indices, mutant_vectors, population)
|
|
354
|
+
|
|
355
|
+
# clip trial vectors to bounds
|
|
356
|
+
trial_vectors = np.clip(trial_vectors, bounds[:, np.newaxis, 0], bounds[:, np.newaxis, 1])
|
|
357
|
+
|
|
358
|
+
# steady-state elitism selection
|
|
359
|
+
trial_fitnesses = obj_function(trial_vectors.T, *args)
|
|
360
|
+
selection_indices = trial_fitnesses < fitnesses
|
|
361
|
+
|
|
362
|
+
new_population = np.where(selection_indices[np.newaxis, :], trial_vectors, population)
|
|
363
|
+
new_fitnesses = np.where(selection_indices, trial_fitnesses, fitnesses)
|
|
364
|
+
|
|
365
|
+
return new_population, new_fitnesses
|
|
366
|
+
|
|
367
|
+
else:
|
|
368
|
+
# extract shape
|
|
369
|
+
popsize, dimensions = population.shape
|
|
370
|
+
|
|
371
|
+
# initialize arrays
|
|
372
|
+
new_population = np.zeros_like(population)
|
|
373
|
+
new_fitnesses = np.zeros_like(fitnesses)
|
|
374
|
+
|
|
375
|
+
# global mutation factor
|
|
376
|
+
global_std = 0.25
|
|
377
|
+
global_peaks = 0.5
|
|
378
|
+
is_positive_peak = np.random.choice([True, False], p=[0.5, 0.5])
|
|
379
|
+
global_mutation = np.random.normal(loc=global_peaks, scale=global_std,
|
|
380
|
+
size=dimensions) if is_positive_peak else np.random.normal(loc=-global_peaks,
|
|
381
|
+
scale=global_std, size=dimensions)
|
|
382
|
+
|
|
383
|
+
# local mutation factor
|
|
384
|
+
local_mutation_std = 0.33
|
|
385
|
+
local_mutation = np.random.normal(0.0, local_mutation_std, size=dimensions)
|
|
386
|
+
|
|
387
|
+
# adaptive crossover calculations
|
|
388
|
+
sorted_indices = np.argsort(fitnesses)
|
|
389
|
+
ranks = np.zeros_like(fitnesses)
|
|
390
|
+
ranks[sorted_indices] = np.arange(popsize)
|
|
391
|
+
max_rank = popsize - 1
|
|
392
|
+
relative_fitness = (max_rank - ranks) / max_rank
|
|
393
|
+
adaptive_crossover_proba_raw = (1 - base_crossover_proba) + base_crossover_proba * relative_fitness
|
|
394
|
+
adaptive_crossover_proba = np.maximum(adaptive_crossover_proba_raw, min_crossover_proba)
|
|
395
|
+
|
|
396
|
+
# loop through each solution in population
|
|
397
|
+
for i in range(popsize):
|
|
398
|
+
solution = population[i]
|
|
399
|
+
current_fitness = fitnesses[i]
|
|
400
|
+
|
|
401
|
+
# select random 'entangled' partner indices
|
|
402
|
+
random_index = np.random.randint(0, popsize)
|
|
403
|
+
entangled_partner = population[random_index]
|
|
404
|
+
|
|
405
|
+
# apply mutations
|
|
406
|
+
if np.random.rand() < entangle_rate:
|
|
407
|
+
mutant_vector = best_solution + local_mutation * (solution - entangled_partner) # original
|
|
408
|
+
else:
|
|
409
|
+
# 50% chance of moving around current location
|
|
410
|
+
mutant_vector = solution + global_mutation * (best_solution - entangled_partner) # original
|
|
411
|
+
# 50% chance of moving to entangled partner
|
|
412
|
+
if np.random.rand() < 0.5:
|
|
413
|
+
mutant_vector = entangled_partner + global_mutation * (solution - entangled_partner) # original
|
|
414
|
+
|
|
415
|
+
crossover_indices = np.random.rand(dimensions) < adaptive_crossover_proba[i]
|
|
416
|
+
trial_vector = np.where(crossover_indices, mutant_vector, solution)
|
|
417
|
+
|
|
418
|
+
# clip trial vectors to bounds
|
|
419
|
+
trial_vector = np.clip(trial_vector, bounds[:, 0], bounds[:, 1])
|
|
420
|
+
|
|
421
|
+
# steady-state elitism selection
|
|
422
|
+
trial_fitness = obj_function(trial_vector, *args)
|
|
423
|
+
|
|
424
|
+
if trial_fitness < current_fitness:
|
|
425
|
+
new_population[i] = trial_vector
|
|
426
|
+
new_fitnesses[i] = trial_fitness
|
|
427
|
+
else:
|
|
428
|
+
new_population[i] = solution
|
|
429
|
+
new_fitnesses[i] = current_fitness
|
|
430
|
+
|
|
431
|
+
return new_population, new_fitnesses
|
|
432
|
+
|
|
433
|
+
def covariance_reinit(population, current_fitnesses, bounds, vectorized):
|
|
434
|
+
'''
|
|
435
|
+
Objective:
|
|
436
|
+
- Reinitializes the worst 33% solutions in the population.
|
|
437
|
+
- Locations are determined based on a Gaussian distribution from the covariance matrix of 25% best solutions.
|
|
438
|
+
- Noise is added to enhance diversity.
|
|
439
|
+
- Probability decays to 33% at the 33% generation.
|
|
440
|
+
- Conceptualizes particles tunneling to a more stable location.
|
|
441
|
+
'''
|
|
442
|
+
|
|
443
|
+
# reshape depending on vectorized input
|
|
444
|
+
if vectorized:
|
|
445
|
+
dimensions, popsize = population.shape
|
|
446
|
+
else:
|
|
447
|
+
popsize, dimensions = population.shape
|
|
448
|
+
|
|
449
|
+
# handle case where not enough points for covariance matrix
|
|
450
|
+
if popsize < dimensions + 1:
|
|
451
|
+
return population
|
|
452
|
+
|
|
453
|
+
# keep 25% of best solutions
|
|
454
|
+
num_to_keep_factor = 0.25
|
|
455
|
+
num_to_keep = int(popsize * num_to_keep_factor)
|
|
456
|
+
if num_to_keep <= dimensions:
|
|
457
|
+
num_to_keep = dimensions + 1 # minimum sample size scaled by dimensions
|
|
458
|
+
|
|
459
|
+
# identify best solutions to calculate covariance gaussian model
|
|
460
|
+
sorted_indices = np.argsort(current_fitnesses)
|
|
461
|
+
best_indices = sorted_indices[:num_to_keep]
|
|
462
|
+
if vectorized:
|
|
463
|
+
best_solutions = population[:, best_indices]
|
|
464
|
+
else:
|
|
465
|
+
best_solutions = population[best_indices]
|
|
466
|
+
|
|
467
|
+
# learn full-covariance matrix
|
|
468
|
+
if vectorized:
|
|
469
|
+
mean_vector = np.mean(best_solutions, axis=1)
|
|
470
|
+
cov_matrix = np.cov(best_solutions)
|
|
471
|
+
else:
|
|
472
|
+
mean_vector = np.mean(best_solutions, axis=0)
|
|
473
|
+
cov_matrix = np.cov(best_solutions, rowvar=False)
|
|
474
|
+
|
|
475
|
+
# add epsilon to the diagonal to prevent singular matrix issues
|
|
476
|
+
cov_matrix += np.eye(dimensions) * epsilon
|
|
477
|
+
|
|
478
|
+
# identify solutions to be reset
|
|
479
|
+
reset_population = 0.33
|
|
480
|
+
num_to_replace = int(popsize * reset_population)
|
|
481
|
+
worst_indices = sorted_indices[-num_to_replace:]
|
|
482
|
+
|
|
483
|
+
# new solutions sampled from multivariate normal distribution
|
|
484
|
+
if vectorized:
|
|
485
|
+
new_solutions_sampled = np.random.multivariate_normal(mean=mean_vector, cov=cov_matrix, size=num_to_replace).T
|
|
486
|
+
else:
|
|
487
|
+
new_solutions_sampled = np.random.multivariate_normal(mean=mean_vector, cov=cov_matrix, size=num_to_replace)
|
|
488
|
+
|
|
489
|
+
# add noise for exploration
|
|
490
|
+
noise_scale = (bounds[:, 1] - bounds[:, 0]) / 20.0
|
|
491
|
+
if vectorized:
|
|
492
|
+
noise = np.random.normal(0, noise_scale[:, np.newaxis], size=new_solutions_sampled.shape)
|
|
493
|
+
new_solutions = new_solutions_sampled + noise
|
|
494
|
+
else:
|
|
495
|
+
noise = np.random.normal(0, noise_scale, size=new_solutions_sampled.shape)
|
|
496
|
+
new_solutions = new_solutions_sampled + noise
|
|
497
|
+
|
|
498
|
+
# clip new solutions to bounds
|
|
499
|
+
if vectorized:
|
|
500
|
+
population[:, worst_indices] = np.clip(new_solutions, bounds[:, np.newaxis, 0], bounds[:, np.newaxis, 1])
|
|
501
|
+
else:
|
|
502
|
+
population[worst_indices] = np.clip(new_solutions, bounds[:, 0], bounds[:, 1])
|
|
503
|
+
|
|
504
|
+
return population
|
|
505
|
+
|
|
506
|
+
def optimize(func, bounds, args=(),
|
|
507
|
+
init='sobol', popsize=None, maxiter=100,
|
|
508
|
+
entangle_rate=0.33, polish=True,
|
|
509
|
+
patience=np.inf, vectorized=False,
|
|
510
|
+
verbose=True, num_to_plot=10,
|
|
511
|
+
hds_weights=None,
|
|
512
|
+
seed=None):
|
|
513
|
+
'''
|
|
514
|
+
Objective:
|
|
515
|
+
- Searches for the optimal solution to minimize the objective function.
|
|
516
|
+
- Conceptualizes quantum stellar particles searching for the most stable energy state.
|
|
517
|
+
- Mutation factors can be visualized with plot_mutations().
|
|
518
|
+
Inputs:
|
|
519
|
+
- func: Objective function to minimize.
|
|
520
|
+
- bounds: Parameter bounds.
|
|
521
|
+
- *args: Input arguments for objective function.
|
|
522
|
+
- init: Initial population sampling method.
|
|
523
|
+
- Options are 'sobol', 'random', 'hds', 'lhs', or a custom sample sequence.
|
|
524
|
+
- Defaults to 'sobol'.
|
|
525
|
+
- 'hds' to accelerate convergence with slower initialization.
|
|
526
|
+
- popsize: Number of solution vectors to evolve.
|
|
527
|
+
- Defaults to 10 * n_dimensions.
|
|
528
|
+
- maxiter: Number of generations to evolve.
|
|
529
|
+
- entangle_rate: Probability of solutions using the local Spooky-Best mutation strategy.
|
|
530
|
+
- Defaults to 0.33. This causes to the three mutation strategies to be applied equally.
|
|
531
|
+
- Decreasing leads to higher exploration.
|
|
532
|
+
- patience: Number of generations without improvement before early convergence.
|
|
533
|
+
- polish: Final polishing step using 'L-BFGS-B' minimization.
|
|
534
|
+
- vectorized: Boolean to accept vectorized objective functions.
|
|
535
|
+
- verbose: Displays prints and plots.
|
|
536
|
+
- num_to_plot: Number of solution trajectories to display in the verbose plot.
|
|
537
|
+
Outputs:
|
|
538
|
+
- (best_solution, best_fitness) tuple:
|
|
539
|
+
- best_solution: Best solution found.
|
|
540
|
+
- best_fitness: Fitness of the optimal solution.
|
|
541
|
+
'''
|
|
542
|
+
|
|
543
|
+
# set random seed
|
|
544
|
+
if seed == None:
|
|
545
|
+
np.random.seed()
|
|
546
|
+
else:
|
|
547
|
+
np.random.seed(seed)
|
|
548
|
+
|
|
549
|
+
# handle case where time is not imported
|
|
550
|
+
if verbose:
|
|
551
|
+
try:
|
|
552
|
+
import time
|
|
553
|
+
start_time = time.time()
|
|
554
|
+
except:
|
|
555
|
+
pass
|
|
556
|
+
|
|
557
|
+
# lowercase initialization
|
|
558
|
+
if type(init) == str:
|
|
559
|
+
init = init.lower()
|
|
560
|
+
|
|
561
|
+
# raise errors for invalid inputs:
|
|
562
|
+
# entangle rate error
|
|
563
|
+
if not 0.0 <= entangle_rate <= 1.0:
|
|
564
|
+
raise ValueError('Entanglement rate must be between [0,1].')
|
|
565
|
+
|
|
566
|
+
# initialization error
|
|
567
|
+
if (type(init) == str) and init not in ['sobol','hds','random','lhs']:
|
|
568
|
+
raise ValueError("Initial sampler must be one of ['sobol','random','ahs','lhs'], or a custom population.")
|
|
569
|
+
|
|
570
|
+
# patience error
|
|
571
|
+
if patience <= 1:
|
|
572
|
+
raise ValueError('Patience must be > 1 generation.')
|
|
573
|
+
|
|
574
|
+
# initialize histories
|
|
575
|
+
pop_history = []
|
|
576
|
+
best_history = []
|
|
577
|
+
|
|
578
|
+
# ensure bounds is array; shape (n_dimensions,2)
|
|
579
|
+
bounds = np.array(bounds)
|
|
580
|
+
n_dimensions = bounds.shape[0]
|
|
581
|
+
|
|
582
|
+
# if init is not a string, assume it is a custom population
|
|
583
|
+
if not isinstance(init, str):
|
|
584
|
+
popsize = init.shape[0]
|
|
585
|
+
|
|
586
|
+
# default popsize to 10*n_dimensions
|
|
587
|
+
if popsize == None:
|
|
588
|
+
popsize = 10*n_dimensions
|
|
589
|
+
|
|
590
|
+
# ensure integers
|
|
591
|
+
popsize, maxiter = int(popsize), int(maxiter)
|
|
592
|
+
|
|
593
|
+
# generate initial population
|
|
594
|
+
initial_population = initialize_population(popsize, bounds, init, hds_weights, seed, verbose)
|
|
595
|
+
if verbose:
|
|
596
|
+
print('\nEvolving population:')
|
|
597
|
+
|
|
598
|
+
# match differential evolution conventions
|
|
599
|
+
if vectorized:
|
|
600
|
+
initial_population = initial_population.T
|
|
601
|
+
initial_fitnesses = func(initial_population.T, *args)
|
|
602
|
+
else:
|
|
603
|
+
initial_fitnesses = np.array([func(sol, *args) for sol in initial_population])
|
|
604
|
+
|
|
605
|
+
# calculate initial best fitness
|
|
606
|
+
min_fitness_idx = np.argmin(initial_fitnesses)
|
|
607
|
+
initial_best_fitness = initial_fitnesses[min_fitness_idx]
|
|
608
|
+
|
|
609
|
+
# identify initial best solution
|
|
610
|
+
if vectorized:
|
|
611
|
+
initial_best_solution = initial_population[:, min_fitness_idx].copy()
|
|
612
|
+
else:
|
|
613
|
+
initial_best_solution = initial_population[min_fitness_idx].copy()
|
|
614
|
+
|
|
615
|
+
# initialze population and fitnesses
|
|
616
|
+
population = initial_population
|
|
617
|
+
current_fitnesses = initial_fitnesses
|
|
618
|
+
|
|
619
|
+
# add initial population to population history
|
|
620
|
+
if verbose:
|
|
621
|
+
if vectorized:
|
|
622
|
+
pop_history.append(population.T.copy())
|
|
623
|
+
else:
|
|
624
|
+
pop_history.append(population.copy())
|
|
625
|
+
|
|
626
|
+
# add initial best solution to best solution history
|
|
627
|
+
best_history.append(initial_best_solution.copy())
|
|
628
|
+
|
|
629
|
+
# initialize best solution and fitness
|
|
630
|
+
best_solution = initial_best_solution
|
|
631
|
+
best_fitness = initial_best_fitness
|
|
632
|
+
|
|
633
|
+
# iterate through generations
|
|
634
|
+
last_improvement_gen = 0
|
|
635
|
+
for generation in range(maxiter):
|
|
636
|
+
|
|
637
|
+
# evolve population
|
|
638
|
+
if vectorized:
|
|
639
|
+
population, current_fitnesses = evolve_generation(func, population, current_fitnesses, best_solution, bounds,
|
|
640
|
+
entangle_rate, generation, maxiter, vectorized, *args)
|
|
641
|
+
else:
|
|
642
|
+
population, current_fitnesses = evolve_generation(func, population, current_fitnesses, best_solution, bounds,
|
|
643
|
+
entangle_rate, generation, maxiter, vectorized, *args)
|
|
644
|
+
|
|
645
|
+
# update best solution found
|
|
646
|
+
min_fitness_idx = np.argmin(current_fitnesses)
|
|
647
|
+
current_best_fitness = current_fitnesses[min_fitness_idx]
|
|
648
|
+
|
|
649
|
+
# identify current best solution
|
|
650
|
+
if vectorized:
|
|
651
|
+
current_best_solution = population[:, min_fitness_idx].copy()
|
|
652
|
+
else:
|
|
653
|
+
current_best_solution = population[min_fitness_idx].copy()
|
|
654
|
+
|
|
655
|
+
# update best known solution
|
|
656
|
+
if current_best_fitness < best_fitness:
|
|
657
|
+
best_fitness = current_best_fitness
|
|
658
|
+
best_solution = current_best_solution
|
|
659
|
+
last_improvement_gen = 0
|
|
660
|
+
else:
|
|
661
|
+
last_improvement_gen += 1
|
|
662
|
+
|
|
663
|
+
# apply asymptotic covariance reinitialization to population
|
|
664
|
+
final_proba = 0.33
|
|
665
|
+
decay_generation = 0.33
|
|
666
|
+
reinit_proba = np.e**((np.log(final_proba)/(decay_generation*maxiter))*generation)
|
|
667
|
+
if np.random.rand() < reinit_proba:
|
|
668
|
+
population = covariance_reinit(population, current_fitnesses, bounds, vectorized=vectorized)
|
|
669
|
+
|
|
670
|
+
# clip population to bounds
|
|
671
|
+
if vectorized:
|
|
672
|
+
population = np.clip(population, bounds[:, np.newaxis, 0], bounds[:, np.newaxis, 1])
|
|
673
|
+
else:
|
|
674
|
+
population = np.clip(population, bounds[:, 0], bounds[:, 1])
|
|
675
|
+
|
|
676
|
+
# add to population history
|
|
677
|
+
if verbose:
|
|
678
|
+
if vectorized:
|
|
679
|
+
pop_history.append(population.T.copy())
|
|
680
|
+
else:
|
|
681
|
+
pop_history.append(population.copy())
|
|
682
|
+
best_history.append(best_solution.copy())
|
|
683
|
+
|
|
684
|
+
# print generation status
|
|
685
|
+
if verbose:
|
|
686
|
+
stdev = np.std(current_fitnesses)
|
|
687
|
+
print(f' Gen. {generation+1}/{maxiter} | f(x)={best_fitness:.2e} | stdev={stdev:.2e} | reinit={reinit_proba:.2f}')
|
|
688
|
+
|
|
689
|
+
# patience for early convergence
|
|
690
|
+
if (generation - last_improvement_gen) > patience:
|
|
691
|
+
if verbose:
|
|
692
|
+
print(f'\nEarly convergence: generations without improvement exceeds patience ({patience}).')
|
|
693
|
+
break
|
|
694
|
+
|
|
695
|
+
# polish final solution step via L-BFGS-B
|
|
696
|
+
if polish:
|
|
697
|
+
if verbose:
|
|
698
|
+
print('Polishing solution.')
|
|
699
|
+
try:
|
|
700
|
+
from scipy.optimize import minimize
|
|
701
|
+
if vectorized:
|
|
702
|
+
res = minimize(func, best_solution, args=args, bounds=bounds, method='L-BFGS-B', tol=0)
|
|
703
|
+
else:
|
|
704
|
+
res = minimize(func, best_solution, args=args, bounds=bounds, method='L-BFGS-B', tol=0)
|
|
705
|
+
if res.success:
|
|
706
|
+
best_solution = res.x
|
|
707
|
+
best_fitness = res.fun
|
|
708
|
+
except Exception as e:
|
|
709
|
+
print(f'Polishing failed: {e}')
|
|
710
|
+
|
|
711
|
+
# final solution prints
|
|
712
|
+
if verbose:
|
|
713
|
+
print('\nResults:')
|
|
714
|
+
|
|
715
|
+
# print best fitness
|
|
716
|
+
print(f'- f(x): {best_fitness:.2e}')
|
|
717
|
+
|
|
718
|
+
#print best solution
|
|
719
|
+
if len(best_solution)>3:
|
|
720
|
+
formatted_display = ', '.join([f'{val:.2e}' for val in best_solution[:3]])
|
|
721
|
+
print(f'- Solution: [{formatted_display}, ...]')
|
|
722
|
+
else:
|
|
723
|
+
formatted_display = ', '.join([f'{val:.2e}' for val in best_solution])
|
|
724
|
+
print(f'- Solution: [{formatted_display}]')
|
|
725
|
+
|
|
726
|
+
# print optimization time
|
|
727
|
+
try:
|
|
728
|
+
print(f'- Elapsed: {(time.time() - start_time):.3f}s')
|
|
729
|
+
except Exception as e:
|
|
730
|
+
print(f'- Elapsed: null') # case where time isn't imported
|
|
731
|
+
|
|
732
|
+
# plotting
|
|
733
|
+
print()
|
|
734
|
+
try:
|
|
735
|
+
plot_trajectories(func, pop_history, best_history, bounds, num_to_plot)
|
|
736
|
+
|
|
737
|
+
except Exception as e:
|
|
738
|
+
print(f'Failed to generate plots: {e}')
|
|
739
|
+
|
|
740
|
+
return (best_solution, best_fitness)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hdim_opt
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: High-Dimensional Optimization Suite: contains QUASAR optimizer and HDS sampling algorithms.
|
|
5
|
+
Author-email: Julian Soltes <jsoltes@regis.edu>
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: optimization,high-dimensional,sampling,QUASAR,HDS
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
16
|
+
Requires-Python: >=3.8
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
Requires-Dist: numpy
|
|
19
|
+
Requires-Dist: scipy
|
|
20
|
+
Provides-Extra: hds
|
|
21
|
+
Requires-Dist: pandas; extra == "hds"
|
|
22
|
+
Requires-Dist: scikit-learn; extra == "hds"
|
|
23
|
+
Requires-Dist: joblib; extra == "hds"
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
hdim_opt/__init__.py,sha256=pMI8O7BXPx6ldK98DCsrjGUEZIjlvTDayhO0ceoa--I,263
|
|
2
|
+
hdim_opt/hyperellipsoid_sampling.py,sha256=B8A238hL9bQxVS7R4OjMGRnM6_UyuT70QmE1IR957Lw,24387
|
|
3
|
+
hdim_opt/quasar_optimization.py,sha256=6UwLwyG5H-Q_lgeSHVPsHJe2mW4adhniOinEPx6_sb0,29075
|
|
4
|
+
hdim_opt-1.0.0.dist-info/METADATA,sha256=Wa7wnKwCHwhgsZXe1IS_bjkHhrmseYQfdOCRveN9Fa4,944
|
|
5
|
+
hdim_opt-1.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
6
|
+
hdim_opt-1.0.0.dist-info/top_level.txt,sha256=1KtWo9tEfEK3GC8D43cwVsC8yVG2Kc-9pl0hhcDjw4o,9
|
|
7
|
+
hdim_opt-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
hdim_opt
|