polystep 0.7.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 (52) hide show
  1. polystep/__init__.py +176 -0
  2. polystep/_compiled.py +334 -0
  3. polystep/_step_blockwise.py +994 -0
  4. polystep/_step_momentum.py +128 -0
  5. polystep/_step_monolithic.py +1070 -0
  6. polystep/adaptive_subspace.py +762 -0
  7. polystep/api.py +307 -0
  8. polystep/ask_tell.py +164 -0
  9. polystep/benchmarks/__init__.py +62 -0
  10. polystep/benchmarks/baselines.py +604 -0
  11. polystep/benchmarks/rl/__init__.py +13 -0
  12. polystep/benchmarks/rl/cartpole.py +260 -0
  13. polystep/benchmarks/rl/gym_evaluator.py +304 -0
  14. polystep/benchmarks/rl/metrics.py +53 -0
  15. polystep/benchmarks/rl/policies.py +133 -0
  16. polystep/benchmarks/utils.py +1262 -0
  17. polystep/blockwise.py +549 -0
  18. polystep/cma.py +400 -0
  19. polystep/cma_subspace.py +404 -0
  20. polystep/cost_nn.py +633 -0
  21. polystep/costs.py +86 -0
  22. polystep/dynamics.py +138 -0
  23. polystep/epsilon.py +217 -0
  24. polystep/geometry.py +410 -0
  25. polystep/hybrid_subspace.py +1335 -0
  26. polystep/layers/__init__.py +16 -0
  27. polystep/layers/attention.py +243 -0
  28. polystep/layers/rnn.py +276 -0
  29. polystep/objectives/__init__.py +25 -0
  30. polystep/objectives/base.py +77 -0
  31. polystep/objectives/synthetic.py +232 -0
  32. polystep/optimizer.py +1405 -0
  33. polystep/projection/__init__.py +5 -0
  34. polystep/projection/sparse.py +291 -0
  35. polystep/py.typed +0 -0
  36. polystep/quadratic_model.py +285 -0
  37. polystep/solver.py +654 -0
  38. polystep/solvers/__init__.py +34 -0
  39. polystep/solvers/_prelude.py +129 -0
  40. polystep/solvers/base.py +77 -0
  41. polystep/solvers/greedy.py +159 -0
  42. polystep/solvers/kl_softmax.py +200 -0
  43. polystep/solvers/sinkhorn.py +522 -0
  44. polystep/solvers/softmax.py +151 -0
  45. polystep/solvers/tempered_softmax.py +97 -0
  46. polystep/subspace.py +694 -0
  47. polystep/transform.py +365 -0
  48. polystep-0.7.0.dist-info/METADATA +285 -0
  49. polystep-0.7.0.dist-info/RECORD +52 -0
  50. polystep-0.7.0.dist-info/WHEEL +5 -0
  51. polystep-0.7.0.dist-info/licenses/LICENSE +191 -0
  52. polystep-0.7.0.dist-info/top_level.txt +1 -0
