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,326 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Test script for per-individual/per-gene parameter support in operators.
|
|
3
|
+
|
|
4
|
+
This script demonstrates the four parameter configurations:
|
|
5
|
+
1. Fixed (scalar): Same value for all individuals and genes
|
|
6
|
+
2. Per-gene [D]: Different value per gene, same across individuals
|
|
7
|
+
3. Per-individual [N]: Different value per individual, same across genes
|
|
8
|
+
4. Per-gene + Per-individual [N, D]: Full matrix
|
|
9
|
+
|
|
10
|
+
Run with:
|
|
11
|
+
python -m tests.test_per_individual
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import torch
|
|
15
|
+
import sys
|
|
16
|
+
import os
|
|
17
|
+
|
|
18
|
+
# Add parent to path
|
|
19
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
20
|
+
|
|
21
|
+
from evograd.operators.crossover import (
|
|
22
|
+
BinomialCrossover,
|
|
23
|
+
SBXCrossover,
|
|
24
|
+
BlendCrossover,
|
|
25
|
+
ArithmeticCrossover,
|
|
26
|
+
)
|
|
27
|
+
from evograd.operators.mutation import (
|
|
28
|
+
PolynomialMutation,
|
|
29
|
+
GaussianMutation,
|
|
30
|
+
UniformMutation,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_crossover_configurations():
|
|
35
|
+
"""Test all four parameter configurations for crossover operators."""
|
|
36
|
+
print("\n" + "=" * 70)
|
|
37
|
+
print("Testing Crossover Per-Individual/Per-Gene Configurations")
|
|
38
|
+
print("=" * 70)
|
|
39
|
+
|
|
40
|
+
N, D = 50, 10 # 50 individuals, 10 variables
|
|
41
|
+
parent1 = torch.randn(N, D)
|
|
42
|
+
parent2 = torch.randn(N, D)
|
|
43
|
+
|
|
44
|
+
# ==========================================================================
|
|
45
|
+
# BinomialCrossover (DE-style) - Most important for SHADE
|
|
46
|
+
# ==========================================================================
|
|
47
|
+
print("\n1. BinomialCrossover configurations:")
|
|
48
|
+
crossover = BinomialCrossover(cr=0.9)
|
|
49
|
+
|
|
50
|
+
# Config 1: Fixed (default)
|
|
51
|
+
print(" a) Fixed CR (scalar) - default behavior")
|
|
52
|
+
trial = crossover(parent1, parent2)
|
|
53
|
+
assert trial.shape == (N, D), f"Expected ({N}, {D}), got {trial.shape}"
|
|
54
|
+
print(f" Shape: {trial.shape} ✓")
|
|
55
|
+
|
|
56
|
+
# Config 2: Per-gene [D]
|
|
57
|
+
print(" b) Per-gene CR [D]")
|
|
58
|
+
cr_per_gene = torch.linspace(0.5, 1.0, D) # Different CR per gene
|
|
59
|
+
trial = crossover(parent1, parent2, cr=cr_per_gene)
|
|
60
|
+
assert trial.shape == (N, D)
|
|
61
|
+
print(f" CR shape: {cr_per_gene.shape} -> Output: {trial.shape} ✓")
|
|
62
|
+
|
|
63
|
+
# Config 3: Per-individual [N] - SHADE needs this!
|
|
64
|
+
print(" c) Per-individual CR [N] - SHADE-style")
|
|
65
|
+
cr_per_ind = torch.rand(N) * 0.5 + 0.5 # CR in [0.5, 1.0] per individual
|
|
66
|
+
trial = crossover(parent1, parent2, cr=cr_per_ind)
|
|
67
|
+
assert trial.shape == (N, D)
|
|
68
|
+
print(f" CR shape: {cr_per_ind.shape} -> Output: {trial.shape} ✓")
|
|
69
|
+
|
|
70
|
+
# Config 4: Full matrix [N, D]
|
|
71
|
+
print(" d) Full matrix CR [N, D]")
|
|
72
|
+
cr_matrix = torch.rand(N, D)
|
|
73
|
+
trial = crossover(parent1, parent2, cr=cr_matrix)
|
|
74
|
+
assert trial.shape == (N, D)
|
|
75
|
+
print(f" CR shape: {cr_matrix.shape} -> Output: {trial.shape} ✓")
|
|
76
|
+
|
|
77
|
+
# ==========================================================================
|
|
78
|
+
# SBXCrossover - Test eta and prob overrides
|
|
79
|
+
# ==========================================================================
|
|
80
|
+
print("\n2. SBXCrossover configurations:")
|
|
81
|
+
sbx = SBXCrossover(eta=15, prob=0.9)
|
|
82
|
+
|
|
83
|
+
# Per-individual eta
|
|
84
|
+
print(" a) Per-individual eta [N]")
|
|
85
|
+
eta_per_ind = torch.rand(N) * 15 + 5 # eta in [5, 20] per individual
|
|
86
|
+
offspring = sbx(parent1, parent2, eta=eta_per_ind)
|
|
87
|
+
assert offspring.shape == (N, D)
|
|
88
|
+
print(f" Eta shape: {eta_per_ind.shape} -> Output: {offspring.shape} ✓")
|
|
89
|
+
|
|
90
|
+
# Per-gene prob
|
|
91
|
+
print(" b) Per-gene prob [D]")
|
|
92
|
+
prob_per_gene = torch.linspace(0.5, 1.0, D)
|
|
93
|
+
offspring = sbx(parent1, parent2, prob=prob_per_gene)
|
|
94
|
+
assert offspring.shape == (N, D)
|
|
95
|
+
print(f" Prob shape: {prob_per_gene.shape} -> Output: {offspring.shape} ✓")
|
|
96
|
+
|
|
97
|
+
# Both eta and prob as full matrices
|
|
98
|
+
print(" c) Full matrix eta and prob [N, D]")
|
|
99
|
+
eta_matrix = torch.rand(N, D) * 15 + 5
|
|
100
|
+
prob_matrix = torch.rand(N, D) * 0.5 + 0.5
|
|
101
|
+
offspring = sbx(parent1, parent2, eta=eta_matrix, prob=prob_matrix)
|
|
102
|
+
assert offspring.shape == (N, D)
|
|
103
|
+
print(f" Eta: {eta_matrix.shape}, Prob: {prob_matrix.shape} -> Output: {offspring.shape} ✓")
|
|
104
|
+
|
|
105
|
+
# ==========================================================================
|
|
106
|
+
# ArithmeticCrossover - Test alpha override
|
|
107
|
+
# ==========================================================================
|
|
108
|
+
print("\n3. ArithmeticCrossover configurations:")
|
|
109
|
+
arith = ArithmeticCrossover(alpha=0.5)
|
|
110
|
+
|
|
111
|
+
# Per-individual alpha
|
|
112
|
+
print(" a) Per-individual alpha [N]")
|
|
113
|
+
alpha_per_ind = torch.rand(N)
|
|
114
|
+
offspring = arith(parent1, parent2, alpha=alpha_per_ind)
|
|
115
|
+
assert offspring.shape == (N, D)
|
|
116
|
+
print(f" Alpha shape: {alpha_per_ind.shape} -> Output: {offspring.shape} ✓")
|
|
117
|
+
|
|
118
|
+
# Verify arithmetic crossover formula
|
|
119
|
+
print(" b) Verify formula: offspring = alpha * p1 + (1-alpha) * p2")
|
|
120
|
+
alpha_test = 0.3
|
|
121
|
+
offspring_test = arith(parent1, parent2, alpha=alpha_test)
|
|
122
|
+
expected = alpha_test * parent1 + (1 - alpha_test) * parent2
|
|
123
|
+
assert torch.allclose(offspring_test, expected, atol=1e-6)
|
|
124
|
+
print(f" Formula verified ✓")
|
|
125
|
+
|
|
126
|
+
print("\n✓ All crossover configurations passed!")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def test_mutation_configurations():
|
|
130
|
+
"""Test all four parameter configurations for mutation operators."""
|
|
131
|
+
print("\n" + "=" * 70)
|
|
132
|
+
print("Testing Mutation Per-Individual/Per-Gene Configurations")
|
|
133
|
+
print("=" * 70)
|
|
134
|
+
|
|
135
|
+
N, D = 50, 10 # 50 individuals, 10 variables
|
|
136
|
+
population = torch.randn(N, D)
|
|
137
|
+
xl = torch.zeros(D)
|
|
138
|
+
xu = torch.ones(D)
|
|
139
|
+
|
|
140
|
+
# ==========================================================================
|
|
141
|
+
# PolynomialMutation - Test eta and prob overrides
|
|
142
|
+
# ==========================================================================
|
|
143
|
+
print("\n1. PolynomialMutation configurations:")
|
|
144
|
+
mutation = PolynomialMutation(eta=20, prob=0.1)
|
|
145
|
+
|
|
146
|
+
# Config 1: Fixed (default)
|
|
147
|
+
print(" a) Fixed eta and prob (scalar) - default behavior")
|
|
148
|
+
mutated = mutation(population, xl, xu)
|
|
149
|
+
assert mutated.shape == (N, D)
|
|
150
|
+
print(f" Shape: {mutated.shape} ✓")
|
|
151
|
+
|
|
152
|
+
# Config 2: Per-gene [D]
|
|
153
|
+
print(" b) Per-gene eta [D]")
|
|
154
|
+
eta_per_gene = torch.linspace(10, 30, D)
|
|
155
|
+
mutated = mutation(population, xl, xu, eta=eta_per_gene)
|
|
156
|
+
assert mutated.shape == (N, D)
|
|
157
|
+
print(f" Eta shape: {eta_per_gene.shape} -> Output: {mutated.shape} ✓")
|
|
158
|
+
|
|
159
|
+
# Config 3: Per-individual [N]
|
|
160
|
+
print(" c) Per-individual eta [N] - Self-adaptive GA style")
|
|
161
|
+
eta_per_ind = torch.rand(N) * 20 + 10 # eta in [10, 30] per individual
|
|
162
|
+
mutated = mutation(population, xl, xu, eta=eta_per_ind)
|
|
163
|
+
assert mutated.shape == (N, D)
|
|
164
|
+
print(f" Eta shape: {eta_per_ind.shape} -> Output: {mutated.shape} ✓")
|
|
165
|
+
|
|
166
|
+
# Config 4: Full matrix [N, D]
|
|
167
|
+
print(" d) Full matrix eta and prob [N, D]")
|
|
168
|
+
eta_matrix = torch.rand(N, D) * 20 + 10
|
|
169
|
+
prob_matrix = torch.rand(N, D) * 0.2
|
|
170
|
+
mutated = mutation(population, xl, xu, eta=eta_matrix, prob=prob_matrix)
|
|
171
|
+
assert mutated.shape == (N, D)
|
|
172
|
+
print(f" Eta: {eta_matrix.shape}, Prob: {prob_matrix.shape} -> Output: {mutated.shape} ✓")
|
|
173
|
+
|
|
174
|
+
# ==========================================================================
|
|
175
|
+
# GaussianMutation - Test sigma override (important for DE/SHADE)
|
|
176
|
+
# ==========================================================================
|
|
177
|
+
print("\n2. GaussianMutation configurations:")
|
|
178
|
+
gauss = GaussianMutation(sigma=0.1)
|
|
179
|
+
|
|
180
|
+
# Per-individual sigma (like F in DE/SHADE)
|
|
181
|
+
print(" a) Per-individual sigma [N] - SHADE F-style")
|
|
182
|
+
F_per_ind = torch.rand(N) * 0.5 + 0.5 # F in [0.5, 1.0] per individual
|
|
183
|
+
mutated = gauss(population, xl, xu, sigma=F_per_ind)
|
|
184
|
+
assert mutated.shape == (N, D)
|
|
185
|
+
print(f" Sigma shape: {F_per_ind.shape} -> Output: {mutated.shape} ✓")
|
|
186
|
+
|
|
187
|
+
# Per-gene sigma
|
|
188
|
+
print(" b) Per-gene sigma [D]")
|
|
189
|
+
sigma_per_gene = torch.linspace(0.05, 0.2, D)
|
|
190
|
+
mutated = gauss(population, xl, xu, sigma=sigma_per_gene)
|
|
191
|
+
assert mutated.shape == (N, D)
|
|
192
|
+
print(f" Sigma shape: {sigma_per_gene.shape} -> Output: {mutated.shape} ✓")
|
|
193
|
+
|
|
194
|
+
# ==========================================================================
|
|
195
|
+
# UniformMutation - Test prob override
|
|
196
|
+
# ==========================================================================
|
|
197
|
+
print("\n3. UniformMutation configurations:")
|
|
198
|
+
unif = UniformMutation(prob=0.05)
|
|
199
|
+
|
|
200
|
+
# Per-individual prob
|
|
201
|
+
print(" a) Per-individual prob [N]")
|
|
202
|
+
prob_per_ind = torch.rand(N) * 0.1
|
|
203
|
+
mutated = unif(population, xl, xu, prob=prob_per_ind)
|
|
204
|
+
assert mutated.shape == (N, D)
|
|
205
|
+
print(f" Prob shape: {prob_per_ind.shape} -> Output: {mutated.shape} ✓")
|
|
206
|
+
|
|
207
|
+
print("\n✓ All mutation configurations passed!")
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def test_shade_style_usage():
|
|
211
|
+
"""Demonstrate SHADE-style usage with per-individual F and CR."""
|
|
212
|
+
print("\n" + "=" * 70)
|
|
213
|
+
print("Demonstrating SHADE-style Usage")
|
|
214
|
+
print("=" * 70)
|
|
215
|
+
|
|
216
|
+
N, D = 100, 30
|
|
217
|
+
|
|
218
|
+
# Simulated SHADE setup
|
|
219
|
+
print("\n1. Setting up SHADE-style parameters:")
|
|
220
|
+
|
|
221
|
+
# Population
|
|
222
|
+
population = torch.rand(N, D)
|
|
223
|
+
xl = torch.zeros(D)
|
|
224
|
+
xu = torch.ones(D)
|
|
225
|
+
|
|
226
|
+
# Per-individual F and CR (sampled from historical memory in real SHADE)
|
|
227
|
+
F_per_ind = torch.randn(N).abs() * 0.3 + 0.5 # F ~ |N(0.5, 0.3)|
|
|
228
|
+
CR_per_ind = torch.randn(N) * 0.1 + 0.5 # CR ~ N(0.5, 0.1)
|
|
229
|
+
CR_per_ind = CR_per_ind.clamp(0, 1)
|
|
230
|
+
|
|
231
|
+
print(f" F per individual: shape={F_per_ind.shape}, range=[{F_per_ind.min():.3f}, {F_per_ind.max():.3f}]")
|
|
232
|
+
print(f" CR per individual: shape={CR_per_ind.shape}, range=[{CR_per_ind.min():.3f}, {CR_per_ind.max():.3f}]")
|
|
233
|
+
|
|
234
|
+
# Select random individuals for mutation (DE/rand/1)
|
|
235
|
+
print("\n2. Creating donor vectors (DE/rand/1 style):")
|
|
236
|
+
r1 = torch.randint(0, N, (N,))
|
|
237
|
+
r2 = torch.randint(0, N, (N,))
|
|
238
|
+
r3 = torch.randint(0, N, (N,))
|
|
239
|
+
|
|
240
|
+
# Mutation: donor = x_r1 + F * (x_r2 - x_r3)
|
|
241
|
+
# Here we use per-individual F
|
|
242
|
+
diff = population[r2] - population[r3]
|
|
243
|
+
donor = population[r1] + F_per_ind.unsqueeze(1) * diff
|
|
244
|
+
print(f" Donor vectors created: {donor.shape}")
|
|
245
|
+
|
|
246
|
+
# Binomial crossover with per-individual CR
|
|
247
|
+
print("\n3. Applying binomial crossover with per-individual CR:")
|
|
248
|
+
crossover = BinomialCrossover(cr=0.5) # Default CR, but we'll override
|
|
249
|
+
trial = crossover(population, donor, cr=CR_per_ind)
|
|
250
|
+
print(f" Trial vectors created: {trial.shape}")
|
|
251
|
+
|
|
252
|
+
# Verify different individuals have different number of genes crossed
|
|
253
|
+
genes_from_donor = (trial == donor).float().sum(dim=1)
|
|
254
|
+
print(f" Genes from donor (per individual): mean={genes_from_donor.mean():.1f}, std={genes_from_donor.std():.1f}")
|
|
255
|
+
|
|
256
|
+
print("\n✓ SHADE-style demonstration complete!")
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def test_differentiable_mode():
|
|
260
|
+
"""Test gradient flow with per-individual parameters."""
|
|
261
|
+
print("\n" + "=" * 70)
|
|
262
|
+
print("Testing Gradient Flow with Per-Individual Parameters")
|
|
263
|
+
print("=" * 70)
|
|
264
|
+
|
|
265
|
+
N, D = 20, 5
|
|
266
|
+
|
|
267
|
+
# Differentiable operators
|
|
268
|
+
print("\n1. SBXCrossover with gradient flow:")
|
|
269
|
+
sbx = SBXCrossover(eta=15, prob=0.9, differentiable=True, learn_eta=True)
|
|
270
|
+
|
|
271
|
+
p1 = torch.nn.Parameter(torch.randn(N, D))
|
|
272
|
+
p2 = torch.randn(N, D)
|
|
273
|
+
eta_per_ind = torch.rand(N) * 10 + 5 # Not learnable, just passed in
|
|
274
|
+
|
|
275
|
+
offspring = sbx(p1, p2, eta=eta_per_ind)
|
|
276
|
+
loss = offspring.sum()
|
|
277
|
+
loss.backward()
|
|
278
|
+
|
|
279
|
+
assert p1.grad is not None, "Gradients should flow to parent1"
|
|
280
|
+
print(f" Gradients flow through per-individual eta: ✓")
|
|
281
|
+
print(f" Parent1 grad norm: {p1.grad.norm():.4f}")
|
|
282
|
+
|
|
283
|
+
# PolynomialMutation with gradient flow
|
|
284
|
+
print("\n2. PolynomialMutation with gradient flow:")
|
|
285
|
+
mutation = PolynomialMutation(eta=20, prob=0.1, differentiable=True, learn_eta=True)
|
|
286
|
+
|
|
287
|
+
x = torch.nn.Parameter(torch.randn(N, D))
|
|
288
|
+
xl = torch.zeros(D)
|
|
289
|
+
xu = torch.ones(D)
|
|
290
|
+
eta_per_ind = torch.rand(N) * 20 + 10
|
|
291
|
+
|
|
292
|
+
mutated = mutation(x, xl, xu, eta=eta_per_ind)
|
|
293
|
+
loss = mutated.sum()
|
|
294
|
+
loss.backward()
|
|
295
|
+
|
|
296
|
+
assert x.grad is not None, "Gradients should flow to input"
|
|
297
|
+
print(f" Gradients flow through per-individual eta: ✓")
|
|
298
|
+
print(f" Input grad norm: {x.grad.norm():.4f}")
|
|
299
|
+
|
|
300
|
+
print("\n✓ All gradient tests passed!")
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def main():
|
|
304
|
+
"""Run all tests."""
|
|
305
|
+
print("\n" + "#" * 70)
|
|
306
|
+
print("# Per-Individual/Per-Gene Parameter Support Tests")
|
|
307
|
+
print("#" * 70)
|
|
308
|
+
|
|
309
|
+
test_crossover_configurations()
|
|
310
|
+
test_mutation_configurations()
|
|
311
|
+
test_shade_style_usage()
|
|
312
|
+
test_differentiable_mode()
|
|
313
|
+
|
|
314
|
+
print("\n" + "=" * 70)
|
|
315
|
+
print("ALL TESTS PASSED! ✓")
|
|
316
|
+
print("=" * 70)
|
|
317
|
+
|
|
318
|
+
print("\nSummary of Four Configurations:")
|
|
319
|
+
print(" 1. Fixed (scalar) - Same value for all")
|
|
320
|
+
print(" 2. Per-gene [D] - Different per variable")
|
|
321
|
+
print(" 3. Per-individual [N] - Different per individual (SHADE needs this!)")
|
|
322
|
+
print(" 4. Full matrix [N, D] - Maximum flexibility")
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
if __name__ == "__main__":
|
|
326
|
+
main()
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Test script for EvoGrad utils module.
|
|
3
|
+
|
|
4
|
+
Tests:
|
|
5
|
+
- device.py: Device detection and tensor movement
|
|
6
|
+
- duplicates.py: Duplicate elimination
|
|
7
|
+
- callbacks.py: Callback system
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
python -m evograd.tests.test_utils
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import sys
|
|
14
|
+
import torch
|
|
15
|
+
import torch.nn as nn
|
|
16
|
+
import tempfile
|
|
17
|
+
import os
|
|
18
|
+
|
|
19
|
+
# Add parent of evograd to path for imports
|
|
20
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
|
21
|
+
|
|
22
|
+
from evograd.utils.device import get_device, to_device
|
|
23
|
+
from evograd.utils.duplicates import (
|
|
24
|
+
DuplicateMethod,
|
|
25
|
+
DuplicateEliminator,
|
|
26
|
+
eliminate_duplicates,
|
|
27
|
+
has_duplicates,
|
|
28
|
+
count_duplicates,
|
|
29
|
+
)
|
|
30
|
+
from evograd.utils.callbacks import (
|
|
31
|
+
Callback,
|
|
32
|
+
CallbackList,
|
|
33
|
+
CallbackState,
|
|
34
|
+
HistoryCallback,
|
|
35
|
+
PrintCallback,
|
|
36
|
+
EarlyStoppingCallback,
|
|
37
|
+
CheckpointCallback,
|
|
38
|
+
ConvergenceCallback,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_device():
|
|
43
|
+
"""Test device detection and tensor movement."""
|
|
44
|
+
print("\n" + "="*60)
|
|
45
|
+
print("Testing device.py")
|
|
46
|
+
print("="*60)
|
|
47
|
+
|
|
48
|
+
# Test get_device
|
|
49
|
+
print("\n1. Testing get_device()...")
|
|
50
|
+
device = get_device()
|
|
51
|
+
print(f" Default device: {device}")
|
|
52
|
+
assert isinstance(device, torch.device)
|
|
53
|
+
|
|
54
|
+
# Test with specific device string
|
|
55
|
+
cpu_device = get_device("cpu")
|
|
56
|
+
print(f" CPU device: {cpu_device}")
|
|
57
|
+
assert cpu_device.type == "cpu"
|
|
58
|
+
|
|
59
|
+
# Test auto detection (None means auto)
|
|
60
|
+
auto_device = get_device(None)
|
|
61
|
+
print(f" Auto device: {auto_device}")
|
|
62
|
+
|
|
63
|
+
# Test to_device with tensors (keyword-only device argument)
|
|
64
|
+
print("\n2. Testing to_device() with tensors...")
|
|
65
|
+
x = torch.randn(10, 5)
|
|
66
|
+
y = torch.randn(3, 3)
|
|
67
|
+
x_moved, y_moved = to_device(x, y, device=cpu_device)
|
|
68
|
+
print(f" Moved tensors to {x_moved.device}")
|
|
69
|
+
assert x_moved.device == cpu_device
|
|
70
|
+
assert y_moved.device == cpu_device
|
|
71
|
+
|
|
72
|
+
# Test to_device with single tensor
|
|
73
|
+
print("\n3. Testing to_device() with single tensor...")
|
|
74
|
+
z = torch.randn(5)
|
|
75
|
+
(z_moved,) = to_device(z, device=cpu_device)
|
|
76
|
+
print(f" Moved single tensor to {z_moved.device}")
|
|
77
|
+
assert z_moved.device == cpu_device
|
|
78
|
+
|
|
79
|
+
print("\n✓ device.py tests passed!")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_duplicates():
|
|
83
|
+
"""Test duplicate elimination strategies."""
|
|
84
|
+
print("\n" + "="*60)
|
|
85
|
+
print("Testing duplicates.py")
|
|
86
|
+
print("="*60)
|
|
87
|
+
|
|
88
|
+
# Create test population with duplicates
|
|
89
|
+
pop = torch.tensor([
|
|
90
|
+
[1.0, 2.0, 3.0],
|
|
91
|
+
[1.0, 2.0, 3.0], # Exact duplicate of row 0
|
|
92
|
+
[4.0, 5.0, 6.0],
|
|
93
|
+
[1.0001, 2.0001, 3.0001], # Near duplicate of row 0
|
|
94
|
+
[7.0, 8.0, 9.0],
|
|
95
|
+
])
|
|
96
|
+
|
|
97
|
+
xl = torch.zeros(3)
|
|
98
|
+
xu = torch.ones(3) * 10
|
|
99
|
+
|
|
100
|
+
# Test DuplicateEliminator with EPSILON_L2
|
|
101
|
+
print("\n1. Testing DuplicateEliminator (EPSILON_L2)...")
|
|
102
|
+
eliminator = DuplicateEliminator(
|
|
103
|
+
method=DuplicateMethod.EPSILON_L2,
|
|
104
|
+
epsilon=0.01,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# Eliminate duplicates by calling the eliminator
|
|
108
|
+
new_pop = eliminator(pop, xl, xu)
|
|
109
|
+
print(f" Original population shape: {pop.shape}")
|
|
110
|
+
print(f" Population after elimination: {new_pop.shape}")
|
|
111
|
+
print(f" Duplicates found: {eliminator.n_duplicates_found}")
|
|
112
|
+
print(f" Duplicates resolved: {eliminator.n_duplicates_resolved}")
|
|
113
|
+
|
|
114
|
+
# Test has_duplicates function
|
|
115
|
+
print("\n2. Testing has_duplicates()...")
|
|
116
|
+
has_dups = has_duplicates(pop, epsilon=0.01)
|
|
117
|
+
print(f" Has duplicates: {has_dups}")
|
|
118
|
+
assert has_dups == True, "Population should have duplicates"
|
|
119
|
+
|
|
120
|
+
# Test count_duplicates function
|
|
121
|
+
print("\n3. Testing count_duplicates()...")
|
|
122
|
+
n_dups = count_duplicates(pop, epsilon=0.01)
|
|
123
|
+
print(f" Number of duplicates: {n_dups}")
|
|
124
|
+
assert n_dups >= 1, "Should find at least 1 duplicate"
|
|
125
|
+
|
|
126
|
+
# Test DuplicateEliminator with HASH method
|
|
127
|
+
print("\n4. Testing DuplicateEliminator (HASH)...")
|
|
128
|
+
hash_eliminator = DuplicateEliminator(
|
|
129
|
+
method=DuplicateMethod.HASH,
|
|
130
|
+
decimals=2,
|
|
131
|
+
)
|
|
132
|
+
new_pop_hash = hash_eliminator(pop, xl, xu)
|
|
133
|
+
print(f" Hash method duplicates found: {hash_eliminator.n_duplicates_found}")
|
|
134
|
+
|
|
135
|
+
# Test DuplicateEliminator with NONE method
|
|
136
|
+
print("\n5. Testing DuplicateEliminator (NONE)...")
|
|
137
|
+
no_elim = DuplicateEliminator(method=DuplicateMethod.NONE)
|
|
138
|
+
new_pop_none = no_elim(pop, xl, xu)
|
|
139
|
+
assert torch.allclose(new_pop_none, pop), "NONE method should not modify population"
|
|
140
|
+
print(" NONE method correctly leaves population unchanged")
|
|
141
|
+
|
|
142
|
+
# Test eliminate_duplicates convenience function
|
|
143
|
+
print("\n6. Testing eliminate_duplicates()...")
|
|
144
|
+
cleaned_pop = eliminate_duplicates(pop, xl, xu, epsilon=0.01)
|
|
145
|
+
print(f" Cleaned population shape: {cleaned_pop.shape}")
|
|
146
|
+
|
|
147
|
+
print("\n✓ duplicates.py tests passed!")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def test_callbacks():
|
|
151
|
+
"""Test callback system."""
|
|
152
|
+
print("\n" + "="*60)
|
|
153
|
+
print("Testing callbacks.py")
|
|
154
|
+
print("="*60)
|
|
155
|
+
|
|
156
|
+
# Test HistoryCallback
|
|
157
|
+
print("\n1. Testing HistoryCallback...")
|
|
158
|
+
history_cb = HistoryCallback(
|
|
159
|
+
track_population=True,
|
|
160
|
+
track_hyperparams=True,
|
|
161
|
+
track_diversity=False,
|
|
162
|
+
track_fitness_stats=True,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# Create initial state
|
|
166
|
+
state = CallbackState(
|
|
167
|
+
generation=0,
|
|
168
|
+
n_evals=0,
|
|
169
|
+
best_fitness=float('inf'),
|
|
170
|
+
best_solution=torch.randn(5),
|
|
171
|
+
current_fitness=torch.randn(10),
|
|
172
|
+
current_population=torch.randn(10, 5),
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
history_cb.on_optimisation_start(state)
|
|
176
|
+
|
|
177
|
+
for gen in range(5):
|
|
178
|
+
state.generation = gen
|
|
179
|
+
state.n_evals = (gen + 1) * 10
|
|
180
|
+
state.best_fitness = 100.0 / (gen + 1)
|
|
181
|
+
state.current_fitness = torch.randn(10)
|
|
182
|
+
history_cb.on_generation_end(state)
|
|
183
|
+
|
|
184
|
+
print(f" Tracked generations: {len(history_cb.generations)}")
|
|
185
|
+
print(f" Best fitness history: {history_cb.best_fitness[:3]}...")
|
|
186
|
+
assert len(history_cb.generations) == 5
|
|
187
|
+
assert len(history_cb.best_fitness) == 5
|
|
188
|
+
|
|
189
|
+
# Test PrintCallback
|
|
190
|
+
print("\n2. Testing PrintCallback...")
|
|
191
|
+
print_cb = PrintCallback(every=2, show_time=True)
|
|
192
|
+
|
|
193
|
+
state = CallbackState(generation=0, n_evals=0, best_fitness=100.0)
|
|
194
|
+
print_cb.on_optimisation_start(state)
|
|
195
|
+
|
|
196
|
+
for gen in range(4):
|
|
197
|
+
state.generation = gen
|
|
198
|
+
state.best_fitness = 100.0 - gen * 10
|
|
199
|
+
state.n_evals = gen * 10
|
|
200
|
+
print_cb.on_generation_end(state)
|
|
201
|
+
|
|
202
|
+
print_cb.on_optimisation_end(state)
|
|
203
|
+
print(" PrintCallback executed without errors")
|
|
204
|
+
|
|
205
|
+
# Test EarlyStoppingCallback
|
|
206
|
+
print("\n3. Testing EarlyStoppingCallback...")
|
|
207
|
+
early_stop_cb = EarlyStoppingCallback(
|
|
208
|
+
patience=3,
|
|
209
|
+
min_delta=0.1,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
state = CallbackState(generation=0, n_evals=0, best_fitness=100.0)
|
|
213
|
+
early_stop_cb.on_optimisation_start(state)
|
|
214
|
+
|
|
215
|
+
# Simulate improvement then stagnation
|
|
216
|
+
fitness_sequence = [100, 90, 80, 80, 80, 80] # Stagnates after 3rd
|
|
217
|
+
stopped_at = None
|
|
218
|
+
for gen, fit in enumerate(fitness_sequence):
|
|
219
|
+
state.generation = gen
|
|
220
|
+
state.best_fitness = fit
|
|
221
|
+
early_stop_cb.on_generation_end(state)
|
|
222
|
+
if state.stop_optimisation:
|
|
223
|
+
stopped_at = gen
|
|
224
|
+
break
|
|
225
|
+
|
|
226
|
+
print(f" Early stopping triggered at generation: {stopped_at}")
|
|
227
|
+
assert stopped_at is not None, "Early stopping should have triggered"
|
|
228
|
+
|
|
229
|
+
# Test CheckpointCallback
|
|
230
|
+
print("\n4. Testing CheckpointCallback...")
|
|
231
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
232
|
+
# Create a mock module for checkpointing
|
|
233
|
+
mock_module = nn.Linear(5, 1)
|
|
234
|
+
|
|
235
|
+
checkpoint_cb = CheckpointCallback(
|
|
236
|
+
directory=tmpdir,
|
|
237
|
+
every=2,
|
|
238
|
+
save_best_only=True,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
state = CallbackState(
|
|
242
|
+
generation=0,
|
|
243
|
+
n_evals=0,
|
|
244
|
+
best_fitness=100.0,
|
|
245
|
+
algorithm=mock_module,
|
|
246
|
+
)
|
|
247
|
+
checkpoint_cb.on_optimisation_start(state)
|
|
248
|
+
|
|
249
|
+
for gen in range(5):
|
|
250
|
+
state.generation = gen
|
|
251
|
+
state.best_fitness = 100.0 - gen * 20
|
|
252
|
+
checkpoint_cb.on_generation_end(state)
|
|
253
|
+
|
|
254
|
+
checkpoint_cb.on_optimisation_end(state)
|
|
255
|
+
|
|
256
|
+
# Check files were created
|
|
257
|
+
files = os.listdir(tmpdir)
|
|
258
|
+
print(f" Checkpoint files created: {files}")
|
|
259
|
+
assert len(files) > 0, "Should have created checkpoint files"
|
|
260
|
+
|
|
261
|
+
# Test ConvergenceCallback
|
|
262
|
+
print("\n5. Testing ConvergenceCallback...")
|
|
263
|
+
conv_cb = ConvergenceCallback(
|
|
264
|
+
threshold=0.001,
|
|
265
|
+
window=3,
|
|
266
|
+
min_generations=0,
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
state = CallbackState(generation=0, n_evals=0, best_fitness=100.0)
|
|
270
|
+
conv_cb.on_optimisation_start(state)
|
|
271
|
+
|
|
272
|
+
# Simulate convergence
|
|
273
|
+
fitness_sequence = [100.0, 50.0, 25.0, 24.999, 24.998, 24.997]
|
|
274
|
+
stopped_at = None
|
|
275
|
+
for gen, fit in enumerate(fitness_sequence):
|
|
276
|
+
state.generation = gen
|
|
277
|
+
state.best_fitness = fit
|
|
278
|
+
conv_cb.on_generation_end(state)
|
|
279
|
+
if state.stop_optimisation:
|
|
280
|
+
stopped_at = gen
|
|
281
|
+
break
|
|
282
|
+
|
|
283
|
+
print(f" Convergence detected at generation: {stopped_at}")
|
|
284
|
+
|
|
285
|
+
# Test CallbackList
|
|
286
|
+
print("\n6. Testing CallbackList...")
|
|
287
|
+
cb_list = CallbackList([
|
|
288
|
+
HistoryCallback(),
|
|
289
|
+
PrintCallback(every=10),
|
|
290
|
+
])
|
|
291
|
+
|
|
292
|
+
state = CallbackState(generation=0, n_evals=0, best_fitness=100.0)
|
|
293
|
+
cb_list.on_optimisation_start(state)
|
|
294
|
+
for gen in range(3):
|
|
295
|
+
state.generation = gen
|
|
296
|
+
state.best_fitness = 100.0 - gen
|
|
297
|
+
cb_list.on_generation_end(state)
|
|
298
|
+
cb_list.on_optimisation_end(state)
|
|
299
|
+
print(" CallbackList executed all callbacks")
|
|
300
|
+
|
|
301
|
+
print("\n✓ callbacks.py tests passed!")
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def run_all_tests():
|
|
305
|
+
"""Run all utils tests."""
|
|
306
|
+
print("\n" + "#"*60)
|
|
307
|
+
print("# EvoGrad Utils Module Tests")
|
|
308
|
+
print("#"*60)
|
|
309
|
+
|
|
310
|
+
try:
|
|
311
|
+
test_device()
|
|
312
|
+
test_duplicates()
|
|
313
|
+
test_callbacks()
|
|
314
|
+
|
|
315
|
+
print("\n" + "="*60)
|
|
316
|
+
print("✓ ALL UTILS TESTS PASSED!")
|
|
317
|
+
print("="*60)
|
|
318
|
+
return True
|
|
319
|
+
except Exception as e:
|
|
320
|
+
print(f"\n✗ TEST FAILED: {e}")
|
|
321
|
+
import traceback
|
|
322
|
+
traceback.print_exc()
|
|
323
|
+
return False
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
if __name__ == "__main__":
|
|
327
|
+
success = run_all_tests()
|
|
328
|
+
sys.exit(0 if success else 1)
|