hdim-opt 1.3.0__tar.gz → 1.3.2__tar.gz

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,19 +1,33 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hdim_opt
3
- Version: 1.3.0
4
- Summary: Optimization toolkit for high-dimensional, non-differentiable problems.
3
+ Version: 1.3.2
4
+ Summary: High-dimensional numerical optimization and sampling toolkit for complex, non-differentiable problems.
5
5
  Author-email: Julian Soltes <jsoltes@regis.edu>
6
6
  License: MIT
7
+ Project-URL: Homepage, https://github.com/jgsoltes/hdim_opt
7
8
  Project-URL: Repository, https://github.com/jgsoltes/hdim_opt
8
- Keywords: optimization,high-dimensional,sampling,QUASAR,hyperellipsoid
9
+ Project-URL: Issues, https://github.com/jgsoltes/hdim_opt/issues
10
+ Project-URL: Changelog, https://github.com/jgsoltes/hdim_opt/releases
11
+ Keywords: optimization,high-dimensional,sampling,QUASAR,hyperellipsoid,evolutionary-algorithm,non-differentiable,global-optimization,stochastic-optimization,black-box-optimization
12
+ Classifier: Development Status :: 5 - Production/Stable
9
13
  Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.8
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
10
19
  Classifier: License :: OSI Approved :: MIT License
11
20
  Classifier: Operating System :: OS Independent
12
21
  Classifier: Intended Audience :: Science/Research
13
22
  Classifier: Intended Audience :: Developers
23
+ Classifier: Intended Audience :: Education
24
+ Classifier: Natural Language :: English
14
25
  Classifier: Topic :: Scientific/Engineering :: Mathematics
15
26
  Classifier: Topic :: Scientific/Engineering :: Physics
16
27
  Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
28
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
29
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
30
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
17
31
  Requires-Python: >=3.8
18
32
  Description-Content-Type: text/markdown
19
33
  Requires-Dist: numpy
@@ -28,13 +42,14 @@ Requires-Dist: SALib; extra == "sensitivity"
28
42
 
29
43
  # hdim-opt: High-Dimensional Optimization Toolkit
30
44
 
31
- A modern optimization package to accelerate convergence in complex, high-dimensional problems. Includes the QUASAR evolutionary algorithm, HDS exploitative QMC sampler, Sobol sensitivity analysis, and signal waveform decomposition.
45
+ Modern optimization package to accelerate convergence in complex, high-dimensional problems. Includes the QUASAR evolutionary algorithm, HDS exploitative QMC sampler, Sobol sensitivity analysis, signal waveform decomposition, and data transformations.
32
46
 
33
47
  All core functions, listed below, are single-line executable and require three essential parameters: [obj_function, bounds, n_samples].
34
48
  * **quasar**: QUASAR optimization for high-dimensional, non-differentiable problems.
35
- * **hds**: Generate an exploitative HDS sequence, to distribute samples in focused regions.
49
+ * **hyperellipsoid**: Generate a non-uniform Hyperellipsoid Density sequence, to focus sample distributions.
36
50
  * **sobol**: Generate a uniform Sobol sequence (via SciPy).
37
51
  * **sensitivity**: Perform Sobol sensitivity analysis to measure each variable's importance on objective function results (via SALib).
52
+ * **isotropize**: Isotropizes the input matrix.
38
53
  * **waveform**: Decompose the input waveform array (handles time- and frequency-domain via FFT / IFFT) into a diagnostic summary.
39
54
 
40
55
  ---
@@ -63,7 +78,7 @@ time, pulse = h.waveform_analysis.e1_waveform()
63
78
  solution, fitness = h.quasar(obj_func, bounds)
64
79
  sens_matrix = h.sensitivity(obj_func, bounds)
65
80
 
66
- hds_samples = h.hds(n_samples, bounds)
81
+ hds_samples = h.hyperellipsoid(n_samples, bounds)
67
82
  sobol_samples = h.sobol(n_samples, bounds)
68
83
  isotropic_samples = h.isotropize(sobol_samples)
69
84
 
@@ -75,7 +90,7 @@ signal_data = h.waveform(x=time,y=pulse)
75
90
 