polystep/api.py ADDED
@@ -0,0 +1,307 @@
1
+ """High-level training API for polystep.
2
+
3
+ Provides a single-call ``train()`` function that wraps PolyStepOptimizer
4
+ with an epoch-based training loop, callback system, and diagnostics.
5
+ Users can train any ``nn.Module`` without manually managing closures,
6
+ epoch iteration, or batch handling.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass
12
+ from typing import Callable, List, Optional
13
+
14
+ import torch
15
+ import torch.nn as nn
16
+ from torch.utils.data import DataLoader
17
+
18
+ from .cost_nn import NNCostEvaluator
19
+ from .optimizer import PolyStepOptimizer
20
+
21
+
22
+ class TrainCallback:
23
+ """Base class for training callbacks.
24
+
25
+ Subclass and override ``on_step_end`` and/or ``on_epoch_end`` to
26
+ hook into the training loop. Return ``True`` from ``on_step_end``
27
+ to signal early stopping.
28
+
29
+ Example::
30
+
31
+ class LossThresholdCallback(TrainCallback):
32
+ def __init__(self, threshold: float = 0.01):
33
+ self.threshold = threshold
34
+
35
+ def on_step_end(self, metrics: dict) -> bool:
36
+ if metrics['loss'] < self.threshold:
37
+ print(f"Target loss reached at step {metrics['step']}")
38
+ return True # stop training
39
+ return False
40
+ """
41
+
42
+ def on_step_end(self, metrics: dict) -> bool:
43
+ """Called after each optimizer step.
44
+
45
+ Args:
46
+ metrics: Dict with keys 'step', 'epoch', 'loss', 'ot_cost',
47
+ 'displacement', 'velocity_mag', 'converged'.
48
+
49
+ Returns:
50
+ True to stop training, False to continue.
51
+ """
52
+ return False
53
+
54
+ def on_epoch_end(self, metrics: dict) -> None:
55
+ """Called after each epoch completes.
56
+
57
+ Args:
58
+ metrics: Dict with keys 'epoch', 'avg_loss'.
59
+ """
60
+ pass
61
+
62
+
63
+ @dataclass
64
+ class TrainConfig:
65
+ """Configuration for the ``train()`` function.
66
+
67
+ Training loop parameters only. Optimizer-specific parameters (epsilon,
68
+ step_radius, subspace, block_strategy, etc.) are set on
69
+ ``PolyStepOptimizer``.
70
+
71
+ Args:
72
+ epochs: Number of training epochs. Must be > 0.
73
+ batch_size: Convenience/documentation field. ``train()`` uses
74
+ the provided DataLoader directly and does NOT build one
75
+ from this value.
76
+ log_every: Step interval for built-in logging. Must be > 0.
77
+ callbacks: List of ``TrainCallback`` instances. ``None`` is
78
+ normalized to an empty list.
79
+ """
80
+
81
+ epochs: int = 10
82
+ batch_size: int = 32
83
+ log_every: int = 10
84
+ callbacks: Optional[List[TrainCallback]] = None
85
+
86
+ def __post_init__(self):
87
+ if self.epochs <= 0:
88
+ raise ValueError(f"epochs must be > 0, got {self.epochs}")
89
+ if self.batch_size <= 0:
90
+ raise ValueError(f"batch_size must be > 0, got {self.batch_size}")
91
+ if self.log_every <= 0:
92
+ raise ValueError(f"log_every must be > 0, got {self.log_every}")
93
+ if self.callbacks is None:
94
+ self.callbacks = []
95
+
96
+
97
+ def train(
98
+ model: nn.Module,
99
+ dataloader: DataLoader,
100
+ loss_fn: Callable,
101
+ optimizer: PolyStepOptimizer,
102
+ config: TrainConfig,
103
+ ) -> nn.Module:
104
+ """Train a model using the Sinkhorn Step optimizer.
105
+
106
+ Builds the OT closure internally from the model, loss function, and
107
+ each mini-batch. Users never handle closures directly. For each batch,
108
+ the function constructs a closure that evaluates the model at multiple
109
+ candidate parameter configurations via ``NNCostEvaluator``, then calls
110
+ ``optimizer.step(closure)`` to update the model weights.
111
+
112
+ Example::
113
+
114
+ from polystep import PolyStepOptimizer, train, TrainConfig, LoggingCallback
115
+
116
+ optimizer = PolyStepOptimizer(model, epsilon=0.1)
117
+ config = TrainConfig(epochs=5, callbacks=[LoggingCallback()])
118
+ trained_model = train(model, dataloader, loss_fn, optimizer, config)
119
+
120
+ See Also:
121
+ ``PolyStepOptimizer`` for optimizer configuration (epsilon,
122
+ step_radius, subspace, block_strategy, etc.).
123
+
124
+ Args:
125
+ model: The ``nn.Module`` to train. Updated in-place.
126
+ dataloader: PyTorch DataLoader yielding ``(inputs, targets)`` batches.
127
+ loss_fn: Loss function with signature ``loss_fn(output, targets) -> scalar``.
128
+ optimizer: A ``PolyStepOptimizer`` instance (already configured).
129
+ config: ``TrainConfig`` with loop hyperparameters and callbacks.
130
+
131
+ Returns:
132
+ The same model object (mutated in-place by the optimizer).
133
+ """
134
+ evaluator = NNCostEvaluator(
135
+ model,
136
+ loss_fn=loss_fn,
137
+ compile_vmap=getattr(optimizer, "_compile_evaluator", False),
138
+ )
139
+ callbacks = list(config.callbacks)
140
+ global_step = 0
141
+ stop = False
142
+
143
+ # Detect model device for automatic batch transfer
144
+ try:
145
+ device = next(model.parameters()).device
146
+ except StopIteration:
147
+ raise ValueError("Model has no trainable parameters")
148
+
149
+ for epoch in range(config.epochs):
150
+ epoch_loss_sum = 0.0
151
+ epoch_loss_count = 0
152
+
153
+ for batch in dataloader:
154
+ inputs, targets = batch
155
+ inputs, targets = inputs.to(device), targets.to(device)
156
+
157
+ # Micro-batch: subsample for cost evaluation if configured.
158
+ cost_bs = getattr(optimizer, "cost_batch_size", None)
159
+ if cost_bs is not None and cost_bs < inputs.shape[0]:
160
+ gen = getattr(optimizer, "_generator", None)
161
+ if gen is not None and gen.device.type == inputs.device.type:
162
+ idx = torch.randperm(inputs.shape[0], device=inputs.device, generator=gen)[:cost_bs]
163
+ else:
164
+ idx = torch.randperm(inputs.shape[0], device=inputs.device)[:cost_bs]
165
+ cost_inputs = inputs[idx]
166
+ cost_targets = targets[idx]
167
+ else:
168
+ cost_inputs = inputs
169
+ cost_targets = targets
170
+
171
+ # Build closure for this batch using default-argument capture
172
+ def closure(batched_params, _in=cost_inputs, _tgt=cost_targets):
173
+ return evaluator.evaluate(batched_params, _in, _tgt)
174
+
175
+ optimizer.step(closure)
176
+
177
+ # Compute training loss separately (not the OT cost)
178
+ with torch.no_grad():
179
+ output = model(inputs)
180
+ train_loss = loss_fn(output, targets).item()
181
+
182
+ epoch_loss_sum += train_loss
183
+ epoch_loss_count += 1
184
+
185
+ # Build metrics dict
186
+ state = optimizer.state
187
+ metrics = {
188
+ "step": global_step,
189
+ "epoch": epoch,
190
+ "loss": train_loss,
191
+ "ot_cost": state.costs[-1] if state.costs else 0.0,
192
+ "displacement": (state.displacement_sqnorms[-1] if state.displacement_sqnorms else 0.0),
193
+ "velocity_mag": (torch.norm(state.velocity).item() if state.velocity is not None else 0.0),
194
+ "converged": (state.linear_convergence[-1] if state.linear_convergence else False),
195
+ "absorb_count": getattr(state, "absorb_count", 0),
196
+ }
197
+
198
+ # Invoke on_step_end callbacks
199
+ for cb in callbacks:
200
+ if cb.on_step_end(metrics):
201
+ stop = True
202
+ break
203
+
204
+ if stop:
205
+ break
206
+
207
+ global_step += 1
208
+
209
+ if stop:
210
+ break
211
+
212
+ # Epoch-level metrics
213
+ avg_loss = epoch_loss_sum / epoch_loss_count if epoch_loss_count > 0 else 0.0
214
+ epoch_metrics = {
215
+ "epoch": epoch,
216
+ "avg_loss": avg_loss,
217
+ }
218
+ for cb in callbacks:
219
+ cb.on_epoch_end(epoch_metrics)
220
+
221
+ return model
222
+
223
+
224
+ # ---------------------------------------------------------------------------
225
+ # Built-in callbacks
226
+ # ---------------------------------------------------------------------------
227
+
228
+
229
+ class LoggingCallback(TrainCallback):
230
+ """Prints step metrics at a configurable interval.
231
+
232
+ Args:
233
+ log_every: Print every *log_every* steps (default 10).
234
+ """
235
+
236
+ def __init__(self, log_every: int = 10):
237
+ self.log_every = log_every
238
+
239
+ def on_step_end(self, metrics: dict) -> bool:
240
+ if metrics["step"] % self.log_every == 0:
241
+ print(
242
+ f"[Step {metrics['step']}] "
243
+ f"loss={metrics['loss']:.4f} "
244
+ f"ot_cost={metrics['ot_cost']:.4f} "
245
+ f"disp={metrics['displacement']:.6f} "
246
+ f"converged={metrics['converged']}"
247
+ )
248
+ return False
249
+
250
+ def on_epoch_end(self, metrics: dict) -> None:
251
+ print(f"--- Epoch {metrics['epoch']} complete | avg_loss={metrics['avg_loss']:.4f} ---")
252
+
253
+
254
+ class EarlyStoppingCallback(TrainCallback):
255
+ """Stops training when loss stagnates for *patience* steps.
256
+
257
+ Args:
258
+ patience: Number of steps without improvement before stopping.
259
+ min_delta: Minimum loss decrease to count as improvement.
260
+ """
261
+
262
+ def __init__(self, patience: int = 10, min_delta: float = 1e-4):
263
+ self.patience = patience
264
+ self.min_delta = min_delta
265
+ self.best_loss: float = float("inf")
266
+ self.counter: int = 0
267
+
268
+ def on_step_end(self, metrics: dict) -> bool:
269
+ loss = metrics["loss"]
270
+ if loss < self.best_loss - self.min_delta:
271
+ self.best_loss = loss
272
+ self.counter = 0
273
+ else:
274
+ self.counter += 1
275
+ if self.counter >= self.patience:
276
+ print(f"Early stopping at step {metrics['step']}")
277
+ return True
278
+ return False
279
+
280
+
281
+ # ---------------------------------------------------------------------------
282
+ # Diagnostics helper
283
+ # ---------------------------------------------------------------------------
284
+
285
+
286
+ def get_diagnostics(optimizer: PolyStepOptimizer) -> dict:
287
+ """Extract diagnostic summary from optimizer state.
288
+
289
+ Args:
290
+ optimizer: A ``PolyStepOptimizer`` instance (may have been stepped).
291
+
292
+ Returns:
293
+ Dict with keys: 'costs', 'displacement_sqnorms', 'convergence',
294
+ 'velocity_magnitude', 'iteration_count', 'epsilon', 'radius_multiplier',
295
+ 'absorb_count'.
296
+ """
297
+ state = optimizer.state
298
+ return {
299
+ "costs": list(state.costs),
300
+ "displacement_sqnorms": list(state.displacement_sqnorms),
301
+ "convergence": list(state.linear_convergence),
302
+ "velocity_magnitude": (torch.norm(state.velocity).item() if state.velocity is not None else None),
303
+ "iteration_count": state.iteration_count,
304
+ "epsilon": state.epsilon,
305
+ "radius_multiplier": state.radius_multiplier,
306
+ "absorb_count": getattr(state, "absorb_count", 0),
307
+ }
polystep/ask_tell.py ADDED
@@ -0,0 +1,164 @@
1
+ """PolyStep as a standard ask/tell optimizer.
2
+
3
+ Exposes the Sinkhorn Step update behind the ``ask``/``tell`` interface used by
4
+ evolution-strategy libraries (evosax, NeuroEvoBench, CMA-ES). ``ask`` returns
5
+ candidate points (rotated polytope vertices around each particle) for the caller
6
+ to evaluate; ``tell`` takes their fitness (lower is better), builds the cost
7
+ matrix, and applies one softmax-weighted barycentric step. PolyStep is then a
8
+ drop-in gradient-free optimizer comparable to any ES.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import math
14
+ import warnings
15
+ from typing import Callable, Optional, Union
16
+
17
+ import torch
18
+
19
+ from ._compiled import _barycentric_projection, _rotate_and_translate
20
+ from .geometry import get_orthoplex_vertices, get_random_rotation_matrices
21
+ from .solvers import SinkhornSolver, SoftmaxSolver
22
+
23
+ __all__ = ["PolyStepES", "minimize"]
24
+
25
+
26
+ class PolyStepES:
27
+ """Ask/tell wrapper around the Sinkhorn Step update.
28
+
29
+ Args:
30
+ dim: Dimensionality of a solution vector.
31
+ num_particles: Number of independent particles (each proposes a
32
+ polytope of candidates). Population size is ``num_particles * 2 * dim``.
33
+ epsilon: Entropic OT temperature for the softmax weighting.
34
+ step_radius: Geometric step size along the polytope directions.
35
+ solver: OT solver instance. Defaults to :class:`SoftmaxSolver`; pass a
36
+ :class:`SinkhornSolver` for the full entropic-OT plan.
37
+ scale_cost: Cost-matrix scaling passed to the solver ("mean", "max_cost",
38
+ a float divisor, or None).
39
+ x0: Initial position(s), shape ``(dim,)`` or ``(num_particles, dim)``.
40
+ seed: Seed for the rotation generator.
41
+ device, dtype: Tensor device and dtype.
42
+ """
43
+
44
+ def __init__(
45
+ self,
46
+ dim: int,
47
+ num_particles: int = 1,
48
+ epsilon: float = 0.5,
49
+ step_radius: float = 0.5,
50
+ solver: Optional[Union[SoftmaxSolver, SinkhornSolver]] = None,
51
+ scale_cost: Optional[Union[str, float]] = "mean",
52
+ x0: Optional[torch.Tensor] = None,
53
+ seed: Optional[int] = None,
54
+ device: Union[str, torch.device] = "cpu",
55
+ dtype: torch.dtype = torch.float32,
56
+ ):
57
+ if dim < 1:
58
+ raise ValueError(f"dim must be >= 1, got {dim}.")
59
+ if num_particles < 1:
60
+ raise ValueError(f"num_particles must be >= 1, got {num_particles}.")
61
+ if not epsilon > 0:
62
+ raise ValueError(f"epsilon must be > 0, got {epsilon}.")
63
+ if not (math.isfinite(step_radius) and step_radius >= 0):
64
+ raise ValueError(f"step_radius must be finite and >= 0, got {step_radius}.")
65
+ self.dim = dim
66
+ self.num_particles = num_particles
67
+ self.epsilon = epsilon
68
+ self.step_radius = step_radius
69
+ self.scale_cost = scale_cost
70
+ self.device = torch.device(device)
71
+ self.dtype = dtype
72
+ self.solver = solver if solver is not None else SoftmaxSolver(epsilon=epsilon)
73
+ if isinstance(self.solver, SinkhornSolver) and num_particles == 1:
74
+ warnings.warn(
75
+ "SinkhornSolver with num_particles=1 yields a uniform transport plan "
76
+ "(the column marginal forces it), so steps ignore fitness. Use the "
77
+ "default SoftmaxSolver, or num_particles > 1.",
78
+ stacklevel=2,
79
+ )
80
+
81
+ self.generator = torch.Generator(device=self.device)
82
+ if seed is not None:
83
+ self.generator.manual_seed(seed)
84
+
85
+ self.vertices = get_orthoplex_vertices(dim, device=self.device, dtype=dtype) # (2d, d)
86
+ self.num_vertices = self.vertices.shape[0]
87
+
88
+ if x0 is None:
89
+ X = torch.zeros(num_particles, dim, device=self.device, dtype=dtype)
90
+ else:
91
+ X = torch.as_tensor(x0, device=self.device, dtype=dtype).reshape(-1, dim)
92
+ if X.shape[0] == 1 and num_particles > 1:
93
+ X = X.expand(num_particles, dim).clone()
94
+ if X.shape[0] != num_particles:
95
+ raise ValueError(f"x0 has {X.shape[0]} particle rows but num_particles={num_particles}.")
96
+ self.X = X.clone()
97
+ self.a = torch.full((num_particles,), 1.0 / num_particles, device=self.device, dtype=dtype)
98
+
99
+ self._pending: Optional[torch.Tensor] = None
100
+ self.best_solution: Optional[torch.Tensor] = None
101
+ self.best_fitness: float = float("inf")
102
+
103
+ @property
104
+ def popsize(self) -> int:
105
+ return self.num_particles * self.num_vertices
106
+
107
+ @property
108
+ def mean(self) -> torch.Tensor:
109
+ """Mean particle position (the current solution estimate)."""
110
+ return self.X.mean(dim=0)
111
+
112
+ @torch.inference_mode()
113
+ def ask(self) -> torch.Tensor:
114
+ """Return candidate points of shape ``(popsize, dim)`` to evaluate."""
115
+ if self._pending is not None:
116
+ raise RuntimeError("ask() called twice before tell(); tell() the previous population first.")
117
+ rot = get_random_rotation_matrices(
118
+ self.num_particles, self.dim, device=self.device, dtype=self.dtype, generator=self.generator
119
+ )
120
+ X_vertices, _ = _rotate_and_translate(rot, self.vertices, self.X, self.step_radius) # (P, V, d)
121
+ self._pending = X_vertices
122
+ return X_vertices.reshape(self.popsize, self.dim)
123
+
124
+ @torch.inference_mode()
125
+ def tell(self, fitness: torch.Tensor) -> None:
126
+ """Update particles from the fitness of the last ``ask`` (lower is better)."""
127
+ if self._pending is None:
128
+ raise RuntimeError("tell() called before ask()")
129
+ cost = torch.as_tensor(fitness, device=self.device, dtype=self.dtype).reshape(
130
+ self.num_particles, self.num_vertices
131
+ )
132
+ self.solver.epsilon = self.epsilon
133
+ # Source marginal defaults to uniform 1/P inside the solver, which is
134
+ # exactly ``self.a`` -- pass None so the solver skips the (host-syncing)
135
+ # user-marginal validation on this per-step path.
136
+ transport = self.solver.solve(cost, scale_cost=self.scale_cost).matrix
137
+ X_new = _barycentric_projection(transport, self.a, self._pending)
138
+ if torch.isfinite(X_new).all():
139
+ self.X = X_new
140
+
141
+ flat_cost = cost.reshape(-1)
142
+ fmin, idx = torch.min(flat_cost, dim=0)
143
+ if fmin.item() < self.best_fitness:
144
+ self.best_fitness = fmin.item()
145
+ self.best_solution = self._pending.reshape(self.popsize, self.dim)[idx].clone()
146
+ self._pending = None
147
+
148
+
149
+ def minimize(
150
+ fn: Callable[[torch.Tensor], torch.Tensor],
151
+ dim: int,
152
+ steps: int = 200,
153
+ **kwargs,
154
+ ) -> PolyStepES:
155
+ """Minimize a batched black-box ``fn: (popsize, dim) -> (popsize,)``.
156
+
157
+ Runs ``steps`` ask/tell rounds and returns the optimizer, whose
158
+ ``best_solution`` / ``best_fitness`` / ``mean`` hold the result.
159
+ """
160
+ es = PolyStepES(dim, **kwargs)
161
+ for _ in range(steps):
162
+ candidates = es.ask()
163
+ es.tell(fn(candidates))
164
+ return es
@@ -0,0 +1,62 @@
1
+ """Benchmark utilities for polystep experiments.
2
+
3
+ Provides shared model factories, data loaders, baselines, and evaluation
4
+ helpers used by the experiment runners and examples.
5
+ """
6
+
7
+ from .utils import (
8
+ SEEDS,
9
+ get_mnist_loaders,
10
+ get_cifar10_loaders,
11
+ get_sst2_loaders,
12
+ get_nmnist_loaders,
13
+ get_dvs_gesture_loaders,
14
+ MNISTNet,
15
+ CIFAR10Net,
16
+ create_model,
17
+ LIFNeuron,
18
+ SpikingNet,
19
+ evaluate_accuracy,
20
+ evaluate_snn_accuracy,
21
+ BenchmarkResult,
22
+ get_environment_info,
23
+ save_results_json,
24
+ format_results_table,
25
+ compute_summary_stats,
26
+ )
27
+
28
+ from .baselines import (
29
+ has_evotorch,
30
+ has_nevergrad,
31
+ train_cmaes,
32
+ train_nevergrad,
33
+ check_gradient_free_deps,
34
+ get_available_optimizers,
35
+ )
36
+
37
+ __all__ = [
38
+ "SEEDS",
39
+ "get_mnist_loaders",
40
+ "get_cifar10_loaders",
41
+ "get_sst2_loaders",
42
+ "get_nmnist_loaders",
43
+ "get_dvs_gesture_loaders",
44
+ "MNISTNet",
45
+ "CIFAR10Net",
46
+ "create_model",
47
+ "LIFNeuron",
48
+ "SpikingNet",
49
+ "evaluate_accuracy",
50
+ "evaluate_snn_accuracy",
51
+ "BenchmarkResult",
52
+ "get_environment_info",
53
+ "save_results_json",
54
+ "format_results_table",
55
+ "compute_summary_stats",
56
+ "has_evotorch",
57
+ "has_nevergrad",
58
+ "train_cmaes",
59
+ "train_nevergrad",
60
+ "check_gradient_free_deps",
61
+ "get_available_optimizers",
62
+ ]