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
@@ -0,0 +1,604 @@
1
+ """EvoTorch CMA-ES and Nevergrad baseline wrappers for benchmarks.
2
+
3
+ Shared implementations used across all benchmark comparison scripts.
4
+
5
+ EvoTorch CMA-ES:
6
+ - Uses separable=True for models >10K params (O(n) vs O(n^2) memory)
7
+ - GPU-accelerated when available
8
+ - Reports function_evals = generations * popsize
9
+
10
+ Nevergrad ES:
11
+ - Uses OnePlusOne (1+1)-ES optimizer
12
+ - Simple and effective for neural network optimization
13
+ - Reports function_evals = budget
14
+
15
+ Usage:
16
+ from polystep.benchmarks.baselines import train_cmaes, train_nevergrad
17
+
18
+ # CMA-ES training
19
+ result = train_cmaes(model, train_data, train_labels, test_data, test_labels)
20
+
21
+ # Nevergrad training
22
+ result = train_nevergrad(model, train_data, train_labels, test_data, test_labels)
23
+
24
+ Installation:
25
+ pip install evotorch nevergrad
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import time
31
+ from typing import Callable, List, Optional, Tuple
32
+
33
+ import numpy as np
34
+ import torch
35
+ import torch.nn as nn
36
+ from torch import Tensor
37
+
38
+ from .utils import BenchmarkResult
39
+
40
+
41
+ # ---------------------------------------------------------------------------
42
+ # Check for optional dependencies
43
+ # ---------------------------------------------------------------------------
44
+
45
+ _HAS_EVOTORCH = False
46
+ _EVOTORCH_ERROR = None
47
+ try:
48
+ from evotorch import Problem
49
+ from evotorch.algorithms import CMAES
50
+
51
+ _HAS_EVOTORCH = True
52
+ except ImportError as e:
53
+ _EVOTORCH_ERROR = str(e)
54
+
55
+
56
+ _HAS_NEVERGRAD = False
57
+ _NEVERGRAD_ERROR = None
58
+ try:
59
+ import nevergrad as ng
60
+
61
+ _HAS_NEVERGRAD = True
62
+ except ImportError as e:
63
+ _NEVERGRAD_ERROR = str(e)
64
+
65
+
66
+ def has_evotorch() -> bool:
67
+ """Check if EvoTorch is available."""
68
+ return _HAS_EVOTORCH
69
+
70
+
71
+ def has_nevergrad() -> bool:
72
+ """Check if Nevergrad is available."""
73
+ return _HAS_NEVERGRAD
74
+
75
+
76
+ # ---------------------------------------------------------------------------
77
+ # EvoTorch CMA-ES Implementation
78
+ # ---------------------------------------------------------------------------
79
+
80
+ if _HAS_EVOTORCH:
81
+
82
+ class NNOptimizationProblem(Problem):
83
+ """EvoTorch Problem wrapper for neural network parameter optimization.
84
+
85
+ This wraps a PyTorch model for use with EvoTorch's CMA-ES optimizer.
86
+ The fitness function evaluates classification accuracy on a batch of data.
87
+
88
+ Adapted from CMA-ES baseline.
89
+
90
+ Args:
91
+ model: PyTorch model to optimize.
92
+ train_data: Training data tensor, shape (N, ...).
93
+ train_labels: Training labels tensor, shape (N,).
94
+ batch_size: Batch size for fitness evaluation.
95
+ device: Device for computation ('cpu' or 'cuda').
96
+ fitness_fn: Optional custom fitness function. If None, uses accuracy.
97
+
98
+ Example:
99
+ >>> model = MNISTNet()
100
+ >>> problem = NNOptimizationProblem(model, X_train, y_train)
101
+ >>> searcher = CMAES(problem, popsize=16, stdev_init=0.5)
102
+ >>> searcher.step()
103
+ """
104
+
105
+ def __init__(
106
+ self,
107
+ model: nn.Module,
108
+ train_data: Tensor,
109
+ train_labels: Tensor,
110
+ batch_size: int = 512,
111
+ device: str = "cpu",
112
+ fitness_fn: Optional[Callable[[nn.Module, Tensor, Tensor], float]] = None,
113
+ ):
114
+ self._model = model
115
+ self._param_count = sum(p.numel() for p in model.parameters())
116
+ self._param_shapes = [p.shape for p in model.parameters()]
117
+
118
+ # Store training data on device
119
+ self._train_data = train_data.to(device)
120
+ self._train_labels = train_labels.to(device)
121
+ self._batch_size = batch_size
122
+ self._fitness_fn = fitness_fn
123
+ self._device_str = device
124
+
125
+ super().__init__(
126
+ objective_sense="max", # Maximize accuracy
127
+ solution_length=self._param_count,
128
+ dtype=torch.float32,
129
+ device=device,
130
+ initial_bounds=(-1.0, 1.0),
131
+ )
132
+
133
+ def _load_weights(self, model: nn.Module, solution: Tensor) -> None:
134
+ """Load flattened solution vector into model parameters."""
135
+ offset = 0
136
+ with torch.no_grad():
137
+ for p in model.parameters():
138
+ numel = p.numel()
139
+ p.data.copy_(solution[offset : offset + numel].view(p.shape))
140
+ offset += numel
141
+
142
+ def _evaluate(self, solution) -> None:
143
+ """Evaluate a single solution (parameter vector).
144
+
145
+ EvoTorch calls this with a Solution object. We must SET the
146
+ evaluation on the solution, not return a value.
147
+
148
+ Args:
149
+ solution: EvoTorch Solution object containing parameter values.
150
+ """
151
+ values = solution.values
152
+ device = values.device
153
+
154
+ batch_size = min(self._batch_size, len(self._train_data))
155
+
156
+ # Sample a random batch for fitness evaluation
157
+ indices = torch.randperm(len(self._train_data), device=device)[:batch_size]
158
+ batch_data = self._train_data[indices]
159
+ batch_labels = self._train_labels[indices]
160
+
161
+ # Create model on correct device for evaluation
162
+ # Clone model architecture (create fresh instance)
163
+ model = type(self._model)(*self._get_model_init_args()).to(device)
164
+ model.eval()
165
+
166
+ self._load_weights(model, values)
167
+
168
+ if self._fitness_fn is not None:
169
+ fitness = self._fitness_fn(model, batch_data, batch_labels)
170
+ else:
171
+ # Default: accuracy
172
+ with torch.no_grad():
173
+ outputs = model(batch_data)
174
+ preds = outputs.argmax(dim=-1)
175
+ fitness = (preds == batch_labels).float().mean().item()
176
+
177
+ solution.set_evaluation(fitness)
178
+
179
+ def _get_model_init_args(self):
180
+ """Get initialization arguments for model cloning.
181
+
182
+ Override this for models with custom initialization.
183
+ Returns empty tuple for default (no-arg) constructors.
184
+ """
185
+ return ()
186
+
187
+
188
+ def train_cmaes(
189
+ model: nn.Module,
190
+ train_data: Tensor,
191
+ train_labels: Tensor,
192
+ test_data: Tensor,
193
+ test_labels: Tensor,
194
+ generations: int = 200,
195
+ popsize: int = 16,
196
+ stdev_init: float = 0.5,
197
+ batch_size: int = 512,
198
+ device: str = "cuda",
199
+ log_interval: int = 10,
200
+ verbose: bool = True,
201
+ model_init_args: tuple = (),
202
+ ) -> BenchmarkResult:
203
+ """Train model using EvoTorch CMA-ES.
204
+
205
+ Uses separable=True for models >10K params (O(n) memory instead of O(n^2)).
206
+ Reports function_evals = generations * popsize.
207
+
208
+ Args:
209
+ model: PyTorch model to optimize.
210
+ train_data: Training data tensor, shape (N, ...).
211
+ train_labels: Training labels tensor, shape (N,).
212
+ test_data: Test data tensor, shape (M, ...).
213
+ test_labels: Test labels tensor, shape (M,).
214
+ generations: Number of CMA-ES generations.
215
+ popsize: Population size.
216
+ stdev_init: Initial standard deviation.
217
+ batch_size: Batch size for fitness evaluation.
218
+ device: Device for computation.
219
+ log_interval: Print stats every N generations.
220
+ verbose: Print progress.
221
+ model_init_args: Arguments for model constructor (for cloning).
222
+
223
+ Returns:
224
+ BenchmarkResult with training metrics.
225
+
226
+ Raises:
227
+ ImportError: If EvoTorch is not installed.
228
+ """
229
+ if not _HAS_EVOTORCH:
230
+ raise ImportError(
231
+ f"EvoTorch is required for CMA-ES baseline. Install with: pip install evotorch\n"
232
+ f"Import error: {_EVOTORCH_ERROR}"
233
+ )
234
+
235
+ # Move model to device
236
+ model = model.to(device)
237
+ param_count = sum(p.numel() for p in model.parameters())
238
+
239
+ # Reset GPU memory stats
240
+ if device == "cuda" and torch.cuda.is_available():
241
+ torch.cuda.reset_peak_memory_stats()
242
+
243
+ # Create Problem with model cloning support
244
+ class ModelProblem(NNOptimizationProblem):
245
+ def _get_model_init_args(self):
246
+ return model_init_args
247
+
248
+ problem = ModelProblem(
249
+ model=model,
250
+ train_data=train_data,
251
+ train_labels=train_labels,
252
+ batch_size=batch_size,
253
+ device=device,
254
+ )
255
+
256
+ # Use separable CMA-ES for large models (O(n) vs O(n^2) memory)
257
+ use_separable = param_count > 10000
258
+ if verbose and use_separable:
259
+ print(f" Using separable (diagonal) CMA-ES for {param_count:,} params")
260
+
261
+ searcher = CMAES(
262
+ problem,
263
+ popsize=popsize,
264
+ stdev_init=stdev_init,
265
+ separable=use_separable,
266
+ )
267
+
268
+ # Convert test data to tensors on device
269
+ test_data_t = test_data.to(device)
270
+ test_labels_t = test_labels.to(device)
271
+
272
+ # Training loop
273
+ epoch_logs = []
274
+ best_test_acc = 0.0
275
+ best_solution = None
276
+ start_time = time.time()
277
+
278
+ # Build the eval model once and reuse across calls; rebuilding it on
279
+ # every log_interval was a hot-path allocation under the original
280
+ # benchmark configuration (generations=200, log_interval=10 ⇒ 21 rebuilds).
281
+ eval_model = type(model)(*model_init_args).to(device)
282
+ eval_model.eval()
283
+
284
+ def evaluate_on_test(solution) -> float:
285
+ """Evaluate ``solution`` on the full test set."""
286
+ if solution is None:
287
+ return 0.0
288
+ values = solution.values if hasattr(solution, "values") else solution
289
+
290
+ # Load weights into the reused model.
291
+ offset = 0
292
+ with torch.no_grad():
293
+ for p in eval_model.parameters():
294
+ numel = p.numel()
295
+ p.data.copy_(values[offset : offset + numel].view(p.shape))
296
+ offset += numel
297
+
298
+ # Evaluate in batches
299
+ correct = 0
300
+ total = 0
301
+ eval_batch_size = 512
302
+
303
+ with torch.no_grad():
304
+ for i in range(0, len(test_data_t), eval_batch_size):
305
+ batch_data = test_data_t[i : i + eval_batch_size]
306
+ batch_labels = test_labels_t[i : i + eval_batch_size]
307
+ outputs = eval_model(batch_data)
308
+ preds = outputs.argmax(dim=-1)
309
+ correct += (preds == batch_labels).sum().item()
310
+ total += len(batch_labels)
311
+
312
+ return correct / total if total > 0 else 0.0
313
+
314
+ for gen in range(generations):
315
+ searcher.step()
316
+
317
+ # Get stats from status
318
+ status = searcher.status
319
+ pop_best_fitness = float(status.get("pop_best_eval", 0.0))
320
+ mean_fitness = float(status.get("mean_eval", 0.0))
321
+ sigma = float(status.get("stepsize", stdev_init))
322
+
323
+ # Evaluate on test set periodically
324
+ if (gen + 1) % log_interval == 0 or gen == generations - 1:
325
+ pop_best_sol = status.get("pop_best", None)
326
+ if pop_best_sol is not None:
327
+ test_acc = evaluate_on_test(pop_best_sol)
328
+ if test_acc > best_test_acc:
329
+ best_test_acc = test_acc
330
+ best_solution = pop_best_sol.clone() if hasattr(pop_best_sol, "clone") else pop_best_sol
331
+ else:
332
+ test_acc = 0.0
333
+
334
+ epoch_logs.append(
335
+ {
336
+ "epoch": gen + 1,
337
+ "generation": gen + 1,
338
+ "accuracy": test_acc,
339
+ "test_accuracy": test_acc,
340
+ "pop_best_fitness": pop_best_fitness,
341
+ "mean_fitness": mean_fitness,
342
+ "loss": -pop_best_fitness,
343
+ "sigma": sigma,
344
+ "time": time.time() - start_time,
345
+ }
346
+ )
347
+
348
+ if verbose:
349
+ print(
350
+ f" Gen {gen + 1:4d} | best_fit={pop_best_fitness:.4f} | "
351
+ f"mean_fit={mean_fitness:.4f} | sigma={sigma:.6f} | "
352
+ f"test_acc={test_acc * 100:.1f}%"
353
+ )
354
+
355
+ elapsed = time.time() - start_time
356
+ total_evals = generations * popsize
357
+
358
+ # Final evaluation
359
+ final_test_acc = evaluate_on_test(best_solution) if best_solution is not None else best_test_acc
360
+
361
+ # Get memory stats
362
+ peak_memory = 0.0
363
+ if device == "cuda" and torch.cuda.is_available():
364
+ peak_memory = torch.cuda.max_memory_allocated() / 1024 / 1024
365
+
366
+ return BenchmarkResult(
367
+ optimizer="cmaes",
368
+ seed=0, # CMA-ES uses internal randomness
369
+ final_accuracy=final_test_acc,
370
+ best_accuracy=best_test_acc,
371
+ final_loss=None, # CMA-ES maximizes accuracy, no loss
372
+ wall_time_seconds=elapsed,
373
+ peak_gpu_memory_mb=peak_memory,
374
+ total_steps=generations,
375
+ function_evals=total_evals,
376
+ convergence_epoch=None,
377
+ epoch_logs=epoch_logs,
378
+ )
379
+
380
+
381
+ # ---------------------------------------------------------------------------
382
+ # Nevergrad ES Implementation
383
+ # ---------------------------------------------------------------------------
384
+
385
+
386
+ def train_nevergrad(
387
+ model: nn.Module,
388
+ train_data: Tensor,
389
+ train_labels: Tensor,
390
+ test_data: Tensor,
391
+ test_labels: Tensor,
392
+ budget: int = 10000,
393
+ batch_size: int = 512,
394
+ device: str = "cuda",
395
+ log_interval: int = 100,
396
+ verbose: bool = True,
397
+ model_init_args: tuple = (),
398
+ ) -> BenchmarkResult:
399
+ """Train model using Nevergrad OnePlusOne ES.
400
+
401
+ Nevergrad minimizes, so we negate accuracy for the fitness function.
402
+ Reports function_evals = budget.
403
+
404
+ Args:
405
+ model: PyTorch model to optimize.
406
+ train_data: Training data tensor, shape (N, ...).
407
+ train_labels: Training labels tensor, shape (N,).
408
+ test_data: Test data tensor, shape (M, ...).
409
+ test_labels: Test labels tensor, shape (M,).
410
+ budget: Total function evaluations (budget).
411
+ batch_size: Batch size for fitness evaluation.
412
+ device: Device for computation.
413
+ log_interval: Print stats every N evaluations.
414
+ verbose: Print progress.
415
+ model_init_args: Arguments for model constructor (for cloning).
416
+
417
+ Returns:
418
+ BenchmarkResult with training metrics.
419
+
420
+ Raises:
421
+ ImportError: If Nevergrad is not installed.
422
+ """
423
+ if not _HAS_NEVERGRAD:
424
+ raise ImportError(
425
+ f"Nevergrad is required for ES baseline. Install with: pip install nevergrad\n"
426
+ f"Import error: {_NEVERGRAD_ERROR}"
427
+ )
428
+
429
+ # Move model to device
430
+ model = model.to(device)
431
+ param_count = sum(p.numel() for p in model.parameters())
432
+
433
+ # Reset GPU memory stats
434
+ if device == "cuda" and torch.cuda.is_available():
435
+ torch.cuda.reset_peak_memory_stats()
436
+
437
+ # Move data to device
438
+ train_data_t = train_data.to(device)
439
+ train_labels_t = train_labels.to(device)
440
+ test_data_t = test_data.to(device)
441
+ test_labels_t = test_labels.to(device)
442
+
443
+ # Create Nevergrad parametrization
444
+ # OnePlusOne works well for neural network optimization
445
+ parametrization = ng.p.Array(shape=(param_count,), lower=-2.0, upper=2.0)
446
+ optimizer = ng.optimizers.OnePlusOne(
447
+ parametrization=parametrization,
448
+ budget=budget,
449
+ )
450
+
451
+ def load_params(params: np.ndarray) -> None:
452
+ """Load flattened numpy array into model parameters."""
453
+ offset = 0
454
+ with torch.no_grad():
455
+ for p in model.parameters():
456
+ numel = p.numel()
457
+ p.data.copy_(torch.from_numpy(params[offset : offset + numel]).view(p.shape).to(device))
458
+ offset += numel
459
+
460
+ def fitness(params: np.ndarray) -> float:
461
+ """Fitness function: negative accuracy (Nevergrad minimizes)."""
462
+ load_params(params)
463
+
464
+ # Sample random batch
465
+ batch_size_actual = min(batch_size, len(train_data_t))
466
+ indices = torch.randperm(len(train_data_t), device=device)[:batch_size_actual]
467
+ batch_data = train_data_t[indices]
468
+ batch_labels = train_labels_t[indices]
469
+
470
+ model.eval()
471
+ with torch.no_grad():
472
+ outputs = model(batch_data)
473
+ preds = outputs.argmax(dim=-1)
474
+ accuracy = (preds == batch_labels).float().mean().item()
475
+
476
+ return -accuracy # Negate for minimization
477
+
478
+ def evaluate_on_test(params: np.ndarray) -> float:
479
+ """Evaluate on full test set."""
480
+ load_params(params)
481
+ model.eval()
482
+
483
+ correct = 0
484
+ total = 0
485
+ eval_batch_size = 512
486
+
487
+ with torch.no_grad():
488
+ for i in range(0, len(test_data_t), eval_batch_size):
489
+ batch_data = test_data_t[i : i + eval_batch_size]
490
+ batch_labels = test_labels_t[i : i + eval_batch_size]
491
+ outputs = model(batch_data)
492
+ preds = outputs.argmax(dim=-1)
493
+ correct += (preds == batch_labels).sum().item()
494
+ total += len(batch_labels)
495
+
496
+ return correct / total if total > 0 else 0.0
497
+
498
+ # Optimization loop
499
+ epoch_logs = []
500
+ best_test_acc = 0.0
501
+ eval_count = 0
502
+ start_time = time.time()
503
+
504
+ if verbose:
505
+ print(f" Nevergrad OnePlusOne: budget={budget}, params={param_count:,}")
506
+
507
+ # Use tell/ask interface for finer control
508
+ while eval_count < budget:
509
+ # Ask for candidate
510
+ candidate = optimizer.ask()
511
+ params = candidate.value
512
+
513
+ # Evaluate fitness
514
+ loss = fitness(params)
515
+ eval_count += 1
516
+
517
+ # Tell optimizer the result
518
+ optimizer.tell(candidate, loss)
519
+
520
+ # Periodic logging and test evaluation
521
+ if eval_count % log_interval == 0 or eval_count == budget:
522
+ test_acc = evaluate_on_test(params)
523
+ if test_acc > best_test_acc:
524
+ best_test_acc = test_acc
525
+
526
+ epoch_logs.append(
527
+ {
528
+ "eval_count": eval_count,
529
+ "fitness": -loss, # Convert back to accuracy
530
+ "test_accuracy": test_acc,
531
+ }
532
+ )
533
+
534
+ if verbose:
535
+ print(f" Eval {eval_count:5d}/{budget} | fitness={-loss:.4f} | test_acc={test_acc * 100:.1f}%")
536
+
537
+ elapsed = time.time() - start_time
538
+
539
+ # Get best recommendation
540
+ recommendation = optimizer.recommend()
541
+ final_params = recommendation.value
542
+ final_test_acc = evaluate_on_test(final_params)
543
+ if final_test_acc > best_test_acc:
544
+ best_test_acc = final_test_acc
545
+
546
+ # Get memory stats
547
+ peak_memory = 0.0
548
+ if device == "cuda" and torch.cuda.is_available():
549
+ peak_memory = torch.cuda.max_memory_allocated() / 1024 / 1024
550
+
551
+ return BenchmarkResult(
552
+ optimizer="nevergrad",
553
+ seed=0, # Nevergrad uses internal randomness
554
+ final_accuracy=final_test_acc,
555
+ best_accuracy=best_test_acc,
556
+ final_loss=None, # ES optimizes accuracy, no loss
557
+ wall_time_seconds=elapsed,
558
+ peak_gpu_memory_mb=peak_memory,
559
+ total_steps=budget,
560
+ function_evals=budget,
561
+ convergence_epoch=None,
562
+ epoch_logs=epoch_logs,
563
+ )
564
+
565
+
566
+ # ---------------------------------------------------------------------------
567
+ # Utility functions for benchmark scripts
568
+ # ---------------------------------------------------------------------------
569
+
570
+
571
+ def check_gradient_free_deps() -> Tuple[bool, bool, str]:
572
+ """Check which gradient-free dependencies are available.
573
+
574
+ Returns:
575
+ Tuple of (has_evotorch, has_nevergrad, message).
576
+ """
577
+ messages = []
578
+ if not _HAS_EVOTORCH:
579
+ messages.append(f"EvoTorch not installed: {_EVOTORCH_ERROR}")
580
+ if not _HAS_NEVERGRAD:
581
+ messages.append(f"Nevergrad not installed: {_NEVERGRAD_ERROR}")
582
+
583
+ message = "\n".join(messages) if messages else "All gradient-free dependencies available"
584
+ return _HAS_EVOTORCH, _HAS_NEVERGRAD, message
585
+
586
+
587
+ def get_available_optimizers(include_gradient_free: bool = True) -> List[str]:
588
+ """Get list of available optimizers.
589
+
590
+ Args:
591
+ include_gradient_free: Whether to include CMA-ES and Nevergrad.
592
+
593
+ Returns:
594
+ List of available optimizer names.
595
+ """
596
+ optimizers = ["sgd", "adam", "adamw", "polystep"]
597
+
598
+ if include_gradient_free:
599
+ if _HAS_EVOTORCH:
600
+ optimizers.append("cmaes")
601
+ if _HAS_NEVERGRAD:
602
+ optimizers.append("nevergrad")
603
+
604
+ return optimizers
@@ -0,0 +1,13 @@
1
+ """Reinforcement-learning benchmark helpers for PolyStep experiments."""
2
+
3
+ from .cartpole import CartPoleEvaluator
4
+ from .metrics import build_rl_metrics, normalize_score
5
+ from .policies import DiscreteMLPPolicy, stack_module_params
6
+
7
+ __all__ = [
8
+ "build_rl_metrics",
9
+ "normalize_score",
10
+ "CartPoleEvaluator",
11
+ "DiscreteMLPPolicy",
12
+ "stack_module_params",
13
+ ]