evograd-diff 0.1.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.
- evograd/__init__.py +67 -0
- evograd/algorithms/__init__.py +138 -0
- evograd/algorithms/cmaes.py +1365 -0
- evograd/algorithms/de.py +895 -0
- evograd/algorithms/ga.py +532 -0
- evograd/algorithms/pso.py +648 -0
- evograd/algorithms/shade.py +1165 -0
- evograd/benchmarks/functions/__init__.py +229 -0
- evograd/benchmarks/functions/base.py +217 -0
- evograd/benchmarks/functions/cec2017/__init__.py +250 -0
- evograd/benchmarks/functions/cec2017/basic.py +413 -0
- evograd/benchmarks/functions/cec2017/composition.py +580 -0
- evograd/benchmarks/functions/cec2017/data.pkl +0 -0
- evograd/benchmarks/functions/cec2017/data.py +350 -0
- evograd/benchmarks/functions/cec2017/hybrid.py +406 -0
- evograd/benchmarks/functions/cec2017/simple.py +326 -0
- evograd/benchmarks/functions/classical.py +649 -0
- evograd/benchmarks/functions/smoothed_funnel.py +476 -0
- evograd/benchmarks/functions/transforms.py +463 -0
- evograd/benchmarks/run_benchmark_functions.py +1208 -0
- evograd/core/__init__.py +73 -0
- evograd/core/algorithm.py +778 -0
- evograd/core/maximize.py +269 -0
- evograd/core/minimize.py +740 -0
- evograd/core/problem.py +444 -0
- evograd/core/result.py +571 -0
- evograd/core/termination.py +602 -0
- evograd/operators/__init__.py +178 -0
- evograd/operators/crossover.py +1117 -0
- evograd/operators/mutation.py +1098 -0
- evograd/operators/relaxations.py +175 -0
- evograd/operators/repair.py +601 -0
- evograd/operators/sampling.py +577 -0
- evograd/operators/selection.py +981 -0
- evograd/operators/survival.py +1000 -0
- evograd/tests/__init__.py +11 -0
- evograd/tests/run_all.py +78 -0
- evograd/tests/test_core.py +528 -0
- evograd/tests/test_ga.py +572 -0
- evograd/tests/test_operators.py +662 -0
- evograd/tests/test_per_individual.py +326 -0
- evograd/tests/test_utils.py +328 -0
- evograd/utils/__init__.py +97 -0
- evograd/utils/callbacks.py +926 -0
- evograd/utils/device.py +502 -0
- evograd/utils/duplicates.py +421 -0
- evograd_diff-0.1.0.dist-info/METADATA +439 -0
- evograd_diff-0.1.0.dist-info/RECORD +50 -0
- evograd_diff-0.1.0.dist-info/WHEEL +4 -0
- evograd_diff-0.1.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sampling operators for population initialisation.
|
|
3
|
+
|
|
4
|
+
This module provides strategies for initialising populations
|
|
5
|
+
in the search space. Samplers are used by algorithms to create
|
|
6
|
+
the initial population.
|
|
7
|
+
|
|
8
|
+
Available samplers:
|
|
9
|
+
- UniformSampling: Uniform random sampling (default)
|
|
10
|
+
- LatinHypercubeSampling: Better space coverage via LHS
|
|
11
|
+
- NormalSampling: Gaussian sampling around center
|
|
12
|
+
- LogUniformSampling: Log-scale uniform sampling
|
|
13
|
+
|
|
14
|
+
Example:
|
|
15
|
+
>>> from evograd.operators import UniformSampling
|
|
16
|
+
>>> from evograd.core import Problem
|
|
17
|
+
>>>
|
|
18
|
+
>>> sampler = UniformSampling()
|
|
19
|
+
>>> problem = Problem(n_var=10, xl=-5.0, xu=5.0)
|
|
20
|
+
>>>
|
|
21
|
+
>>> # Sample 100 individuals
|
|
22
|
+
>>> population = sampler(100, problem)
|
|
23
|
+
>>> print(population.shape) # torch.Size([100, 10])
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
from abc import ABC, abstractmethod
|
|
29
|
+
from typing import TYPE_CHECKING, Optional, Union
|
|
30
|
+
|
|
31
|
+
import torch
|
|
32
|
+
import torch.nn as nn
|
|
33
|
+
from torch import Tensor
|
|
34
|
+
|
|
35
|
+
if TYPE_CHECKING:
|
|
36
|
+
from evograd.core.problem import Problem
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
"Sampling",
|
|
40
|
+
"UniformSampling",
|
|
41
|
+
"LatinHypercubeSampling",
|
|
42
|
+
"NormalSampling",
|
|
43
|
+
"LogUniformSampling",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# =============================================================================
|
|
48
|
+
# Base Sampling Class
|
|
49
|
+
# =============================================================================
|
|
50
|
+
|
|
51
|
+
class Sampling(nn.Module, ABC):
|
|
52
|
+
"""
|
|
53
|
+
Abstract base class for population sampling strategies.
|
|
54
|
+
|
|
55
|
+
Subclasses must implement:
|
|
56
|
+
- _sample(): Generate samples within [0, 1]^d
|
|
57
|
+
|
|
58
|
+
The base class handles:
|
|
59
|
+
- Scaling samples to problem bounds
|
|
60
|
+
- Device/dtype management
|
|
61
|
+
- Seeding for reproducibility
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
seed: Random seed for reproducibility.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def __init__(self, seed: Optional[int] = None) -> None:
|
|
68
|
+
super().__init__()
|
|
69
|
+
self.seed = seed
|
|
70
|
+
self._generator: Optional[torch.Generator] = None
|
|
71
|
+
|
|
72
|
+
def _get_generator(self, device: torch.device) -> Optional[torch.Generator]:
|
|
73
|
+
"""Get or create random generator for reproducibility."""
|
|
74
|
+
if self._generator is None and self.seed is not None:
|
|
75
|
+
# Handle MPS device - use CPU generator since MPS has limited generator support
|
|
76
|
+
# Also handle the 'mps' vs 'mps:0' issue
|
|
77
|
+
if device.type == 'mps':
|
|
78
|
+
# MPS generators have issues - use CPU generator instead
|
|
79
|
+
# The results will still be on MPS, just seeded from CPU
|
|
80
|
+
gen_device = torch.device('cpu')
|
|
81
|
+
else:
|
|
82
|
+
gen_device = device
|
|
83
|
+
|
|
84
|
+
self._generator = torch.Generator(device=gen_device)
|
|
85
|
+
self._generator.manual_seed(self.seed)
|
|
86
|
+
return self._generator
|
|
87
|
+
|
|
88
|
+
@abstractmethod
|
|
89
|
+
def _sample(
|
|
90
|
+
self,
|
|
91
|
+
n_samples: int,
|
|
92
|
+
n_var: int,
|
|
93
|
+
device: torch.device,
|
|
94
|
+
dtype: torch.dtype,
|
|
95
|
+
) -> Tensor:
|
|
96
|
+
"""
|
|
97
|
+
Generate samples in [0, 1]^d.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
n_samples: Number of samples to generate.
|
|
101
|
+
n_var: Number of variables (dimensions).
|
|
102
|
+
device: Target device.
|
|
103
|
+
dtype: Target dtype.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
Tensor of shape [n_samples, n_var] with values in [0, 1].
|
|
107
|
+
"""
|
|
108
|
+
pass
|
|
109
|
+
|
|
110
|
+
def forward(
|
|
111
|
+
self,
|
|
112
|
+
n_samples: int,
|
|
113
|
+
problem: Problem,
|
|
114
|
+
) -> Tensor:
|
|
115
|
+
"""
|
|
116
|
+
Sample population for a problem.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
n_samples: Number of individuals to sample.
|
|
120
|
+
problem: Problem instance with bounds.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Population tensor of shape [n_samples, n_var].
|
|
124
|
+
"""
|
|
125
|
+
# Get bounds from problem
|
|
126
|
+
xl = problem.xl # [n_var] or scalar
|
|
127
|
+
xu = problem.xu # [n_var] or scalar
|
|
128
|
+
device = xl.device
|
|
129
|
+
dtype = xl.dtype
|
|
130
|
+
n_var = problem.n_var
|
|
131
|
+
|
|
132
|
+
# Sample in [0, 1]^d
|
|
133
|
+
samples = self._sample(n_samples, n_var, device, dtype)
|
|
134
|
+
|
|
135
|
+
# Scale to problem bounds: x = xl + samples * (xu - xl)
|
|
136
|
+
population = xl + samples * (xu - xl)
|
|
137
|
+
|
|
138
|
+
return population
|
|
139
|
+
|
|
140
|
+
def __call__(
|
|
141
|
+
self,
|
|
142
|
+
n_samples: int,
|
|
143
|
+
problem: Problem,
|
|
144
|
+
) -> Tensor:
|
|
145
|
+
"""Sample population (alias for forward)."""
|
|
146
|
+
return self.forward(n_samples, problem)
|
|
147
|
+
|
|
148
|
+
def __repr__(self) -> str:
|
|
149
|
+
seed_str = f", seed={self.seed}" if self.seed is not None else ""
|
|
150
|
+
return f"{self.__class__.__name__}({seed_str})"
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# =============================================================================
|
|
154
|
+
# Uniform Random Sampling
|
|
155
|
+
# =============================================================================
|
|
156
|
+
|
|
157
|
+
class UniformSampling(Sampling):
|
|
158
|
+
"""
|
|
159
|
+
Uniform random sampling in the search space.
|
|
160
|
+
|
|
161
|
+
The simplest and most common initialisation strategy.
|
|
162
|
+
Samples are drawn uniformly from [xl, xu] for each variable.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
seed: Random seed for reproducibility.
|
|
166
|
+
|
|
167
|
+
Example:
|
|
168
|
+
>>> sampler = UniformSampling()
|
|
169
|
+
>>> population = sampler(100, problem)
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
def _sample(
|
|
173
|
+
self,
|
|
174
|
+
n_samples: int,
|
|
175
|
+
n_var: int,
|
|
176
|
+
device: torch.device,
|
|
177
|
+
dtype: torch.dtype,
|
|
178
|
+
) -> Tensor:
|
|
179
|
+
generator = self._get_generator(device)
|
|
180
|
+
|
|
181
|
+
if generator is not None:
|
|
182
|
+
# Generator may be on CPU for MPS devices
|
|
183
|
+
gen_device = generator.device
|
|
184
|
+
samples = torch.rand(
|
|
185
|
+
n_samples, n_var,
|
|
186
|
+
device=gen_device,
|
|
187
|
+
dtype=dtype,
|
|
188
|
+
generator=generator,
|
|
189
|
+
)
|
|
190
|
+
# Move to target device if needed
|
|
191
|
+
if gen_device != device:
|
|
192
|
+
samples = samples.to(device)
|
|
193
|
+
return samples
|
|
194
|
+
else:
|
|
195
|
+
return torch.rand(n_samples, n_var, device=device, dtype=dtype)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
# =============================================================================
|
|
199
|
+
# Latin Hypercube Sampling
|
|
200
|
+
# =============================================================================
|
|
201
|
+
|
|
202
|
+
class LatinHypercubeSampling(Sampling):
|
|
203
|
+
"""
|
|
204
|
+
Latin Hypercube Sampling (LHS) for better space coverage.
|
|
205
|
+
|
|
206
|
+
LHS ensures that samples are well-distributed across each
|
|
207
|
+
dimension by dividing each dimension into n equal intervals
|
|
208
|
+
and sampling exactly once from each interval.
|
|
209
|
+
|
|
210
|
+
This provides better coverage of the search space compared
|
|
211
|
+
to pure random sampling, especially for small sample sizes.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
smooth: If True, add jitter within each stratum (default).
|
|
215
|
+
If False, sample at stratum centers.
|
|
216
|
+
seed: Random seed for reproducibility.
|
|
217
|
+
|
|
218
|
+
Example:
|
|
219
|
+
>>> sampler = LatinHypercubeSampling()
|
|
220
|
+
>>> population = sampler(100, problem)
|
|
221
|
+
|
|
222
|
+
Note:
|
|
223
|
+
LHS is particularly useful for:
|
|
224
|
+
- Small population sizes
|
|
225
|
+
- High-dimensional problems
|
|
226
|
+
- When initial coverage is important
|
|
227
|
+
"""
|
|
228
|
+
|
|
229
|
+
def __init__(
|
|
230
|
+
self,
|
|
231
|
+
smooth: bool = True,
|
|
232
|
+
seed: Optional[int] = None,
|
|
233
|
+
) -> None:
|
|
234
|
+
super().__init__(seed=seed)
|
|
235
|
+
self.smooth = smooth
|
|
236
|
+
|
|
237
|
+
def _sample(
|
|
238
|
+
self,
|
|
239
|
+
n_samples: int,
|
|
240
|
+
n_var: int,
|
|
241
|
+
device: torch.device,
|
|
242
|
+
dtype: torch.dtype,
|
|
243
|
+
) -> Tensor:
|
|
244
|
+
generator = self._get_generator(device)
|
|
245
|
+
|
|
246
|
+
# Determine generator device (may be CPU for MPS)
|
|
247
|
+
gen_device = generator.device if generator is not None else device
|
|
248
|
+
|
|
249
|
+
# Create intervals: [0, 1/n), [1/n, 2/n), ..., [(n-1)/n, 1)
|
|
250
|
+
# Sample one point in each interval for each dimension
|
|
251
|
+
|
|
252
|
+
# Base positions (left edge of each stratum)
|
|
253
|
+
indices = torch.arange(n_samples, device=device, dtype=dtype)
|
|
254
|
+
|
|
255
|
+
# Randomly permute indices for each dimension
|
|
256
|
+
samples = torch.zeros(n_samples, n_var, device=device, dtype=dtype)
|
|
257
|
+
|
|
258
|
+
for j in range(n_var):
|
|
259
|
+
# Random permutation (generate on generator device, then move if needed)
|
|
260
|
+
if generator is not None:
|
|
261
|
+
perm = torch.randperm(n_samples, device=gen_device, generator=generator)
|
|
262
|
+
if gen_device != device:
|
|
263
|
+
perm = perm.to(device)
|
|
264
|
+
else:
|
|
265
|
+
perm = torch.randperm(n_samples, device=device)
|
|
266
|
+
|
|
267
|
+
if self.smooth:
|
|
268
|
+
# Add random jitter within each stratum
|
|
269
|
+
if generator is not None:
|
|
270
|
+
jitter = torch.rand(n_samples, device=gen_device, dtype=dtype, generator=generator)
|
|
271
|
+
if gen_device != device:
|
|
272
|
+
jitter = jitter.to(device)
|
|
273
|
+
else:
|
|
274
|
+
jitter = torch.rand(n_samples, device=device, dtype=dtype)
|
|
275
|
+
samples[:, j] = (perm.to(dtype) + jitter) / n_samples
|
|
276
|
+
else:
|
|
277
|
+
# Sample at stratum centers
|
|
278
|
+
samples[:, j] = (perm.to(dtype) + 0.5) / n_samples
|
|
279
|
+
|
|
280
|
+
return samples
|
|
281
|
+
|
|
282
|
+
def __repr__(self) -> str:
|
|
283
|
+
seed_str = f", seed={self.seed}" if self.seed is not None else ""
|
|
284
|
+
return f"LatinHypercubeSampling(smooth={self.smooth}{seed_str})"
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
# =============================================================================
|
|
288
|
+
# Normal (Gaussian) Sampling
|
|
289
|
+
# =============================================================================
|
|
290
|
+
|
|
291
|
+
class NormalSampling(Sampling):
|
|
292
|
+
"""
|
|
293
|
+
Gaussian sampling around the center of the search space.
|
|
294
|
+
|
|
295
|
+
Samples are drawn from a normal distribution centered at
|
|
296
|
+
the midpoint of the bounds, with standard deviation scaled
|
|
297
|
+
to fit within the bounds.
|
|
298
|
+
|
|
299
|
+
This is useful when:
|
|
300
|
+
- Good solutions are expected near the center
|
|
301
|
+
- A focused initial search is desired
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
sigma_factor: Standard deviation as fraction of range.
|
|
305
|
+
Default 1/3 means 99.7% of samples within bounds
|
|
306
|
+
(before clipping).
|
|
307
|
+
clip_to_bounds: If True, clip samples to [xl, xu].
|
|
308
|
+
seed: Random seed for reproducibility.
|
|
309
|
+
|
|
310
|
+
Example:
|
|
311
|
+
>>> sampler = NormalSampling(sigma_factor=0.25)
|
|
312
|
+
>>> population = sampler(100, problem)
|
|
313
|
+
"""
|
|
314
|
+
|
|
315
|
+
def __init__(
|
|
316
|
+
self,
|
|
317
|
+
sigma_factor: float = 1.0 / 3.0,
|
|
318
|
+
clip_to_bounds: bool = True,
|
|
319
|
+
seed: Optional[int] = None,
|
|
320
|
+
) -> None:
|
|
321
|
+
super().__init__(seed=seed)
|
|
322
|
+
self.sigma_factor = sigma_factor
|
|
323
|
+
self.clip_to_bounds = clip_to_bounds
|
|
324
|
+
|
|
325
|
+
def _sample(
|
|
326
|
+
self,
|
|
327
|
+
n_samples: int,
|
|
328
|
+
n_var: int,
|
|
329
|
+
device: torch.device,
|
|
330
|
+
dtype: torch.dtype,
|
|
331
|
+
) -> Tensor:
|
|
332
|
+
generator = self._get_generator(device)
|
|
333
|
+
|
|
334
|
+
# Sample from standard normal
|
|
335
|
+
if generator is not None:
|
|
336
|
+
# Generator may be on CPU for MPS devices
|
|
337
|
+
gen_device = generator.device
|
|
338
|
+
z = torch.randn(
|
|
339
|
+
n_samples, n_var,
|
|
340
|
+
device=gen_device,
|
|
341
|
+
dtype=dtype,
|
|
342
|
+
generator=generator,
|
|
343
|
+
)
|
|
344
|
+
if gen_device != device:
|
|
345
|
+
z = z.to(device)
|
|
346
|
+
else:
|
|
347
|
+
z = torch.randn(n_samples, n_var, device=device, dtype=dtype)
|
|
348
|
+
|
|
349
|
+
# Transform to [0, 1] centered at 0.5 with scaled std
|
|
350
|
+
# mean=0.5, std=sigma_factor * 0.5
|
|
351
|
+
samples = 0.5 + self.sigma_factor * 0.5 * z
|
|
352
|
+
|
|
353
|
+
if self.clip_to_bounds:
|
|
354
|
+
samples = torch.clamp(samples, 0.0, 1.0)
|
|
355
|
+
|
|
356
|
+
return samples
|
|
357
|
+
|
|
358
|
+
def __repr__(self) -> str:
|
|
359
|
+
seed_str = f", seed={self.seed}" if self.seed is not None else ""
|
|
360
|
+
return f"NormalSampling(sigma_factor={self.sigma_factor}{seed_str})"
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
# =============================================================================
|
|
364
|
+
# Log-Uniform Sampling
|
|
365
|
+
# =============================================================================
|
|
366
|
+
|
|
367
|
+
class LogUniformSampling(Sampling):
|
|
368
|
+
"""
|
|
369
|
+
Log-uniform sampling for problems with log-scale parameters.
|
|
370
|
+
|
|
371
|
+
Samples are uniformly distributed in log-space, useful for
|
|
372
|
+
parameters that span multiple orders of magnitude (e.g.,
|
|
373
|
+
learning rates, regularisation coefficients).
|
|
374
|
+
|
|
375
|
+
Note: Bounds must be strictly positive!
|
|
376
|
+
|
|
377
|
+
Args:
|
|
378
|
+
base: Deprecated, kept for backwards compatibility.
|
|
379
|
+
The implementation now uses natural log internally.
|
|
380
|
+
seed: Random seed for reproducibility.
|
|
381
|
+
|
|
382
|
+
Example:
|
|
383
|
+
>>> # Sample learning rates from [1e-5, 1e-1]
|
|
384
|
+
>>> problem = Problem(n_var=1, xl=1e-5, xu=1e-1)
|
|
385
|
+
>>> sampler = LogUniformSampling()
|
|
386
|
+
>>> population = sampler(100, problem)
|
|
387
|
+
|
|
388
|
+
Warning:
|
|
389
|
+
Both xl and xu must be > 0 for all variables.
|
|
390
|
+
"""
|
|
391
|
+
|
|
392
|
+
def __init__(
|
|
393
|
+
self,
|
|
394
|
+
base: float = 10.0, # Kept for backwards compatibility, not used
|
|
395
|
+
seed: Optional[int] = None,
|
|
396
|
+
) -> None:
|
|
397
|
+
super().__init__(seed=seed)
|
|
398
|
+
self.base = base # Kept for repr, not used in computation
|
|
399
|
+
|
|
400
|
+
def forward(
|
|
401
|
+
self,
|
|
402
|
+
n_samples: int,
|
|
403
|
+
problem: Problem,
|
|
404
|
+
) -> Tensor:
|
|
405
|
+
"""
|
|
406
|
+
Sample in log-space and transform back.
|
|
407
|
+
|
|
408
|
+
Overrides base forward to handle log transformation.
|
|
409
|
+
"""
|
|
410
|
+
xl = problem.xl
|
|
411
|
+
xu = problem.xu
|
|
412
|
+
device = xl.device
|
|
413
|
+
dtype = xl.dtype
|
|
414
|
+
n_var = problem.n_var
|
|
415
|
+
|
|
416
|
+
# Validate positive bounds
|
|
417
|
+
if (xl <= 0).any() or (xu <= 0).any():
|
|
418
|
+
raise ValueError(
|
|
419
|
+
"LogUniformSampling requires strictly positive bounds. "
|
|
420
|
+
f"Got xl={xl}, xu={xu}"
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
# Work in natural log space (avoids torch.pow(scalar, tensor) which isn't supported on MPS)
|
|
424
|
+
# Mathematically equivalent: uniform in log-space => log-uniform in original space
|
|
425
|
+
log_xl = torch.log(xl)
|
|
426
|
+
log_xu = torch.log(xu)
|
|
427
|
+
|
|
428
|
+
# Sample uniformly in [0, 1]
|
|
429
|
+
samples = self._sample(n_samples, n_var, device, dtype)
|
|
430
|
+
|
|
431
|
+
# Scale to log-space bounds (natural log)
|
|
432
|
+
log_samples = log_xl + samples * (log_xu - log_xl)
|
|
433
|
+
|
|
434
|
+
# Transform back to original space using exp (works on all devices)
|
|
435
|
+
population = torch.exp(log_samples)
|
|
436
|
+
|
|
437
|
+
return population
|
|
438
|
+
|
|
439
|
+
def _sample(
|
|
440
|
+
self,
|
|
441
|
+
n_samples: int,
|
|
442
|
+
n_var: int,
|
|
443
|
+
device: torch.device,
|
|
444
|
+
dtype: torch.dtype,
|
|
445
|
+
) -> Tensor:
|
|
446
|
+
generator = self._get_generator(device)
|
|
447
|
+
|
|
448
|
+
if generator is not None:
|
|
449
|
+
# Generator may be on CPU for MPS devices
|
|
450
|
+
gen_device = generator.device
|
|
451
|
+
samples = torch.rand(
|
|
452
|
+
n_samples, n_var,
|
|
453
|
+
device=gen_device,
|
|
454
|
+
dtype=dtype,
|
|
455
|
+
generator=generator,
|
|
456
|
+
)
|
|
457
|
+
if gen_device != device:
|
|
458
|
+
samples = samples.to(device)
|
|
459
|
+
return samples
|
|
460
|
+
else:
|
|
461
|
+
return torch.rand(n_samples, n_var, device=device, dtype=dtype)
|
|
462
|
+
|
|
463
|
+
def __repr__(self) -> str:
|
|
464
|
+
seed_str = f", seed={self.seed}" if self.seed is not None else ""
|
|
465
|
+
return f"LogUniformSampling(base={self.base}{seed_str})"
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
# =============================================================================
|
|
469
|
+
# Halton Sequence Sampling (Quasi-Random)
|
|
470
|
+
# =============================================================================
|
|
471
|
+
|
|
472
|
+
class HaltonSampling(Sampling):
|
|
473
|
+
"""
|
|
474
|
+
Halton sequence quasi-random sampling.
|
|
475
|
+
|
|
476
|
+
Generates low-discrepancy sequences that fill the space
|
|
477
|
+
more uniformly than random sampling. Each dimension uses
|
|
478
|
+
a different prime base.
|
|
479
|
+
|
|
480
|
+
Particularly useful for:
|
|
481
|
+
- Integration/Monte Carlo methods
|
|
482
|
+
- Surrogate model initialisation
|
|
483
|
+
- When uniform coverage is critical
|
|
484
|
+
|
|
485
|
+
Args:
|
|
486
|
+
scramble: If True, apply random scrambling to reduce
|
|
487
|
+
correlation in high dimensions.
|
|
488
|
+
seed: Random seed for scrambling.
|
|
489
|
+
|
|
490
|
+
Example:
|
|
491
|
+
>>> sampler = HaltonSampling()
|
|
492
|
+
>>> population = sampler(100, problem)
|
|
493
|
+
"""
|
|
494
|
+
|
|
495
|
+
# First 100 primes for high-dimensional problems
|
|
496
|
+
_PRIMES = [
|
|
497
|
+
2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47,
|
|
498
|
+
53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113,
|
|
499
|
+
127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197,
|
|
500
|
+
199, 211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281,
|
|
501
|
+
283, 293, 307, 311, 313, 317, 331, 337, 347, 349, 353, 359, 367, 373, 379,
|
|
502
|
+
383, 389, 397, 401, 409, 419, 421, 431, 433, 439, 443, 449, 457, 461, 463,
|
|
503
|
+
467, 479, 487, 491, 499, 503, 509, 521, 523, 541,
|
|
504
|
+
]
|
|
505
|
+
|
|
506
|
+
def __init__(
|
|
507
|
+
self,
|
|
508
|
+
scramble: bool = True,
|
|
509
|
+
seed: Optional[int] = None,
|
|
510
|
+
) -> None:
|
|
511
|
+
super().__init__(seed=seed)
|
|
512
|
+
self.scramble = scramble
|
|
513
|
+
|
|
514
|
+
def _halton_sequence(
|
|
515
|
+
self,
|
|
516
|
+
n_samples: int,
|
|
517
|
+
base: int,
|
|
518
|
+
device: torch.device,
|
|
519
|
+
dtype: torch.dtype,
|
|
520
|
+
) -> Tensor:
|
|
521
|
+
"""Generate Halton sequence for a single dimension."""
|
|
522
|
+
result = torch.zeros(n_samples, device=device, dtype=dtype)
|
|
523
|
+
|
|
524
|
+
for i in range(n_samples):
|
|
525
|
+
f = 1.0
|
|
526
|
+
r = 0.0
|
|
527
|
+
idx = i + 1 # Start from 1
|
|
528
|
+
|
|
529
|
+
while idx > 0:
|
|
530
|
+
f = f / base
|
|
531
|
+
r = r + f * (idx % base)
|
|
532
|
+
idx = idx // base
|
|
533
|
+
|
|
534
|
+
result[i] = r
|
|
535
|
+
|
|
536
|
+
return result
|
|
537
|
+
|
|
538
|
+
def _sample(
|
|
539
|
+
self,
|
|
540
|
+
n_samples: int,
|
|
541
|
+
n_var: int,
|
|
542
|
+
device: torch.device,
|
|
543
|
+
dtype: torch.dtype,
|
|
544
|
+
) -> Tensor:
|
|
545
|
+
if n_var > len(self._PRIMES):
|
|
546
|
+
raise ValueError(
|
|
547
|
+
f"HaltonSampling supports up to {len(self._PRIMES)} dimensions, "
|
|
548
|
+
f"got n_var={n_var}"
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
samples = torch.zeros(n_samples, n_var, device=device, dtype=dtype)
|
|
552
|
+
|
|
553
|
+
for j in range(n_var):
|
|
554
|
+
base = self._PRIMES[j]
|
|
555
|
+
samples[:, j] = self._halton_sequence(n_samples, base, device, dtype)
|
|
556
|
+
|
|
557
|
+
# Optional scrambling
|
|
558
|
+
if self.scramble:
|
|
559
|
+
generator = self._get_generator(device)
|
|
560
|
+
|
|
561
|
+
for j in range(n_var):
|
|
562
|
+
if generator is not None:
|
|
563
|
+
# Generator may be on CPU for MPS devices
|
|
564
|
+
gen_device = generator.device
|
|
565
|
+
shift = torch.rand(1, device=gen_device, dtype=dtype, generator=generator)
|
|
566
|
+
if gen_device != device:
|
|
567
|
+
shift = shift.to(device)
|
|
568
|
+
else:
|
|
569
|
+
shift = torch.rand(1, device=device, dtype=dtype)
|
|
570
|
+
|
|
571
|
+
samples[:, j] = (samples[:, j] + shift) % 1.0
|
|
572
|
+
|
|
573
|
+
return samples
|
|
574
|
+
|
|
575
|
+
def __repr__(self) -> str:
|
|
576
|
+
seed_str = f", seed={self.seed}" if self.seed is not None else ""
|
|
577
|
+
return f"HaltonSampling(scramble={self.scramble}{seed_str})"
|