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.
Files changed (50) hide show
  1. evograd/__init__.py +67 -0
  2. evograd/algorithms/__init__.py +138 -0
  3. evograd/algorithms/cmaes.py +1365 -0
  4. evograd/algorithms/de.py +895 -0
  5. evograd/algorithms/ga.py +532 -0
  6. evograd/algorithms/pso.py +648 -0
  7. evograd/algorithms/shade.py +1165 -0
  8. evograd/benchmarks/functions/__init__.py +229 -0
  9. evograd/benchmarks/functions/base.py +217 -0
  10. evograd/benchmarks/functions/cec2017/__init__.py +250 -0
  11. evograd/benchmarks/functions/cec2017/basic.py +413 -0
  12. evograd/benchmarks/functions/cec2017/composition.py +580 -0
  13. evograd/benchmarks/functions/cec2017/data.pkl +0 -0
  14. evograd/benchmarks/functions/cec2017/data.py +350 -0
  15. evograd/benchmarks/functions/cec2017/hybrid.py +406 -0
  16. evograd/benchmarks/functions/cec2017/simple.py +326 -0
  17. evograd/benchmarks/functions/classical.py +649 -0
  18. evograd/benchmarks/functions/smoothed_funnel.py +476 -0
  19. evograd/benchmarks/functions/transforms.py +463 -0
  20. evograd/benchmarks/run_benchmark_functions.py +1208 -0
  21. evograd/core/__init__.py +73 -0
  22. evograd/core/algorithm.py +778 -0
  23. evograd/core/maximize.py +269 -0
  24. evograd/core/minimize.py +740 -0
  25. evograd/core/problem.py +444 -0
  26. evograd/core/result.py +571 -0
  27. evograd/core/termination.py +602 -0
  28. evograd/operators/__init__.py +178 -0
  29. evograd/operators/crossover.py +1117 -0
  30. evograd/operators/mutation.py +1098 -0
  31. evograd/operators/relaxations.py +175 -0
  32. evograd/operators/repair.py +601 -0
  33. evograd/operators/sampling.py +577 -0
  34. evograd/operators/selection.py +981 -0
  35. evograd/operators/survival.py +1000 -0
  36. evograd/tests/__init__.py +11 -0
  37. evograd/tests/run_all.py +78 -0
  38. evograd/tests/test_core.py +528 -0
  39. evograd/tests/test_ga.py +572 -0
  40. evograd/tests/test_operators.py +662 -0
  41. evograd/tests/test_per_individual.py +326 -0
  42. evograd/tests/test_utils.py +328 -0
  43. evograd/utils/__init__.py +97 -0
  44. evograd/utils/callbacks.py +926 -0
  45. evograd/utils/device.py +502 -0
  46. evograd/utils/duplicates.py +421 -0
  47. evograd_diff-0.1.0.dist-info/METADATA +439 -0
  48. evograd_diff-0.1.0.dist-info/RECORD +50 -0
  49. evograd_diff-0.1.0.dist-info/WHEEL +4 -0
  50. 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
+ )