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
evograd/core/result.py
ADDED
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Result container for EvoGrad optimisation.
|
|
3
|
+
|
|
4
|
+
This module provides a clean container for storing and accessing
|
|
5
|
+
optimisation results returned by minimize/maximize functions.
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> from evograd.core import minimize
|
|
9
|
+
>>>
|
|
10
|
+
>>> result = minimize(algorithm, problem, max_evals=10000)
|
|
11
|
+
>>>
|
|
12
|
+
>>> print(f"Best fitness: {result.best_fitness}")
|
|
13
|
+
>>> print(f"Best solution: {result.best_solution}")
|
|
14
|
+
>>> print(f"Evaluations: {result.n_evals}")
|
|
15
|
+
>>> print(f"Generations: {result.n_gen}")
|
|
16
|
+
>>> print(f"Success: {result.success}")
|
|
17
|
+
>>>
|
|
18
|
+
>>> # Access convergence history (if tracked by callback)
|
|
19
|
+
>>> if result.history:
|
|
20
|
+
... plt.plot(result.history['best_fitness'])
|
|
21
|
+
>>>
|
|
22
|
+
>>> # Save/load results
|
|
23
|
+
>>> result.save('result.pt')
|
|
24
|
+
>>> loaded = Result.load('result.pt')
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
from dataclasses import dataclass, field
|
|
30
|
+
from datetime import datetime
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
from typing import Any, Dict, List, Optional, Union
|
|
33
|
+
|
|
34
|
+
import torch
|
|
35
|
+
from torch import Tensor
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"Result",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class Result:
|
|
44
|
+
"""
|
|
45
|
+
Container for optimisation results.
|
|
46
|
+
|
|
47
|
+
Attributes:
|
|
48
|
+
best_solution: Best solution found (shape: [n_var]).
|
|
49
|
+
best_fitness: Best fitness value (scalar).
|
|
50
|
+
population: Final population (shape: [pop_size, n_var]).
|
|
51
|
+
fitness: Final fitness values (shape: [pop_size]).
|
|
52
|
+
n_evals: Total number of fitness evaluations.
|
|
53
|
+
n_gen: Total number of generations.
|
|
54
|
+
success: Whether optimisation succeeded (target reached, etc.).
|
|
55
|
+
termination_reason: Why optimisation stopped.
|
|
56
|
+
history: Convergence history (from callbacks).
|
|
57
|
+
hyperparams: Final algorithm hyperparameters.
|
|
58
|
+
algorithm_state: Full algorithm state for checkpointing.
|
|
59
|
+
problem_name: Name of the problem.
|
|
60
|
+
algorithm_name: Name of the algorithm.
|
|
61
|
+
start_time: When optimisation started.
|
|
62
|
+
end_time: When optimisation ended.
|
|
63
|
+
elapsed_time: Total time in seconds.
|
|
64
|
+
device: Device used for computation.
|
|
65
|
+
extra: Additional user-defined data.
|
|
66
|
+
|
|
67
|
+
Example:
|
|
68
|
+
>>> result = minimize(ga, problem, max_evals=10000)
|
|
69
|
+
>>>
|
|
70
|
+
>>> # Access results
|
|
71
|
+
>>> print(result.best_fitness)
|
|
72
|
+
>>> print(result.best_solution)
|
|
73
|
+
>>>
|
|
74
|
+
>>> # Check success
|
|
75
|
+
>>> if result.success:
|
|
76
|
+
... print("Target reached!")
|
|
77
|
+
>>>
|
|
78
|
+
>>> # Plot convergence
|
|
79
|
+
>>> if 'best_fitness' in result.history:
|
|
80
|
+
... plt.plot(result.history['best_fitness'])
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
# Core results
|
|
84
|
+
best_solution: Tensor
|
|
85
|
+
best_fitness: float
|
|
86
|
+
|
|
87
|
+
# Final population state
|
|
88
|
+
population: Optional[Tensor] = None
|
|
89
|
+
fitness: Optional[Tensor] = None
|
|
90
|
+
|
|
91
|
+
# Counts
|
|
92
|
+
n_evals: int = 0
|
|
93
|
+
n_gen: int = 0
|
|
94
|
+
|
|
95
|
+
# Termination info
|
|
96
|
+
success: bool = False
|
|
97
|
+
termination_reason: Optional[str] = None
|
|
98
|
+
|
|
99
|
+
# History and state
|
|
100
|
+
history: Dict[str, List[Any]] = field(default_factory=dict)
|
|
101
|
+
hyperparams: Dict[str, Any] = field(default_factory=dict)
|
|
102
|
+
algorithm_state: Optional[Dict[str, Any]] = None
|
|
103
|
+
|
|
104
|
+
# Metadata
|
|
105
|
+
problem_name: Optional[str] = None
|
|
106
|
+
algorithm_name: Optional[str] = None
|
|
107
|
+
start_time: Optional[datetime] = None
|
|
108
|
+
end_time: Optional[datetime] = None
|
|
109
|
+
elapsed_time: Optional[float] = None
|
|
110
|
+
device: Optional[str] = None
|
|
111
|
+
|
|
112
|
+
# User data
|
|
113
|
+
extra: Dict[str, Any] = field(default_factory=dict)
|
|
114
|
+
|
|
115
|
+
# -------------------------------------------------------------------------
|
|
116
|
+
# Properties
|
|
117
|
+
# -------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def x(self) -> Tensor:
|
|
121
|
+
"""Alias for best_solution."""
|
|
122
|
+
return self.best_solution
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def f(self) -> float:
|
|
126
|
+
"""Alias for best_fitness."""
|
|
127
|
+
return self.best_fitness
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def X(self) -> Optional[Tensor]:
|
|
131
|
+
"""Alias for population."""
|
|
132
|
+
return self.population
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def F(self) -> Optional[Tensor]:
|
|
136
|
+
"""Alias for fitness."""
|
|
137
|
+
return self.fitness
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def n_var(self) -> int:
|
|
141
|
+
"""Number of variables (dimensions)."""
|
|
142
|
+
return self.best_solution.shape[-1]
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
def pop_size(self) -> Optional[int]:
|
|
146
|
+
"""Population size, if available."""
|
|
147
|
+
if self.population is not None:
|
|
148
|
+
return self.population.shape[0]
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
# -------------------------------------------------------------------------
|
|
152
|
+
# History access
|
|
153
|
+
# -------------------------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
def get_history(self, key: str) -> Optional[List[Any]]:
|
|
156
|
+
"""
|
|
157
|
+
Get history for a specific key.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
key: History key (e.g., 'best_fitness', 'population').
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
List of values or None if key not found.
|
|
164
|
+
"""
|
|
165
|
+
return self.history.get(key)
|
|
166
|
+
|
|
167
|
+
@property
|
|
168
|
+
def best_fitness_history(self) -> Optional[List[float]]:
|
|
169
|
+
"""Convenience accessor for best fitness history."""
|
|
170
|
+
return self.history.get("best_fitness")
|
|
171
|
+
|
|
172
|
+
@property
|
|
173
|
+
def convergence(self) -> Optional[List[float]]:
|
|
174
|
+
"""Alias for best_fitness_history."""
|
|
175
|
+
return self.best_fitness_history
|
|
176
|
+
|
|
177
|
+
# -------------------------------------------------------------------------
|
|
178
|
+
# Utility methods
|
|
179
|
+
# -------------------------------------------------------------------------
|
|
180
|
+
|
|
181
|
+
def to_numpy(self) -> "Result":
|
|
182
|
+
"""
|
|
183
|
+
Convert tensors to numpy arrays.
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
New Result with numpy arrays instead of tensors.
|
|
187
|
+
"""
|
|
188
|
+
import numpy as np
|
|
189
|
+
|
|
190
|
+
def to_np(x: Any) -> Any:
|
|
191
|
+
if isinstance(x, Tensor):
|
|
192
|
+
return x.detach().cpu().numpy()
|
|
193
|
+
return x
|
|
194
|
+
|
|
195
|
+
return Result(
|
|
196
|
+
best_solution=to_np(self.best_solution),
|
|
197
|
+
best_fitness=self.best_fitness,
|
|
198
|
+
population=to_np(self.population),
|
|
199
|
+
fitness=to_np(self.fitness),
|
|
200
|
+
n_evals=self.n_evals,
|
|
201
|
+
n_gen=self.n_gen,
|
|
202
|
+
success=self.success,
|
|
203
|
+
termination_reason=self.termination_reason,
|
|
204
|
+
history={k: [to_np(v) for v in vals] for k, vals in self.history.items()},
|
|
205
|
+
hyperparams=self.hyperparams,
|
|
206
|
+
algorithm_state=None, # State may contain non-serializable items
|
|
207
|
+
problem_name=self.problem_name,
|
|
208
|
+
algorithm_name=self.algorithm_name,
|
|
209
|
+
start_time=self.start_time,
|
|
210
|
+
end_time=self.end_time,
|
|
211
|
+
elapsed_time=self.elapsed_time,
|
|
212
|
+
device=self.device,
|
|
213
|
+
extra=self.extra,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
217
|
+
"""
|
|
218
|
+
Convert to dictionary.
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
Dictionary representation of results.
|
|
222
|
+
"""
|
|
223
|
+
def tensor_to_list(x: Any) -> Any:
|
|
224
|
+
if isinstance(x, Tensor):
|
|
225
|
+
return x.detach().cpu().tolist()
|
|
226
|
+
return x
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
"best_solution": tensor_to_list(self.best_solution),
|
|
230
|
+
"best_fitness": self.best_fitness,
|
|
231
|
+
"population": tensor_to_list(self.population),
|
|
232
|
+
"fitness": tensor_to_list(self.fitness),
|
|
233
|
+
"n_evals": self.n_evals,
|
|
234
|
+
"n_gen": self.n_gen,
|
|
235
|
+
"success": self.success,
|
|
236
|
+
"termination_reason": self.termination_reason,
|
|
237
|
+
"history": {
|
|
238
|
+
k: [tensor_to_list(v) for v in vals]
|
|
239
|
+
for k, vals in self.history.items()
|
|
240
|
+
},
|
|
241
|
+
"hyperparams": self.hyperparams,
|
|
242
|
+
"problem_name": self.problem_name,
|
|
243
|
+
"algorithm_name": self.algorithm_name,
|
|
244
|
+
"start_time": self.start_time.isoformat() if self.start_time else None,
|
|
245
|
+
"end_time": self.end_time.isoformat() if self.end_time else None,
|
|
246
|
+
"elapsed_time": self.elapsed_time,
|
|
247
|
+
"device": self.device,
|
|
248
|
+
"extra": self.extra,
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
# -------------------------------------------------------------------------
|
|
252
|
+
# Save/Load
|
|
253
|
+
# -------------------------------------------------------------------------
|
|
254
|
+
|
|
255
|
+
def save(
|
|
256
|
+
self,
|
|
257
|
+
path: Union[str, Path],
|
|
258
|
+
include_state: bool = True,
|
|
259
|
+
include_history: bool = True,
|
|
260
|
+
) -> None:
|
|
261
|
+
"""
|
|
262
|
+
Save result to file.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
path: File path (will use torch.save).
|
|
266
|
+
include_state: Whether to include algorithm state.
|
|
267
|
+
include_history: Whether to include convergence history.
|
|
268
|
+
|
|
269
|
+
Example:
|
|
270
|
+
>>> result.save('optimization_result.pt')
|
|
271
|
+
"""
|
|
272
|
+
path = Path(path)
|
|
273
|
+
|
|
274
|
+
data = {
|
|
275
|
+
"best_solution": self.best_solution,
|
|
276
|
+
"best_fitness": self.best_fitness,
|
|
277
|
+
"population": self.population,
|
|
278
|
+
"fitness": self.fitness,
|
|
279
|
+
"n_evals": self.n_evals,
|
|
280
|
+
"n_gen": self.n_gen,
|
|
281
|
+
"success": self.success,
|
|
282
|
+
"termination_reason": self.termination_reason,
|
|
283
|
+
"hyperparams": self.hyperparams,
|
|
284
|
+
"problem_name": self.problem_name,
|
|
285
|
+
"algorithm_name": self.algorithm_name,
|
|
286
|
+
"start_time": self.start_time,
|
|
287
|
+
"end_time": self.end_time,
|
|
288
|
+
"elapsed_time": self.elapsed_time,
|
|
289
|
+
"device": self.device,
|
|
290
|
+
"extra": self.extra,
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if include_history:
|
|
294
|
+
data["history"] = self.history
|
|
295
|
+
|
|
296
|
+
if include_state:
|
|
297
|
+
data["algorithm_state"] = self.algorithm_state
|
|
298
|
+
|
|
299
|
+
torch.save(data, path)
|
|
300
|
+
|
|
301
|
+
@classmethod
|
|
302
|
+
def load(cls, path: Union[str, Path]) -> "Result":
|
|
303
|
+
"""
|
|
304
|
+
Load result from file.
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
path: File path to load from.
|
|
308
|
+
|
|
309
|
+
Returns:
|
|
310
|
+
Loaded Result instance.
|
|
311
|
+
|
|
312
|
+
Example:
|
|
313
|
+
>>> result = Result.load('optimization_result.pt')
|
|
314
|
+
"""
|
|
315
|
+
path = Path(path)
|
|
316
|
+
data = torch.load(path, weights_only=False)
|
|
317
|
+
|
|
318
|
+
return cls(
|
|
319
|
+
best_solution=data["best_solution"],
|
|
320
|
+
best_fitness=data["best_fitness"],
|
|
321
|
+
population=data.get("population"),
|
|
322
|
+
fitness=data.get("fitness"),
|
|
323
|
+
n_evals=data.get("n_evals", 0),
|
|
324
|
+
n_gen=data.get("n_gen", 0),
|
|
325
|
+
success=data.get("success", False),
|
|
326
|
+
termination_reason=data.get("termination_reason"),
|
|
327
|
+
history=data.get("history", {}),
|
|
328
|
+
hyperparams=data.get("hyperparams", {}),
|
|
329
|
+
algorithm_state=data.get("algorithm_state"),
|
|
330
|
+
problem_name=data.get("problem_name"),
|
|
331
|
+
algorithm_name=data.get("algorithm_name"),
|
|
332
|
+
start_time=data.get("start_time"),
|
|
333
|
+
end_time=data.get("end_time"),
|
|
334
|
+
elapsed_time=data.get("elapsed_time"),
|
|
335
|
+
device=data.get("device"),
|
|
336
|
+
extra=data.get("extra", {}),
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
# -------------------------------------------------------------------------
|
|
340
|
+
# String representation
|
|
341
|
+
# -------------------------------------------------------------------------
|
|
342
|
+
|
|
343
|
+
def __repr__(self) -> str:
|
|
344
|
+
return (
|
|
345
|
+
f"Result(\n"
|
|
346
|
+
f" best_fitness={self.best_fitness:.6g},\n"
|
|
347
|
+
f" n_var={self.n_var},\n"
|
|
348
|
+
f" n_evals={self.n_evals},\n"
|
|
349
|
+
f" n_gen={self.n_gen},\n"
|
|
350
|
+
f" success={self.success}\n"
|
|
351
|
+
f")"
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
def summary(self) -> str:
|
|
355
|
+
"""
|
|
356
|
+
Generate detailed summary string.
|
|
357
|
+
|
|
358
|
+
Returns:
|
|
359
|
+
Multi-line summary of optimisation results.
|
|
360
|
+
"""
|
|
361
|
+
lines = [
|
|
362
|
+
"=" * 60,
|
|
363
|
+
"OPTIMISATION RESULT",
|
|
364
|
+
"=" * 60,
|
|
365
|
+
]
|
|
366
|
+
|
|
367
|
+
# Problem/Algorithm info
|
|
368
|
+
if self.problem_name or self.algorithm_name:
|
|
369
|
+
lines.append("")
|
|
370
|
+
if self.problem_name:
|
|
371
|
+
lines.append(f"Problem: {self.problem_name}")
|
|
372
|
+
if self.algorithm_name:
|
|
373
|
+
lines.append(f"Algorithm: {self.algorithm_name}")
|
|
374
|
+
|
|
375
|
+
# Best result
|
|
376
|
+
lines.extend([
|
|
377
|
+
"",
|
|
378
|
+
"Best Solution:",
|
|
379
|
+
f" Fitness: {self.best_fitness:.10g}",
|
|
380
|
+
f" n_var: {self.n_var}",
|
|
381
|
+
])
|
|
382
|
+
|
|
383
|
+
# Show solution if small
|
|
384
|
+
if self.n_var <= 10:
|
|
385
|
+
sol_str = ", ".join(f"{x:.4g}" for x in self.best_solution.tolist())
|
|
386
|
+
lines.append(f" x: [{sol_str}]")
|
|
387
|
+
|
|
388
|
+
# Counts
|
|
389
|
+
lines.extend([
|
|
390
|
+
"",
|
|
391
|
+
"Statistics:",
|
|
392
|
+
f" Evaluations: {self.n_evals:,}",
|
|
393
|
+
f" Generations: {self.n_gen:,}",
|
|
394
|
+
])
|
|
395
|
+
|
|
396
|
+
if self.pop_size:
|
|
397
|
+
lines.append(f" Pop size: {self.pop_size}")
|
|
398
|
+
|
|
399
|
+
# Timing
|
|
400
|
+
if self.elapsed_time is not None:
|
|
401
|
+
lines.append(f" Time: {self.elapsed_time:.2f}s")
|
|
402
|
+
if self.n_evals > 0:
|
|
403
|
+
evals_per_sec = self.n_evals / self.elapsed_time
|
|
404
|
+
lines.append(f" Evals/sec: {evals_per_sec:,.0f}")
|
|
405
|
+
|
|
406
|
+
# Termination
|
|
407
|
+
lines.extend([
|
|
408
|
+
"",
|
|
409
|
+
"Termination:",
|
|
410
|
+
f" Success: {self.success}",
|
|
411
|
+
])
|
|
412
|
+
if self.termination_reason:
|
|
413
|
+
lines.append(f" Reason: {self.termination_reason}")
|
|
414
|
+
|
|
415
|
+
# Hyperparameters
|
|
416
|
+
if self.hyperparams:
|
|
417
|
+
lines.extend(["", "Final Hyperparameters:"])
|
|
418
|
+
for key, value in self.hyperparams.items():
|
|
419
|
+
if isinstance(value, float):
|
|
420
|
+
lines.append(f" {key}: {value:.6g}")
|
|
421
|
+
else:
|
|
422
|
+
lines.append(f" {key}: {value}")
|
|
423
|
+
|
|
424
|
+
# History
|
|
425
|
+
if self.history:
|
|
426
|
+
lines.extend(["", "History Keys:"])
|
|
427
|
+
for key, values in self.history.items():
|
|
428
|
+
lines.append(f" {key}: {len(values)} entries")
|
|
429
|
+
|
|
430
|
+
# Device
|
|
431
|
+
if self.device:
|
|
432
|
+
lines.extend(["", f"Device: {self.device}"])
|
|
433
|
+
|
|
434
|
+
lines.append("=" * 60)
|
|
435
|
+
|
|
436
|
+
return "\n".join(lines)
|
|
437
|
+
|
|
438
|
+
def print_summary(self) -> None:
|
|
439
|
+
"""Print detailed summary to stdout."""
|
|
440
|
+
print(self.summary())
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
# =============================================================================
|
|
444
|
+
# Result Builder (for internal use by minimize/maximize)
|
|
445
|
+
# =============================================================================
|
|
446
|
+
|
|
447
|
+
class ResultBuilder:
|
|
448
|
+
"""
|
|
449
|
+
Builder for constructing Result objects.
|
|
450
|
+
|
|
451
|
+
Used internally by minimize/maximize functions to accumulate
|
|
452
|
+
results during optimisation.
|
|
453
|
+
|
|
454
|
+
Example:
|
|
455
|
+
>>> builder = ResultBuilder()
|
|
456
|
+
>>> builder.set_problem(problem)
|
|
457
|
+
>>> builder.set_algorithm(algorithm)
|
|
458
|
+
>>> builder.start()
|
|
459
|
+
>>> # ... run optimisation ...
|
|
460
|
+
>>> builder.finish(algorithm, termination)
|
|
461
|
+
>>> result = builder.build()
|
|
462
|
+
"""
|
|
463
|
+
|
|
464
|
+
def __init__(self) -> None:
|
|
465
|
+
self._start_time: Optional[datetime] = None
|
|
466
|
+
self._end_time: Optional[datetime] = None
|
|
467
|
+
self._problem_name: Optional[str] = None
|
|
468
|
+
self._algorithm_name: Optional[str] = None
|
|
469
|
+
self._device: Optional[str] = None
|
|
470
|
+
self._history: Dict[str, List[Any]] = {}
|
|
471
|
+
self._extra: Dict[str, Any] = {}
|
|
472
|
+
|
|
473
|
+
def set_problem(self, problem: Any) -> "ResultBuilder":
|
|
474
|
+
"""Set problem information."""
|
|
475
|
+
self._problem_name = getattr(problem, "name", None)
|
|
476
|
+
return self
|
|
477
|
+
|
|
478
|
+
def set_algorithm(self, algorithm: Any) -> "ResultBuilder":
|
|
479
|
+
"""Set algorithm information."""
|
|
480
|
+
self._algorithm_name = algorithm.__class__.__name__
|
|
481
|
+
self._device = str(getattr(algorithm, "device", "unknown"))
|
|
482
|
+
return self
|
|
483
|
+
|
|
484
|
+
def start(self) -> "ResultBuilder":
|
|
485
|
+
"""Mark start of optimisation."""
|
|
486
|
+
self._start_time = datetime.now()
|
|
487
|
+
return self
|
|
488
|
+
|
|
489
|
+
def finish(
|
|
490
|
+
self,
|
|
491
|
+
algorithm: Any,
|
|
492
|
+
termination: Optional[Any] = None,
|
|
493
|
+
success: bool = False,
|
|
494
|
+
) -> "ResultBuilder":
|
|
495
|
+
"""
|
|
496
|
+
Mark end of optimisation and capture final state.
|
|
497
|
+
|
|
498
|
+
Args:
|
|
499
|
+
algorithm: The algorithm instance.
|
|
500
|
+
termination: The termination criterion.
|
|
501
|
+
success: Whether target was reached.
|
|
502
|
+
"""
|
|
503
|
+
self._end_time = datetime.now()
|
|
504
|
+
self._algorithm = algorithm
|
|
505
|
+
self._termination = termination
|
|
506
|
+
self._success = success
|
|
507
|
+
return self
|
|
508
|
+
|
|
509
|
+
def set_history(self, history: Dict[str, List[Any]]) -> "ResultBuilder":
|
|
510
|
+
"""Set convergence history."""
|
|
511
|
+
self._history = history
|
|
512
|
+
return self
|
|
513
|
+
|
|
514
|
+
def add_extra(self, key: str, value: Any) -> "ResultBuilder":
|
|
515
|
+
"""Add extra user data."""
|
|
516
|
+
self._extra[key] = value
|
|
517
|
+
return self
|
|
518
|
+
|
|
519
|
+
def build(self, include_state: bool = True) -> Result:
|
|
520
|
+
"""
|
|
521
|
+
Build the final Result object.
|
|
522
|
+
|
|
523
|
+
Args:
|
|
524
|
+
include_state: Whether to include full algorithm state.
|
|
525
|
+
|
|
526
|
+
Returns:
|
|
527
|
+
Constructed Result instance.
|
|
528
|
+
"""
|
|
529
|
+
algorithm = self._algorithm
|
|
530
|
+
termination = self._termination
|
|
531
|
+
|
|
532
|
+
# Calculate elapsed time
|
|
533
|
+
elapsed = None
|
|
534
|
+
if self._start_time and self._end_time:
|
|
535
|
+
elapsed = (self._end_time - self._start_time).total_seconds()
|
|
536
|
+
|
|
537
|
+
# Get termination reason
|
|
538
|
+
reason = None
|
|
539
|
+
if termination is not None:
|
|
540
|
+
reason = getattr(termination, "reason", None)
|
|
541
|
+
|
|
542
|
+
# Get hyperparameters
|
|
543
|
+
hyperparams = {}
|
|
544
|
+
if hasattr(algorithm, "_get_hyperparams"):
|
|
545
|
+
hyperparams = algorithm._get_hyperparams()
|
|
546
|
+
|
|
547
|
+
# Get algorithm state
|
|
548
|
+
state = None
|
|
549
|
+
if include_state and hasattr(algorithm, "state_dict"):
|
|
550
|
+
state = algorithm.state_dict()
|
|
551
|
+
|
|
552
|
+
return Result(
|
|
553
|
+
best_solution=algorithm.best_solution.clone(),
|
|
554
|
+
best_fitness=float(algorithm.best_fitness),
|
|
555
|
+
population=algorithm.population.clone() if algorithm.population is not None else None,
|
|
556
|
+
fitness=algorithm.fitness.clone() if algorithm.fitness is not None else None,
|
|
557
|
+
n_evals=algorithm.n_evals,
|
|
558
|
+
n_gen=algorithm.generation,
|
|
559
|
+
success=self._success,
|
|
560
|
+
termination_reason=reason,
|
|
561
|
+
history=self._history,
|
|
562
|
+
hyperparams=hyperparams,
|
|
563
|
+
algorithm_state=state,
|
|
564
|
+
problem_name=self._problem_name,
|
|
565
|
+
algorithm_name=self._algorithm_name,
|
|
566
|
+
start_time=self._start_time,
|
|
567
|
+
end_time=self._end_time,
|
|
568
|
+
elapsed_time=elapsed,
|
|
569
|
+
device=self._device,
|
|
570
|
+
extra=self._extra,
|
|
571
|
+
)
|