76
91
  * Benefit: Significant improvements in convergence speed and solution quality compared to contemporary optimizers. (Reference: [https://arxiv.org/abs/2511.13843]).
77
92
 
78
- ## HDS Sampler (Hyperellipsoid Density Sampling)
79
- **HDS** is a non-uniform Quasi-Monte Carlo sampling method, specifically designed to exploit promising regions of the search space.
93
+ ## HDS Sampler
94
+ **HDS** (Hyperellipsoid Density Sampling) is a non-uniform Quasi-Monte Carlo sampling method, specifically designed to exploit promising regions of the search space.
80
95
 
81
96
  * Benefit: Provides control over the sample distribution. Results in higher average optimization solution quality when used for population initialization compared to uniform QMC methods. (Reference: [https://arxiv.org/abs/2511.07836]).
@@ -1,12 +1,13 @@
1
1
  # hdim-opt: High-Dimensional Optimization Toolkit
2
2
 
3
- A modern optimization package to accelerate convergence in complex, high-dimensional problems. Includes the QUASAR evolutionary algorithm, HDS exploitative QMC sampler, Sobol sensitivity analysis, and signal waveform decomposition.
3
+ Modern optimization package to accelerate convergence in complex, high-dimensional problems. Includes the QUASAR evolutionary algorithm, HDS exploitative QMC sampler, Sobol sensitivity analysis, signal waveform decomposition, and data transformations.
4
4
 
5
5
  All core functions, listed below, are single-line executable and require three essential parameters: [obj_function, bounds, n_samples].
6
6
  * **quasar**: QUASAR optimization for high-dimensional, non-differentiable problems.
7
- * **hds**: Generate an exploitative HDS sequence, to distribute samples in focused regions.
7
+ * **hyperellipsoid**: Generate a non-uniform Hyperellipsoid Density sequence, to focus sample distributions.
8
8
  * **sobol**: Generate a uniform Sobol sequence (via SciPy).
9
9
  * **sensitivity**: Perform Sobol sensitivity analysis to measure each variable's importance on objective function results (via SALib).
10
+ * **isotropize**: Isotropizes the input matrix.
10
11
  * **waveform**: Decompose the input waveform array (handles time- and frequency-domain via FFT / IFFT) into a diagnostic summary.
11
12
 
12
13
  ---
@@ -35,7 +36,7 @@ time, pulse = h.waveform_analysis.e1_waveform()
35
36
  solution, fitness = h.quasar(obj_func, bounds)
36
37
  sens_matrix = h.sensitivity(obj_func, bounds)
37
38
 
38
- hds_samples = h.hds(n_samples, bounds)
39
+ hds_samples = h.hyperellipsoid(n_samples, bounds)
39
40
  sobol_samples = h.sobol(n_samples, bounds)
40
41
  isotropic_samples = h.isotropize(sobol_samples)
41
42
 
@@ -47,7 +48,7 @@ signal_data = h.waveform(x=time,y=pulse)
47
48
 
48
49
  * Benefit: Significant improvements in convergence speed and solution quality compared to contemporary optimizers. (Reference: [https://arxiv.org/abs/2511.13843]).
49
50
 
50
- ## HDS Sampler (Hyperellipsoid Density Sampling)
51
- **HDS** is a non-uniform Quasi-Monte Carlo sampling method, specifically designed to exploit promising regions of the search space.
51
+ ## HDS Sampler
52
+ **HDS** (Hyperellipsoid Density Sampling) is a non-uniform Quasi-Monte Carlo sampling method, specifically designed to exploit promising regions of the search space.
52
53
 
53
54
  * Benefit: Provides control over the sample distribution. Results in higher average optimization solution quality when used for population initialization compared to uniform QMC methods. (Reference: [https://arxiv.org/abs/2511.07836]).
@@ -4,14 +4,15 @@
4
4
 
5
5
  Functions:
6
6
  - quasar: QUASAR optimization for high-dimensional, non-differentiable problems.
7
- - hds: Generate an exploitative HDS sequence, to distribute samples in focused regions.
7
+ - hyperellipsoid: Generate a non-uniform hyperellipsoid density sequence.
8
8
  - sobol: Generate a uniform Sobol sequence (via SciPy).
9
9
  - sensitivity: Perform Sobol sensitivity analysis to measure each variable's importance on objective function results (via SALib).
10
+ - isotropize/deisotropize: Isotropize the input matrix using ZCA.
10
11
  - waveform: Decompose the input waveform array (handles time- and frequency-domain via FFT / IFFT) into a diagnostic summary.
11
12
 
12
13
  Modules:
13
14
  - test_functions: Contains test functions for local optimization testing.
14
- - waveform_analysis: Contains pulse generation functions.
15
+ - waveform_analysis: Contains pulse signal generation functions.
15
16
 
16
17
  Example Usage:
17
18
 
@@ -29,7 +30,7 @@ Example Usage:
29
30
  >>> solution, fitness = h.quasar(obj_func, bounds)
30
31
  >>> sens_matrix = h.sensitivity(obj_func, bounds)
31
32
 
32
- >>> hds_samples = h.hds(n_samples, bounds)
33
+ >>> hds_samples = h.hyperellipsoid(n_samples, bounds)
33
34
  >>> sobol_samples = h.sobol(n_samples, bounds)
34
35
  >>> isotropic_samples = h.isotropize(sobol_samples)
35
36
 
@@ -37,17 +38,17 @@ Example Usage:
37
38
  """
38
39
 
39
40
  # package version
40
- __version__ = "1.3.0"
41
- __all__ = ['quasar', 'hds', 'sobol', 'sensitivity', 'test_functions', 'quasar_helpers'] # available for star imports
41
+ __version__ = "1.3.2"
42
+ __all__ = ['quasar','hyperellipsoid','sensitivity','waveform','sobol','isotropize','deisotropize',
43
+ 'test_functions','quasar_helpers','waveform_analysis'] # available for star imports
42
44
 
43
45
  # import core components
44
46
  from .quasar_optimization import optimize as quasar
45
- from .hyperellipsoid_sampling import sample as hds
46
- from .sobol_sampling import sobol_sample as sobol
47
+ from .hyperellipsoid_sampling import sample as hyperellipsoid
48
+ from .sobol_sensitivity import sobol_sample as sobol
47
49
  from .sobol_sensitivity import sens_analysis as sensitivity
50
+ from .quasar_helpers import isotropize, deisotropize
48
51
  from .waveform_analysis import analyze_waveform as waveform
49
- from .quasar_helpers import isotropize
50
- from .quasar_helpers import deisotropize
51
52
  from . import test_functions
52
53
  from . import quasar_helpers
53
54
  from . import waveform_analysis
@@ -1,14 +1,15 @@
1
- # global epslion
2
- epsilon = 1e-12
1
+ # global epsilon
2
+ epsilon = 1e-16
3
+ import numpy as np
3
4
 
4
5
  ### misc helper functions
5
6
 
6
- def sample_hypersphere(n_dimensions, radius, n_samples_in_sphere, radius_qmc_sequence):
7
+ def sample_hypersphere(n_dimensions, radius,
8
+ n_samples_in_sphere, radius_qmc_sequence):
7
9
  '''
8
10
  Objective:
9
11
  - Samples unit hyperspheres using Marsaglia polar vectors scaled by a QMC sequence.
10
12
  '''
11
- import numpy as np
12
13
 
13
14
  # generate normal distribution (for angular direction)
14
15
  samples = np.random.normal(size=(n_samples_in_sphere, n_dimensions))
@@ -28,14 +29,15 @@ def sample_hypersphere(n_dimensions, radius, n_samples_in_sphere, radius_qmc_seq
28
29
 
29
30
  return samples
30
31
 
31
- def sample_hyperellipsoid(n_dimensions, n_samples_in_ellipsoid, origin, pca_components, pca_variances, scaling_factor, radius_qmc_sequence=None):
32
+ def sample_hyperellipsoid(n_dimensions, n_samples_in_ellipsoid,
33
+ origin, pca_components, pca_variances,
34
+ scaling_factor, radius_qmc_sequence=None):
32
35
  '''
33
36
  Objective:
34
37
  - Generates samples inside the hyperellipsoid.
35
38
  - Calls the function to sample unit hyperspheres.
36
39
  - Transforms the hyperspherical samples to the ellipsoid axes defined using the PCA variances.
37
40
  '''
38
- import numpy as np
39
41
 
40
42
  # generate samples in unit hypersphere
41
43
  unit_sphere_samples = sample_hypersphere(n_dimensions, 1.0, n_samples_in_ellipsoid, radius_qmc_sequence)
@@ -61,7 +63,6 @@ def sample_in_voids(existing_samples, n_to_fill, bounds_min, bounds_max,
61
63
  '''
62
64
  from sklearn.neighbors import BallTree
63
65
  from sklearn.random_projection import GaussianRandomProjection
64
- import numpy as np
65
66
  from scipy import stats
66
67
  import time
67
68
 
@@ -146,14 +147,14 @@ def sample_in_voids(existing_samples, n_to_fill, bounds_min, bounds_max,
146
147
 
147
148
  return new_samples
148
149
 
149
- def fit_pca_for_cluster(cluster_samples, current_origin, initial_samples_std, n_dimensions):
150
+ def fit_pca_for_cluster(cluster_samples, current_origin,
151
+ initial_samples_std, n_dimensions):
150
152
  '''
151
153
  Performs PCA on a single cluster's samples or returns a default,
152
154
  called in parallel.
153
155
  '''
154
156
 
155
157
  from sklearn.decomposition import PCA
156
- import numpy as np
157
158
 
158
159
  # extract shape
159
160
  n_cluster_samples = len(cluster_samples)
@@ -174,11 +175,12 @@ def fit_pca_for_cluster(cluster_samples, current_origin, initial_samples_std, n_
174
175
  'variances': fixed_variance}
175
176
 
176
177
 
177
- # main sample function
178
+ ### main sampling function
178
179
 
179
180
  def sample(n_samples, bounds,
180
181
  weights=None, normalize=False,
181
182
  n_ellipsoids=None, n_initial_clusters=None, n_initial_qmc=None,
183
+ ellipsoid_scaling_factor=None,
182
184
  seed=None, plot_dendrogram=False, verbose=False):
183
185
  '''
184
186
  Objective:
@@ -208,7 +210,6 @@ def sample(n_samples, bounds,
208
210
 
209
211
  # imports
210
212
  try:
211
- import numpy as np
212
213
  import pandas as pd
213
214
  from scipy import stats
214
215
  from joblib import Parallel, delayed
@@ -367,11 +368,12 @@ def sample(n_samples, bounds,
367
368
  confidence_level = 0.9999 # captures 99.99% of cluster's samples
368
369
 
369
370
  # critical value (the statistical radius squared)
370
- chi2_critical_value = stats.chi2.ppf(confidence_level, df=n_dimensions)
371
- baseline_factor = 0.55 - 0.01*np.log(n_dimensions) # empirically derived to resample out-of-bounds points
372
-
373
- # square root as the scaling factor (Mahalanobis distance)
374
- ellipsoid_scaling_factor = baseline_factor * np.sqrt(chi2_critical_value)
371
+ if ellipsoid_scaling_factor == None:
372
+ chi2_critical_value = stats.chi2.ppf(confidence_level, df=n_dimensions)
373
+ baseline_factor = 0.55 - 0.01*np.log(n_dimensions) # empirically derived to resample out-of-bounds points
374
+
375
+ # square root as the scaling factor (Mahalanobis distance)
376
+ ellipsoid_scaling_factor = baseline_factor * np.sqrt(chi2_critical_value)
375
377
 
376
378
  # QMC sequence for radius scaling
377
379
  radius_qmc_sampler = stats.qmc.Sobol(d=1, seed=seed+1) # offset seed from initial qmc
@@ -522,7 +524,6 @@ def sample(n_samples, bounds,
522
524
 
523
525
 
524
526
  ### PCA for n_dim > 2:
525
-
526
527
  if normalize:
527
528
  data_to_plot_raw = initial_samples
528
529
  else:
@@ -546,8 +547,6 @@ def sample(n_samples, bounds,
546
547
  xlabel_str = f'Dimension 0'
547
548
  ylabel_str = f'Dimension 1'
548
549
 
549
- # dark visualization parameters for better sample visuals
550
-
551
550
  # samples
552
551
  fig, ax = plt.subplots(1,2,figsize=(9,5))
553
552
 
@@ -566,7 +565,6 @@ def sample(n_samples, bounds,
566
565
  ax[0].set_title(title_str, fontsize=14)
567
566
  ax[0].set_xlabel(xlabel_str)
568
567
  ax[0].set_ylabel(ylabel_str)
569
- # ax[0].axis(False)
570
568
  ax[0].legend(loc=(0.7,0.87), fontsize=8)
571
569
 
572
570
  # plot histograms
@@ -1,56 +1,53 @@
1
1
  # global imports
2
- import pandas as pd
3
- from sklearn.preprocessing import StandardScaler
4
2
  import numpy as np
5
- from scipy.linalg import cholesky, solve_triangular
6
- epsilon = 1e-12
3
+ epsilon = 1e-16
7
4
 
8
5
  def isotropize(data):
9
6
  '''
10
7
  Objective:
11
- - Converts data to isotropic space. Removes correlations and scales to mean and variance.
12
- - Promotes optimization stability.
13
- Inputs:
14
- - data: Input data.
15
- Outputs:
16
- - data_isotropic: Isotropized data.
17
- - metadata: Scaler and whitening matrix
8
+ - Isotropizes the input matrix using Zero-Phase Component Analysis (ZCA).
9
+ - Maintains original parameter orientation while removing correlations.
10
+ - 'deisotropize' function inverse transforms to the original parameter space.
18
11
  '''
19
-
12
+ from scipy.linalg import eigh
20
13
  # convert to array
21
14
  X = np.array(data)
22
15
 
23
16
  # standard scaling (mean = 0, var = 1)
24
17
  mean = np.mean(X, axis=0)
25
- stdev = np.std(X, axis=0)
18
+ stdev = np.std(X, axis=0) + epsilon # add epsilon to avoid div0
26
19
  X_centered = (X - mean) / stdev
27
20
 
28
- # whitening parameters
29
- n_dims = X_centered.shape[1]
30
- cov = np.cov(X_centered, rowvar=False) + np.eye(n_dims) * epsilon
31
- L = cholesky(cov, lower=True)
21
+ # eigen-decomposition of the correlation matrix
22
+ cov = np.cov(X_centered, rowvar=False) + np.eye(X_centered.shape[1]) * epsilon
23
+ eigenvalues, eigenvectors = eigh(cov) # eigh is more stable for symmetric matrices like covariance
32
24
 
33
- # transform Y = (X_centered) @ (L^-1).T
34
- data_iso = solve_triangular(L, X_centered.T, lower=True).T
25
+ # ZCA whitening matrix: W_zca = U @ diag(1/sqrt(lambda)) @ U.T
26
+ diag_inv_sqrt = np.diag(1.0 / np.sqrt(np.maximum(eigenvalues, epsilon))) # use maximum to avoid div0
27
+ W_zca = eigenvectors @ diag_inv_sqrt @ eigenvectors.T # whitening matrix
28
+ W_zca_inv = (eigenvectors * np.sqrt(np.maximum(eigenvalues, epsilon))) @ eigenvectors.T # save for deisotropization
29
+
30
+ # transform: y = X_centered @ W_zca.T
31
+ data_iso = np.dot(X_centered, W_zca) # no .T needed because W_zca is symmetric
35
32
 
36
33
  # store parameters for deisotropization
37
34
  params = {
38
35
  'mean': mean,
39
36
  'stdev': stdev,
40
- 'L': L
37
+ 'W_zca_inv': W_zca_inv
41
38
  }
42
39
  return data_iso, params
43
40
 
44
41
  def deisotropize(data_iso, params):
45
- '''De-isotropize data to its original parameter space.'''
42
+ '''De-isotropize data to its original parameter space via inverse ZCA.'''
46
43
 
47
- # reverse whitening: X_centered = Y @ L.T
48
- data_centered = np.dot(data_iso, params['L'].T)
44
+ # inverse ZCA: X_centered = y @ W_zca_inv.T
45
+ data_centered = np.dot(data_iso, params['W_zca_inv'].T)
49
46
 
50
- # reverse scaling: X = (X_centered * std) + mean
47
+ # inverse scaling: X = (X_centered * std) + mean
51
48
  data_original = (data_centered * params['stdev']) + params['mean']
52
49
  return data_original
53
-
50
+
54
51
  ############## CONSTRAINTS ##############
55
52
  def apply_penalty(fitnesses, solutions, constraints, constraint_penalty, vectorized):
56
53
  '''
@@ -164,7 +164,6 @@ def evolve_generation(obj_function, population, fitnesses, best_solution,
164
164
 
165
165
  # greedy elitism selection
166
166
  selection_indices = trial_fitnesses < fitnesses
167
-
168
167
  new_population = np.where(selection_indices[np.newaxis, :], trial_vectors, population)
169
168
  new_fitnesses = np.where(selection_indices, trial_fitnesses, fitnesses)
170
169
 
@@ -263,7 +262,7 @@ def evolve_generation(obj_function, population, fitnesses, best_solution,
263
262
 
264
263
  return new_population, new_fitnesses
265
264
 
266
- def asym_reinit(population, current_fitnesses, bounds, reinit_method, seed, vectorized):
265
+ def asym_reinit(population, current_fitnesses, bounds, reinit_method, seed, generation, vectorized):
267
266
  '''
268
267
  Objective:
269
268
  - Reinitializes the worst 33% solutions in the population.
@@ -340,7 +339,7 @@ def asym_reinit(population, current_fitnesses, bounds, reinit_method, seed, vect
340
339
  elif reinit_method == 'sobol':
341
340
 
342
341
  # generate sobol samples
343
- sobol_sampler = stats.qmc.Sobol(d=dimensions, seed=seed)
342
+ sobol_sampler = stats.qmc.Sobol(d=dimensions, seed=seed+generation)
344
343
  sobol_samples_unit = sobol_sampler.random(n=num_to_replace)
345
344
 
346
345
  bounds_low = bounds[:, 0]
@@ -367,14 +366,13 @@ def asym_reinit(population, current_fitnesses, bounds, reinit_method, seed, vect
367
366
 
368
367
 
369
368
  # main optimize function
370
-
371
369
  def optimize(func, bounds, args=(),
372
370
  init='sobol', popsize=None, maxiter=100,
373
371
  entangle_rate=0.33, polish=True, polish_minimizer=None,
374
- patience=np.inf, vectorized=False,
375
- hds_weights=None, kwargs={},
372
+ patience=np.inf, tolerance=-np.inf, vectorized=False,
373
+ kwargs={},
376
374
  constraints=None, constraint_penalty=1e9,
377
- reinitialization_method='covariance',
375
+ reinitialization='covariance', hds_weights=None,
378
376
  verbose=True, plot_solutions=True, num_to_plot=10, plot_contour=True,
379
377
  workers=1, seed=None
380
378
  ):
@@ -382,7 +380,6 @@ def optimize(func, bounds, args=(),
382
380
  Objective:
383
381
  - Finds the optimal solution for a given objective function.
384
382
  - Designed for non-differentiable, high-dimensional problems.
385
- - For explorative problems chance reinitialization_method to '
386
383
  - Test functions available for local testing, called as hdim_opt.test_functions.function_name.
387
384
  - Existing test functions: [rastrigin, ackley, sinusoid, sphere, shubert].
388
385
 
@@ -393,31 +390,30 @@ def optimize(func, bounds, args=(),
393
390
  - kwargs: Dictionary of keyword arguments for the objective function.
394
391
 
395
392
  - init: Initial population sampling method.
396
- - Defaults to 'sobol'. (Recommended power-of-2 population sizes for maximum uniformity).
393
+ - Defaults to 'sobol' (recommended power-of-2 population sizes for maximum uniformity).
397
394
  - Existing options are:
398
- - 'sobol': Sobol (highly uniform QMC; powers of 2 population sizes recommended).
399
- - 'hds': Hyperellipsoid Density (non-uniform; density weights 'hds_weights' recommended).
400
- - 'lhs': Latin Hypercube (uniform QMC).
401
- - 'random': Random sampling (uniform).
395
+ - 'sobol': Sobol (spatially uniform; power of 2 population sizes recommended).
396
+ - 'hds': Hyperellipsoid (non-uniform; density weights 'hds_weights' recommended).
397
+ - 'lhs': Latin Hypercube (uniform across each dimension).
398
+ - 'random': Random sampling (quasi-uniform).
402
399
  - custom population (N x D matrix).
403
- - popsize: Number of solution vectors to evolve (default 10 * n_dimensions).
404
- - Recommended to be a power of 2 for Sobol initialization.
400
+ - popsize: Number of solution vectors to evolve (default is next power of 2 of 10 * n_dimensions).
405
401
  - maxiter: Number of generations to evolve (default 100).
406
402
 
407
403
  - entangle_rate: Probability of solutions using the local Spooky-Best mutation strategy.
408
- - Defaults to 0.33. This causes to the three mutation strategies to be applied equally.
404
+ - Defaults to 0.33; each mutation strategy is applied equally.
409
405
  - Higher implies more exploitation.
406
+
410
407
  - polish: Boolean to implement final polishing step, using SciPy.optimize.minimize.
411
- - polish_minimizer: Minimization function to use for polishing step (using SciPy naming conventions).
408
+ - polish_minimizer: Name of Scipy minimization function to polish with.
412
409
  - Defaults to 'Powell' minimization, or 'SLSQP' if 'constraints' parameter is provided.
413
- - Recommended to place constraints in objective function to use Powell.
410
+ - Accepts all minimizers ('L-BFGS-B', ...).
414
411
 
415
412
  - patience: Number of generations without improvement before early convergence.
413
+ - tolerance: Target objective function value for early convergence.
416
414
  - vectorized: Boolean to accept vectorized (N,D) objective functions
417
- - Extremely efficient, highly recommended whenever possible.
415
+ - Highly efficient, recommended when possible.
418
416
 
419
- - hds_weights: Optional weights for hyperellipsoid density sampling initialization.
420
- - {0 : {'center' : center, 'std': stdev}, 1: {...} }
421
417
  - kwargs: Dictionary of keyword arguments for the objective function.
422
418
 
423
419
  - constraints: Dictionary of constraints to penalize.
@@ -433,11 +429,13 @@ def optimize(func, bounds, args=(),
433
429
  }
434
430
  - constraint_penalty: Penalty applied to each constraint violated, defaults to 1e12.
435
431
 
436
- - reinitialization_method: Type of re-sampling to use in the asymptotic reinitialization.
432
+ - reinitialization: Type of re-sampling to use in the asymptotic reinitialization.
437
433
  - Options are ['covariance', 'sobol'].
438
- - 'covariance' (exploitative) is default for most problems.
434
+ - 'covariance' (exploitative) is default for N > D problems.
439
435
  - 'sobol' (explorative) is optional, for high exploration and faster computation.
440
436
  - None to disable reinitialization calculations.
437
+ - hds_weights: Optional weights for hyperellipsoid density sampling initialization.
438
+ - {0 : {'center' : center, 'std': stdev}, 1: {...} }
441
439
 
442
440
  - verbose: Displays prints and plots.
443
441
  - Mutation factor distribution shown with hdim_opt.test_functions.plot_mutations()
@@ -446,7 +444,7 @@ def optimize(func, bounds, args=(),
446
444
 
447
445
  - workers: Number of workers / jobs / cores to use.
448
446
  - Default is 1. Set to -1 to use all available.
449
- - If workers != 1, constraint & objective functions must be imported from external module, for pickling.
447
+ - If workers != 1, constraint & objective functions must be imported from external module for pickling.
450
448
  - seed: Random seed for deterministic & reproducible results.
451
449
 
452
450
  Outputs:
@@ -499,10 +497,7 @@ def optimize(func, bounds, args=(),
499
497
  # ensure bounds is array; shape (n_dimensions,2)
500
498
  bounds = np.array(bounds)
501
499
  n_dimensions = bounds.shape[0]
502
-
503
- if n_dimensions == 1:
504
- reinitialization = False
505
-
500
+
506
501
  # if init is not a string, assume it is a custom population
507
502
  if not isinstance(init, str):
508
503
  popsize = init.shape[0]
@@ -516,7 +511,11 @@ def optimize(func, bounds, args=(),
516
511
  # ensure integers
517
512
  popsize, maxiter = int(popsize), int(maxiter)
518
513
 
514
+ # map to sobol if N < D
515
+ if n_dimensions == 1:
516
+ reinitialization = None
519
517
 
518
+
520
519
  ################################# INPUT ERRORS #################################
521
520
 
522
521
  # entangle rate error
@@ -528,8 +527,8 @@ def optimize(func, bounds, args=(),
528
527
  raise ValueError("Initial sampler must be one of ['sobol','random','hds','lhs'], or a custom population.")
529
528
 
530
529
  # patience error
531
- if patience <= 1:
532
- raise ValueError('Patience must be > 1 generation.')
530
+ if patience < 1:
531
+ raise ValueError('Patience must be > 0 generations.')
533
532
 
534
533
 
535
534
  ################################# INITIAL POPULATION #################################
@@ -537,11 +536,11 @@ def optimize(func, bounds, args=(),
537
536
  # generate initial population
538
537
  initial_population = initialize_population(popsize, bounds, init, hds_weights, seed, verbose)
539
538
  if verbose:
540
- if reinitialization_method not in ['sobol', 'covariance', None]:
541
- print("reinitialization_method must be one of ['covariance', 'sobol', None].")
539
+ if reinitialization not in ['sobol', 'covariance', None]:
540
+ print("reinitialization must be one of ['covariance', 'sobol', None].")
542
541
  print(f'\nEvolving (None):')
543
542
  else:
544
- print(f'\nEvolving ({reinitialization_method}):')
543
+ print(f'\nEvolving ({reinitialization}):')
545
544
 
546
545
  # match differential evolution conventions
547
546
  if vectorized:
@@ -648,12 +647,12 @@ def optimize(func, bounds, args=(),
648
647
  # apply asymptotic covariance reinitialization to population
649
648
  final_proba = 0.33
650
649
  decay_generation = 0.33
651
- if reinitialization_method in ['sobol','covariance']:
650
+ if (reinitialization in ['sobol','covariance']):
652
651
  reinit_proba = np.e**((np.log(final_proba)/(decay_generation*maxiter))*generation)
653
652
  else:
654
653
  reinit_proba = 0.0
655
654
  if np.random.rand() < reinit_proba:
656
- population = asym_reinit(population, current_fitnesses, bounds, reinitialization_method, seed, vectorized=vectorized)
655
+ population = asym_reinit(population, current_fitnesses, bounds, reinitialization, seed, generation, vectorized=vectorized)
657
656
 
658
657
  # clip population to bounds
659
658
  if vectorized:
@@ -663,19 +662,19 @@ def optimize(func, bounds, args=(),
663
662
 
664
663
  # add to population history
665
664
  if verbose:
666
- # determine which solutions to sample
667
- if popsize <= num_to_plot:
668
- indices_to_sample = np.arange(popsize)
669
- else:
670
- indices_to_sample = np.random.choice(popsize, num_to_plot, replace=False)
671
-
672
- if vectorized:
673
- sampled_population = population[:, indices_to_sample].T.copy()
674
- else:
675
- sampled_population = population[indices_to_sample].copy()
676
-
677
- pop_history.append(sampled_population)
678
- best_history.append(best_solution.copy())
665
+ # determine which solutions to sample
666
+ if popsize <= num_to_plot:
667
+ indices_to_sample = np.arange(popsize)
668
+ else:
669
+ indices_to_sample = np.random.choice(popsize, num_to_plot, replace=False)
670
+
671
+ if vectorized:
672
+ sampled_population = population[:, indices_to_sample].T.copy()
673
+ else:
674
+ sampled_population = population[indices_to_sample].copy()
675
+
676
+ pop_history.append(sampled_population)
677
+ best_history.append(best_solution.copy())
679
678
 
680
679
  # print generation status
681
680
  if verbose:
@@ -683,11 +682,14 @@ def optimize(func, bounds, args=(),
683
682
  print(f' Gen. {generation+1}/{maxiter} | f(x)={best_fitness:.2e} | stdev={stdev:.2e} | reinit={reinit_proba:.2f}')
684
683
 
685
684
  # patience for early convergence
686
- if (generation - last_improvement_gen) > patience:
685
+ if last_improvement_gen >= patience:
687
686
  if verbose:
688
- print(f'\nEarly convergence: number of generations without improvement exceeds patience ({patience}).')
687
+ print(f'Early convergence: patience exceeded ({patience}).')
688
+ break
689
+ if best_fitness <= tolerance:
690
+ if verbose:
691
+ print(f'Early convergence: f(x) below tolerance ({tolerance:.2e}).')
689
692
  break
690
-
691
693
 
692
694
  ################################# POLISH #################################
693
695
 
@@ -703,7 +705,7 @@ def optimize(func, bounds, args=(),
703
705
  maxiter=maxiter, vectorized=vectorized, constraints=constraints,
704
706
  args=args, kwargs=kwargs,
705
707
  polish_minimizer=polish_minimizer, verbose=verbose
706
- )
708
+ )
707
709
 
708
710
 
709
711
  ################################# VERBOSE #################################
@@ -0,0 +1,185 @@
1
+ import numpy as np
2
+ from scipy import stats
3
+
4
+ ### sensitivity analysis
5
+ def sens_analysis(func, bounds, n_samples=2**7,
6
+ args=None, kwargs=None,
7
+ param_names=None, calc_second_order=True,
8
+ log_scale=False, num_to_plot=10, verbose=True):
9
+ '''
10
+ Objective:
11
+ - Perform global Sobol sensitivity analysis on the objective function.
12
+ - Utilizes the SALib package.
13
+ Inputs:
14
+ - func: Objective function (Problem) to analyze.
15
+ - bounds: Parameter space bounds, as an array of tuples.
16
+ - n_samples: Number of Sobol samples to generate.
17
+ - kwargs: Keyword arguments (dictionary) for objective function.
18
+ - param_names: Optional parameter names for each dimension.
19
+ - calc_second_order: Boolean to calculate second-order interactions. Disable to improve computation speed.
20
+ - log_scale: Boolean to log-scale plots.
21
+ - num_to_plot: Number of dimensions to plot.
22
+ - verbose: Boolean to display plots.
23
+ Outputs:
24
+ - Si: Matrix of first- and total-order sensitivity indices and confidences.
25
+ - S2_matrix: Matrix of second-order interactions.
26
+ '''
27
+
28
+ ### imports
29
+ try:
30
+ import numpy as np
31
+ from SALib.sample import sobol as sobol_sample
32
+ from SALib.analyze import sobol as sobol_analyze
33
+ import pandas as pd
34
+ import time
35
+ except ImportError as e:
36
+ raise ImportError(f'Sensitivity analysis requires dependencies: (SALib, pandas).') from e
37
+
38
+ ### extract inputs
39
+ start_time = time.time()
40
+ bounds = np.array(bounds)
41
+ n_params = bounds.shape[0]
42
+ if param_names == None:
43
+ param_names = range(0,n_params)
44
+ elif len(param_names) != len(bounds):
45
+ raise ValueError('Length of param_names does not match length of bounds.')
46
+
47
+ ### define problem for SALib
48
+ problem = {
49
+ 'num_vars': n_params,
50
+ 'names': param_names,
51
+ 'bounds' : bounds
52
+ }
53
+
54
+ ### generate samples
55
+ if verbose:
56
+ print(f'Generating Sobol samples (N={n_samples:,.0f}, D={n_params}).')
57
+ param_values = sobol_sample.sample(problem, n_samples, calc_second_order=calc_second_order)
58
+
59
+ ### args / kwargs for the objective function
60
+ if args is None:
61
+ args = []
62
+ if kwargs is None:
63
+ kwargs = {}
64
+ def wrapped_func(x_samples):
65
+ return func(x_samples, *args, **kwargs)
66
+
67
+ ### evaluate samples
68
+ # vectorized evaluation
69
+ n_expected = param_values.shape[0]
70
+ try:
71
+ values = wrapped_func(param_values)
72
+ values = np.asarray(values).flatten()
73
+ if values.shape[0] != n_expected:
74
+ raise ValueError('Non-vectorized objective function.')
75
+
76
+ # loop-based evaluation
77
+ except ValueError as e:
78
+ if verbose:
79
+ print(f'Non-vectorized objective function; loop-based evaluation.')
80
+ values = np.array([wrapped_func(sample) for sample in param_values])
81
+
82
+ # run sensitivity analysis
83
+ print('Running sensitivity analysis.')
84
+ Si = sobol_analyze.analyze(problem, values, calc_second_order=calc_second_order, print_to_console=False)
85
+
86
+ # create Si output dataframe
87
+ Si_keys = ['S1', 'S1_conf', 'ST', 'ST_conf']
88
+ Si_filtered = {k: Si[k] for k in Si_keys if k in Si} # filter for output
89
+ Si_df = pd.DataFrame(Si_filtered, index=param_names)
90
+
91
+ # create S2 output dataframe
92
+ if calc_second_order:
93
+ S2_matrix = Si['S2']
94
+ S2_df = pd.DataFrame(S2_matrix, index=param_names, columns=param_names)
95
+ S2_df = S2_df.fillna(S2_df.T)
96
+ else:
97
+ S2_df = pd.DataFrame()
98
+
99
+ ### end of calculations
100
+ end_time = time.time()
101
+ run_time = end_time - start_time
102
+ if verbose:
103
+ num_to_plot = np.minimum(num_to_plot, n_params)
104
+ print(f'\nRun time: {run_time:.2f}s')
105
+ # plotting imports
106
+ try:
107
+ import matplotlib.pyplot as plt
108
+ import seaborn as sns
109
+ except ImportError as e:
110
+ raise ImportError(f'Plotting requires dependencies: (matplotlib, seaborn).') from e
111
+
112
+ # sort by S1
113
+ sort_idx = np.argsort(Si['S1'])
114
+ s1_sorted = Si['S1'][sort_idx][-num_to_plot:]
115
+ st_sorted = Si['ST'][sort_idx][-num_to_plot:]
116
+ s1_conf_sorted = Si['S1_conf'][sort_idx][-num_to_plot:]
117
+ st_conf_sorted = Si['ST_conf'][sort_idx][-num_to_plot:]
118
+ names_sorted = [np.array(param_names)[i] for i in sort_idx][-num_to_plot:]
119
+ index = np.arange(len(names_sorted))
120
+
121
+ ### plot 1: first-order (S1) and total-order (ST) indices
122
+ fig, ax = plt.subplots(1,1,figsize=(9, 7))
123
+
124
+ bar_width = 0.35
125
+ ax.barh(index + bar_width/2, s1_sorted, bar_width, xerr=s1_conf_sorted,
126
+ label='First-order ($S_1$)',
127
+ alpha=1,
128
+ capsize=2.5)
129
+ ax.set_yticks(index)
130
+ ax.set_yticklabels(names_sorted)
131
+
132
+ ax.barh(index - bar_width/2, st_sorted, bar_width,
133
+ xerr=st_conf_sorted,
134
+ label='Total-order ($S_T$)',
135
+ alpha=0.75,
136
+ capsize=2.5)
137
+ if log_scale:
138
+ ax.set_xscale('log')
139
+ ax.set_title('Sensitivity Indices ($S_1$, $S_T$)')
140
+ ax.legend()
141
+ plt.tight_layout()
142
+ plt.show()
143
+
144
+ if calc_second_order:
145
+ ### plot 2: heatmap of second order indices
146
+ s2_plot, ax = plt.subplots(1,1,figsize=(9, 7))
147
+
148
+ top_idx_to_show = sort_idx[-num_to_plot:]
149
+ S2_filtered = S2_df.iloc[top_idx_to_show, top_idx_to_show]
150
+ mask_filtered = np.tril(np.ones_like(S2_filtered, dtype=bool))
151
+ sns.heatmap(data=S2_filtered, mask=mask_filtered, annot=True, vmin=0.0, fmt='.2f')
152
+ ax.set_title('Second-order Interactions ($S_2$)')
153
+ ax.invert_yaxis()
154
+ plt.tight_layout()
155
+ plt.show()
156
+
157
+ return Si_df, S2_df
158
+
159
+ ### sobol sampling
160
+ def sobol_sample(n_samples, bounds, normalize=False, seed=None):
161
+ '''
162
+ Objective:
163
+ - Generates a uniform scrambled Sobol sample sequence.
164
+ Inputs:
165
+ - n_samples: Number of samples to generate.
166
+ - bounds: Range to sample over.
167
+ - normalize: Boolean, if True keeps samples normalized to [0,1].
168
+ - seed: Random seed.
169
+ Outputs:
170
+ - sobol_sequence: Sobol sample sequence.
171
+ '''
172
+
173
+ # clean bounds & n_dimensions
174
+ bounds = np.array(bounds)
175
+ n_dimensions = bounds.shape[0]
176
+
177
+ sobol_sampler = stats.qmc.Sobol(d=n_dimensions, scramble=True, seed=seed)
178
+ sobol_samples_unit = sobol_sampler.random(n=n_samples)
179
+
180
+ if not normalize:
181
+ sobol_sequence = stats.qmc.scale(sobol_samples_unit, bounds[:, 0], bounds[:, 1])
182
+ else:
183
+ sobol_sequence = sobol_samples_unit
184
+
185
+ return sobol_sequence
@@ -406,7 +406,7 @@ def plot_diagnostic_dashboard(temporal, spectral, metrics):
406
406
  ax_cum.plot(f_mhz, spectral['energy'], color='gold', linewidth=2)
407
407
  ax_cum.fill_between(f_mhz, spectral['energy'], color='gold', alpha=0.2)
408
408
  ax_cum.axvline(metrics['bw_90_hz'], color='red', linestyle='--',
409
- label=f'90% Band: {metrics['bw_90_hz']:.1f} MHz')
409
+ label=f'90% Band: {metrics['bw_90_hz']:.1e} Hz')
410
410
  ax_cum.set_title('Cumulative Energy')
411
411
  ax_cum.set_ylabel('Normalized Energy')
412
412
  ax_cum.set_xlabel('Frequency (MHz)')
@@ -440,7 +440,10 @@ def plot_diagnostic_dashboard(temporal, spectral, metrics):
440
440
  im = ax_spec.pcolormesh(t_spec*1e6, f, 10*np.log10(Sxx + epsilon),
441
441
  shading='gouraud', cmap='plasma')
442
442
 
443
+ ax_spec.set_title('Spectrogram')
443
444
  ax_spec.set_yscale('log')
444
445
  ax_spec.set_ylim(np.abs(spectral['freq']).min()+epsilon, np.abs(spectral['freq'].max()/sample_rate))
446
+ cbar = fig.colorbar(im, ax=ax_spec)
447
+ cbar.set_label('Power/Frequency (dB/Hz)')
445
448
 
446
449
  plt.show()
@@ -1,19 +1,33 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hdim_opt
3
- Version: 1.3.0
4
- Summary: Optimization toolkit for high-dimensional, non-differentiable problems.
3
+ Version: 1.3.2
4
+ Summary: High-dimensional numerical optimization and sampling toolkit for complex, non-differentiable problems.
5
5
  Author-email: Julian Soltes <jsoltes@regis.edu>
6
6
  License: MIT
7
+ Project-URL: Homepage, https://github.com/jgsoltes/hdim_opt
7
8
  Project-URL: Repository, https://github.com/jgsoltes/hdim_opt
8
- Keywords: optimization,high-dimensional,sampling,QUASAR,hyperellipsoid
9
+ Project-URL: Issues, https://github.com/jgsoltes/hdim_opt/issues
10
+ Project-URL: Changelog, https://github.com/jgsoltes/hdim_opt/releases
11
+ Keywords: optimization,high-dimensional,sampling,QUASAR,hyperellipsoid,evolutionary-algorithm,non-differentiable,global-optimization,stochastic-optimization,black-box-optimization
12
+ Classifier: Development Status :: 5 - Production/Stable
9
13
  Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.8
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
10
19
  Classifier: License :: OSI Approved :: MIT License
11
20
  Classifier: Operating System :: OS Independent
12
21
  Classifier: Intended Audience :: Science/Research
13
22
  Classifier: Intended Audience :: Developers
23
+ Classifier: Intended Audience :: Education
24
+ Classifier: Natural Language :: English
14
25
  Classifier: Topic :: Scientific/Engineering :: Mathematics
15
26
  Classifier: Topic :: Scientific/Engineering :: Physics
16
27
  Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
28
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
29
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
30
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
17
31
  Requires-Python: >=3.8
18
32
  Description-Content-Type: text/markdown
19
33
  Requires-Dist: numpy
@@ -28,13 +42,14 @@ Requires-Dist: SALib; extra == "sensitivity"
28
42
 
29
43
  # hdim-opt: High-Dimensional Optimization Toolkit
30
44
 
31
- A modern optimization package to accelerate convergence in complex, high-dimensional problems. Includes the QUASAR evolutionary algorithm, HDS exploitative QMC sampler, Sobol sensitivity analysis, and signal waveform decomposition.
45
+ Modern optimization package to accelerate convergence in complex, high-dimensional problems. Includes the QUASAR evolutionary algorithm, HDS exploitative QMC sampler, Sobol sensitivity analysis, signal waveform decomposition, and data transformations.
32
46
 
33
47
  All core functions, listed below, are single-line executable and require three essential parameters: [obj_function, bounds, n_samples].
34
48
  * **quasar**: QUASAR optimization for high-dimensional, non-differentiable problems.
35
- * **hds**: Generate an exploitative HDS sequence, to distribute samples in focused regions.
49
+ * **hyperellipsoid**: Generate a non-uniform Hyperellipsoid Density sequence, to focus sample distributions.
36
50
  * **sobol**: Generate a uniform Sobol sequence (via SciPy).
37
51
  * **sensitivity**: Perform Sobol sensitivity analysis to measure each variable's importance on objective function results (via SALib).
52
+ * **isotropize**: Isotropizes the input matrix.
38
53
  * **waveform**: Decompose the input waveform array (handles time- and frequency-domain via FFT / IFFT) into a diagnostic summary.
39
54
 
40
55
  ---
@@ -63,7 +78,7 @@ time, pulse = h.waveform_analysis.e1_waveform()
63
78
  solution, fitness = h.quasar(obj_func, bounds)
64
79
  sens_matrix = h.sensitivity(obj_func, bounds)
65
80
 
66
- hds_samples = h.hds(n_samples, bounds)
81
+ hds_samples = h.hyperellipsoid(n_samples, bounds)
67
82
  sobol_samples = h.sobol(n_samples, bounds)
68
83
  isotropic_samples = h.isotropize(sobol_samples)
69
84
 
@@ -75,7 +90,7 @@ signal_data = h.waveform(x=time,y=pulse)
75
90
 
76
91
  * Benefit: Significant improvements in convergence speed and solution quality compared to contemporary optimizers. (Reference: [https://arxiv.org/abs/2511.13843]).
77
92
 
78
- ## HDS Sampler (Hyperellipsoid Density Sampling)
79
- **HDS** is a non-uniform Quasi-Monte Carlo sampling method, specifically designed to exploit promising regions of the search space.
93
+ ## HDS Sampler
94
+ **HDS** (Hyperellipsoid Density Sampling) is a non-uniform Quasi-Monte Carlo sampling method, specifically designed to exploit promising regions of the search space.
80
95
 
81
96
  * Benefit: Provides control over the sample distribution. Results in higher average optimization solution quality when used for population initialization compared to uniform QMC methods. (Reference: [https://arxiv.org/abs/2511.07836]).
@@ -4,7 +4,6 @@ hdim_opt/__init__.py
4
4
  hdim_opt/hyperellipsoid_sampling.py
5
5
  hdim_opt/quasar_helpers.py
6
6
  hdim_opt/quasar_optimization.py
7
- hdim_opt/sobol_sampling.py
8
7
  hdim_opt/sobol_sensitivity.py
9
8
  hdim_opt/test_functions.py
10
9
  hdim_opt/waveform_analysis.py
@@ -0,0 +1,76 @@
1
+ # pyproject.toml
2
+
3
+ [build-system]
4
+ requires = ["setuptools>=61.0.0", "wheel"]
5
+ build-backend = "setuptools.build_meta"
6
+
7
+ [project]
8
+ name = "hdim_opt"
9
+ version = "1.3.2" # match version in __init__.py
10
+ description = "High-dimensional numerical optimization and sampling toolkit for complex, non-differentiable problems."
11
+ readme = {file = "README.md", content-type = "text/markdown"}
12
+
13
+ authors = [
14
+ {name="Julian Soltes", email="jsoltes@regis.edu"}
15
+ ]
16
+
17
+ license = { text = "MIT" }
18
+ requires-python = ">=3.8"
19
+ keywords = [
20
+ "optimization",
21
+ "high-dimensional",
22
+ "sampling",
23
+ "QUASAR",
24
+ "hyperellipsoid",
25
+ "evolutionary-algorithm",
26
+ "non-differentiable",
27
+ "global-optimization",
28
+ "stochastic-optimization",
29
+ "black-box-optimization"
30
+ ]
31
+ classifiers = [
32
+ "Development Status :: 5 - Production/Stable",
33
+ "Programming Language :: Python :: 3",
34
+ "Programming Language :: Python :: 3.8",
35
+ "Programming Language :: Python :: 3.9",
36
+ "Programming Language :: Python :: 3.10",
37
+ "Programming Language :: Python :: 3.11",
38
+ "Programming Language :: Python :: 3.12",
39
+ "License :: OSI Approved :: MIT License",
40
+ "Operating System :: OS Independent",
41
+ "Intended Audience :: Science/Research",
42
+ "Intended Audience :: Developers",
43
+ "Intended Audience :: Education",
44
+ "Natural Language :: English",
45
+ "Topic :: Scientific/Engineering :: Mathematics",
46
+ "Topic :: Scientific/Engineering :: Physics",
47
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
48
+ "Topic :: Scientific/Engineering :: Information Analysis",
49
+ "Topic :: Software Development :: Libraries :: Python Modules",
50
+ "Topic :: Scientific/Engineering :: Bio-Informatics"
51
+ ]
52
+ dependencies = ["numpy", "scipy"]
53
+
54
+ [project.optional-dependencies]
55
+ hds = [
56
+ # required by HDS `pip install hdim_opt[hds]`
57
+ "pandas",
58
+ "scikit-learn",
59
+ "joblib"
60
+ ]
61
+
62
+ sensitivity = [
63
+ # required by sensitivity `pip install hdim_opt[sensitivity]`
64
+ "pandas",
65
+ "SALib"
66
+ ]
67
+
68
+ [project.urls]
69
+ Homepage = "https://github.com/jgsoltes/hdim_opt"
70
+ Repository = "https://github.com/jgsoltes/hdim_opt"
71
+ Issues = "https://github.com/jgsoltes/hdim_opt/issues"
72
+ Changelog = "https://github.com/jgsoltes/hdim_opt/releases"
73
+
74
+ [tool.setuptools.packages.find]
75
+ where = ["."]
76
+ include = ["hdim_opt*"] # find hdim_opt package
@@ -1,29 +0,0 @@
1
- from scipy import stats
2
- import numpy as np
3
-
4
- def sobol_sample(n_samples, bounds, normalize=False, seed=None):
5
- '''
6
- Objective:
7
- - Generate a uniform scrambled Sobol sample sequence.
8
- Inputs:
9
- - n_samples: Number of samples to generate.
10
- - bounds: Range to sample over.
11
- - normalize: Boolean, if True keeps samples normalized to [0,1].
12
- - seed: Random seed.
13
- Outputs:
14
- - sobol_sequence: Sobol sample sequence.
15
- '''
16
-
17
- # clean bounds & n_dimensions
18
- bounds = np.array(bounds)
19
- n_dimensions = bounds.shape[0]
20
-
21
- sobol_sampler = stats.qmc.Sobol(d=n_dimensions, scramble=True, seed=seed)
22
- sobol_samples_unit = sobol_sampler.random(n=n_samples)
23
-
24
- if not normalize:
25
- sobol_sequence = stats.qmc.scale(sobol_samples_unit, bounds[:, 0], bounds[:, 1])
26
- else:
27
- sobol_sequence = sobol_samples_unit
28
-
29
- return sobol_sequence
@@ -1,125 +0,0 @@
1
- def sens_analysis(func, bounds, n_samples=None,
2
- kwargs=None, param_names=None,
3
- verbose=True, log_scale=True):
4
- '''
5
- Objective:
6
- - Perform Sobol sensitivity analysis on the vectorized objective function.
7
- Inputs:
8
- - func: Objective function (Problem) to analyze.
9
- - bounds: Parameter space bounds, as an array of tuples.
10
- - n_samples: Number of Sobol samples to generate.
11
- - kwargs: Keyword arguments (dictionary) for objective function.
12
- - param_names: Optional parameter names for each dimension.
13
- - verbose: Boolean to display plots.
14
- - log_scale: Boolean to log-scale plots.
15
- Outputs:
16
- - Si: Full sensitivity indices and confidences.
17
- - S2_matrix: Matrix of S2 relationship sensitivity indices.
18
- '''
19
-
20
- # imports
21
- try:
22
- import numpy as np
23
- from SALib.sample import sobol as sobol_sample
24
- from SALib.analyze import sobol as sobol_analyze
25
- import pandas as pd
26
- from functools import partial
27
- except ImportError as e:
28
- raise ImportError(f'Sensitivity analysis requires dependencies: (SALib, pandas, functools).') from e
29
-
30
-
31
- # define input parameters and their ranges
32
- bounds = np.array(bounds)
33
- n_params = bounds.shape[0]
34
- if param_names == None:
35
- param_names = range(0,n_params)
36
-
37
- # scale default n_samples by dimension (power of 2)
38
- if n_samples == None:
39
- n_samples = int(2**np.ceil(np.log2(10*n_params)))
40
-
41
- # define problem
42
- problem = {
43
- 'num_vars': n_params,
44
- 'names': param_names,
45
- 'bounds' : bounds
46
- }
47
-
48
- # generate samples
49
- if verbose:
50
- print(f'Generating {n_samples:,.0f} Sobol samples for sensitivity analysis.')
51
- param_values = sobol_sample.sample(problem, n_samples)
52
-
53
- # kwargs for the objective function
54
- if kwargs:
55
- func = partial(func, **kwargs)
56
-
57
- # evaluate the samples
58
- values = func(param_values)
59
-
60
- # running sensitivity analysis
61
- print('Running sensitivity analysis.')
62
- Si = sobol_analyze.analyze(problem, values, calc_second_order=True, print_to_console=False)
63
-
64
- # calculate S2 sensitivities
65
- # convert S2 indices to dataframe to process easier
66
- S2_matrix = Si['S2']
67
- S2_df = pd.DataFrame(S2_matrix, index=param_names, columns=param_names)
68
- S2_df = S2_df.fillna(S2_df.T)
69
- mask = np.tril(np.ones_like(S2_df, dtype=bool))
70
-
71
- if verbose:
72
- # import
73
- try:
74
- import matplotlib.pyplot as plt
75
- import seaborn as sns
76
- except ImportError as e:
77
- raise ImportError(f'Plotting requires dependencies: (matplotlib, seaborn).') from e
78
-
79
- # sort by S1 values
80
- sort_idx = np.argsort(Si['S1'])
81
- s1_sorted = Si['S1'][sort_idx]
82
- st_sorted = Si['ST'][sort_idx]
83
- s1_conf_sorted = Si['S1_conf'][sort_idx]
84
- st_conf_sorted = Si['ST_conf'][sort_idx]
85
- names_sorted = [np.array(param_names)[i] for i in sort_idx]
86
-
87
-
88
- bar_width = 0.35
89
- index = np.arange(n_params)
90
-
91
- # plot 1: first-order (S1) and total-order (ST) indices
92
- sens_plot, axs = plt.subplots(2,1,figsize=(9, 13))
93
-
94
- # define bar width and positions
95
- bar_width = 0.35
96
- index = np.arange(n_params)
97
-
98
- # plot S1 (first order) sensitivities
99
- axs[0].barh(index - bar_width/2, s1_sorted, bar_width,
100
- xerr=s1_conf_sorted,
101
- label='First-order ($S_1$)',
102
- alpha=1,
103
- capsize=2.5)
104
-
105
- axs[0].barh(index + bar_width/2, st_sorted, bar_width,
106
- xerr=st_conf_sorted,
107
- label='Total-order ($S_T$)',
108
- alpha=0.75,
109
- capsize=2.5)
110
-
111
- axs[0].set_title('Sensitivity Indices ($S_1$, $S_T$)')
112
- if log_scale:
113
- axs[0].set_xscale('log')
114
-
115
- axs[0].set_yticks(index)
116
- axs[0].set_yticklabels(names_sorted)
117
-
118
- # plot 2: heatmap of second order indices
119
- sns.heatmap(data=S2_df, mask=mask, cbar_kws={'label': 'Second-order Index ($S_2$)'},ax=axs[1]) # magma
120
- axs[1].set_title('Second-order Interactions ($S_2$)')
121
- axs[1].invert_yaxis()
122
- sens_plot.tight_layout()
123
- plt.show()
124
-
125
- return Si, S2_matrix
@@ -1,51 +0,0 @@
1
- # pyproject.toml
2
-
3
- [build-system]
4
- requires = ["setuptools>=61.0.0", "wheel"]
5
- build-backend = "setuptools.build_meta"
6
-
7
- [project]
8
- name = "hdim_opt"
9
- version = "1.3.0" # match __version__ in __init__.py
10
- description = "Optimization toolkit for high-dimensional, non-differentiable problems."
11
- readme = {file = "README.md", content-type = "text/markdown"}
12
-
13
- authors = [
14
- {name="Julian Soltes", email="jsoltes@regis.edu"}
15
- ]
16
-
17
- license = { text = "MIT" }
18
- requires-python = ">=3.8"
19
- keywords = ["optimization", "high-dimensional", "sampling", "QUASAR", "hyperellipsoid"]
20
- classifiers = [
21
- "Programming Language :: Python :: 3",
22
- "License :: OSI Approved :: MIT License",
23
- "Operating System :: OS Independent",
24
- "Intended Audience :: Science/Research",
25
- "Intended Audience :: Developers",
26
- "Topic :: Scientific/Engineering :: Mathematics",
27
- "Topic :: Scientific/Engineering :: Physics",
28
- "Topic :: Scientific/Engineering :: Artificial Intelligence"
29
- ]
30
- dependencies = ["numpy", "scipy"]
31
-
32
- [project.optional-dependencies]
33
- hds = [
34
- # required by HDS `pip install hdim_opt[hds]`
35
- "pandas",
36
- "scikit-learn",
37
- "joblib"
38
- ]
39
-
40
- sensitivity = [
41
- # required by sensitivity `pip install hdim_opt[sensitivity]`
42
- "pandas",
43
- "SALib"
44
- ]
45
-
46
- [project.urls]
47
- Repository = "https://github.com/jgsoltes/hdim_opt"
48
-
49
- [tool.setuptools.packages.find]
50
- where = ["."]
51
- include = ["hdim_opt*"] # find hdim_opt package
File without changes