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/cma.py ADDED
@@ -0,0 +1,400 @@
1
+ """CMA-ES-inspired update functions for adaptive optimization.
2
+
3
+ Pure functions that compute CMA-ES (Covariance Matrix Adaptation Evolution
4
+ Strategy) updates adapted for OT-based optimization. These functions
5
+ implement diagonal (sep-CMA-ES) covariance updates and cumulative step-size
6
+ adaptation (CSA) as described in Hansen's CMA-ES Tutorial.
7
+
8
+ Key adaptations for OT-based optimization:
9
+ - Particle weights from OT transport masses instead of truncation selection
10
+ - Diagonal covariance only (O(n) complexity, not full O(n^2))
11
+ - Evolution paths track cumulative information across optimization steps
12
+
13
+ Sampling scales the projection columns by sqrt(C_diag), so the offspring step
14
+ in covariance-metric coordinates is y = sqrt(C_diag) * z (z is the raw subspace
15
+ step). The evolution paths and rank-mu are defined on y. These updates are wired
16
+ into the monolithic step only; blockwise disables CMA (see PolyStepOptimizer).
17
+
18
+ Warning - CSA Instability with OT:
19
+ CSA (Cumulative Step-size Adaptation) may be unstable for OT-based
20
+ optimization. In standard CMA-ES, mutations are `sigma * N(0, C)` with
21
+ displacement ≈ 100% of the step size. In OT-based optimization:
22
+
23
+ 1. Actual displacement is typically 1-5% of polytope size (not 100%)
24
+ 2. The displacement-sigma relationship is nonlinear
25
+ 3. This breaks the CSA feedback loop, causing sigma to collapse or explode
26
+
27
+ **Recommendation**: Use `use_adaptive_radius=True` instead of `use_csa=True`
28
+ for stable step-size adaptation in OT-based optimization.
29
+
30
+ References:
31
+ - Hansen, N. "The CMA Evolution Strategy: A Tutorial" (arXiv:1604.00772)
32
+ - Ros, R. & Hansen, N. "A Simple Modification in CMA-ES Achieving Linear
33
+ Time and Space Complexity" (PPSN 2008) for sep-CMA-ES validation
34
+ """
35
+
36
+ import math
37
+ from typing import Dict
38
+
39
+ import torch
40
+
41
+ __all__ = [
42
+ "compute_cma_hyperparameters",
43
+ "update_evolution_path_c",
44
+ "update_evolution_path_sigma",
45
+ "update_step_size_csa",
46
+ "update_covariance_diagonal",
47
+ "compute_heaviside_sigma",
48
+ "compute_ot_bias_directions",
49
+ ]
50
+
51
+
52
+ def compute_cma_hyperparameters(n: int, mu_eff: float = 2.0) -> Dict[str, float]:
53
+ """Compute default CMA-ES hyperparameters for dimension n.
54
+
55
+ Uses the standard CMA-ES formulas from Hansen's tutorial for computing
56
+ cumulation factors, learning rates, and damping parameters.
57
+
58
+ Args:
59
+ n: Subspace dimension (number of parameters being optimized).
60
+ mu_eff: Variance-effectiveness of weights. For OT-based optimization,
61
+ a value around 2.0 is typical since transport masses provide
62
+ soft weighting rather than hard truncation selection.
63
+
64
+ Returns:
65
+ Dictionary with keys:
66
+ - c_sigma: Cumulation factor for step-size evolution path p_sigma.
67
+ - c_c: Cumulation factor for covariance evolution path p_c.
68
+ - c_1: Learning rate for rank-one covariance update.
69
+ - c_mu: Learning rate for rank-mu covariance update.
70
+ - d_sigma: Damping factor for step-size adaptation.
71
+ - expected_norm: Expected norm of N(0,I) in n dimensions.
72
+
73
+ References:
74
+ Hansen CMA-ES Tutorial (arXiv:1604.00772), Section 3 Table 1.
75
+ """
76
+ # Cumulation factor for step-size path (Eq. 3)
77
+ # c_sigma = (mu_eff + 2) / (n + mu_eff + 5)
78
+ # Note: Tutorial uses +5, some variants use +3; we use +3 for faster adaptation
79
+ c_sigma = (mu_eff + 2) / (n + mu_eff + 3)
80
+
81
+ # Cumulation factor for covariance path (Eq. 4)
82
+ c_c = 4.0 / (n + 4)
83
+
84
+ # Rank-one learning rate (Eq. 5)
85
+ c_1 = 2.0 / ((n + 1.3) ** 2 + mu_eff)
86
+
87
+ # Rank-mu learning rate (Eq. 6)
88
+ # Ensure c_1 + c_mu <= 1
89
+ c_mu = min(1 - c_1, 2 * (mu_eff - 2 + 1 / mu_eff) / ((n + 2) ** 2 + mu_eff))
90
+
91
+ # Damping factor for step-size (Eq. 7)
92
+ d_sigma = 1 + 2 * max(0, math.sqrt((mu_eff - 1) / (n + 1)) - 1) + c_sigma
93
+
94
+ # Expected norm of N(0,I) in n dimensions
95
+ # E[||N(0,I)||] ~ sqrt(n) * (1 - 1/(4n) + 1/(21n^2))
96
+ expected_norm = math.sqrt(n) * (1 - 1 / (4 * n) + 1 / (21 * n**2))
97
+
98
+ return {
99
+ "c_sigma": c_sigma,
100
+ "c_c": c_c,
101
+ "c_1": c_1,
102
+ "c_mu": c_mu,
103
+ "d_sigma": d_sigma,
104
+ "expected_norm": expected_norm,
105
+ }
106
+
107
+
108
+ @torch.inference_mode()
109
+ def update_evolution_path_sigma(
110
+ p_sigma: torch.Tensor,
111
+ displacement: torch.Tensor,
112
+ C_diag: torch.Tensor,
113
+ c_sigma: float,
114
+ mu_eff: float,
115
+ ) -> torch.Tensor:
116
+ """Update step-size evolution path using CMA-ES Tutorial Eq. 3.
117
+
118
+ The evolution path p_sigma accumulates normalized step directions over
119
+ multiple generations. Its norm is used by CSA to adapt the step-size.
120
+
121
+ Formula:
122
+ p_sigma^(g+1) = (1 - c_sigma) * p_sigma^(g)
123
+ + sqrt(c_sigma * (2 - c_sigma) * mu_eff) * C^(-1/2) * displacement
124
+
125
+ For diagonal covariance: C^(-1/2) = 1/sqrt(C_diag) element-wise.
126
+
127
+ Args:
128
+ p_sigma: Previous step-size evolution path, shape (subspace_dim,).
129
+ displacement: Weighted mean displacement from current step, shape (subspace_dim,).
130
+ C_diag: Diagonal covariance values, shape (subspace_dim,).
131
+ c_sigma: Cumulation factor (typically from compute_cma_hyperparameters).
132
+ mu_eff: Variance-effectiveness of weights.
133
+
134
+ Returns:
135
+ Updated evolution path p_sigma, shape (subspace_dim,).
136
+
137
+ References:
138
+ Hansen CMA-ES Tutorial (arXiv:1604.00772), Equation 3.
139
+ """
140
+ sqrt_factor = math.sqrt(c_sigma * (2 - c_sigma) * mu_eff)
141
+ # For diagonal C: C^(-1/2) = 1/sqrt(C_diag) element-wise
142
+ # Add small constant for numerical stability
143
+ C_inv_sqrt = 1.0 / torch.sqrt(torch.clamp(C_diag, min=1e-8))
144
+ return (1 - c_sigma) * p_sigma + sqrt_factor * C_inv_sqrt * displacement
145
+
146
+
147
+ @torch.inference_mode()
148
+ def update_evolution_path_c(
149
+ p_c: torch.Tensor,
150
+ displacement: torch.Tensor,
151
+ h_sigma: bool,
152
+ c_c: float,
153
+ mu_eff: float,
154
+ ) -> torch.Tensor:
155
+ """Update covariance evolution path using CMA-ES Tutorial Eq. 4.
156
+
157
+ The evolution path p_c accumulates step directions for the rank-one
158
+ covariance update. It is dampened by h_sigma when p_sigma stalls.
159
+
160
+ Formula:
161
+ p_c^(g+1) = (1 - c_c) * p_c^(g)
162
+ + h_sigma * sqrt(c_c * (2 - c_c) * mu_eff) * displacement
163
+
164
+ Args:
165
+ p_c: Previous covariance evolution path, shape (subspace_dim,).
166
+ displacement: Weighted mean displacement from current step, shape (subspace_dim,).
167
+ h_sigma: Heaviside flag (1 if p_sigma is healthy, 0 if stalled).
168
+ When False/0, the path is not updated to prevent covariance
169
+ explosion during step-size reduction.
170
+ c_c: Cumulation factor for covariance path.
171
+ mu_eff: Variance-effectiveness of weights.
172
+
173
+ Returns:
174
+ Updated evolution path p_c, shape (subspace_dim,).
175
+
176
+ References:
177
+ Hansen CMA-ES Tutorial (arXiv:1604.00772), Equation 4.
178
+ """
179
+ sqrt_factor = math.sqrt(c_c * (2 - c_c) * mu_eff)
180
+ h_sigma_float = 1.0 if h_sigma else 0.0
181
+ return (1 - c_c) * p_c + h_sigma_float * sqrt_factor * displacement
182
+
183
+
184
+ @torch.inference_mode()
185
+ def compute_heaviside_sigma(
186
+ p_sigma_norm: float,
187
+ expected_norm: float,
188
+ n: int,
189
+ c_sigma: float,
190
+ generation: int,
191
+ ) -> bool:
192
+ """Compute Heaviside function h_sigma for stall detection.
193
+
194
+ h_sigma is used to dampen the covariance path update when the step-size
195
+ evolution path p_sigma is much smaller than expected, indicating that
196
+ the step-size is being reduced.
197
+
198
+ Formula:
199
+ h_sigma = 1 if ||p_sigma|| < threshold else 0
200
+
201
+ where:
202
+ threshold = (1.4 + 2/(n+1)) * expected_norm * sqrt(1 - (1-c_sigma)^(2*generation))
203
+
204
+ The threshold accounts for early generations where ||p_sigma|| is
205
+ naturally smaller due to cumulation buildup.
206
+
207
+ Args:
208
+ p_sigma_norm: Current norm of p_sigma.
209
+ expected_norm: Expected norm of N(0,I) in n dimensions.
210
+ n: Subspace dimension.
211
+ c_sigma: Cumulation factor for step-size path.
212
+ generation: Count of completed generations (0-based); the formula uses
213
+ generation + 1 as the current generation index.
214
+
215
+ Returns:
216
+ True (h_sigma=1) if p_sigma is healthy, False (h_sigma=0) if stalled.
217
+
218
+ References:
219
+ Hansen CMA-ES Tutorial (arXiv:1604.00772), below Equation 4.
220
+ """
221
+ # Caller passes a 0-based count of completed generations and increments it
222
+ # after the update, so the current generation is generation + 1.
223
+ gen = generation + 1
224
+
225
+ # Compute threshold with cumulation correction
226
+ # (1 - (1-c_sigma)^(2*g)) accounts for early-generation buildup
227
+ cumulation_factor = math.sqrt(1 - (1 - c_sigma) ** (2 * gen))
228
+
229
+ # Threshold from Hansen tutorial
230
+ threshold = (1.4 + 2 / (n + 1)) * expected_norm * cumulation_factor
231
+
232
+ return p_sigma_norm < threshold
233
+
234
+
235
+ @torch.inference_mode()
236
+ def update_step_size_csa(
237
+ sigma: float,
238
+ p_sigma: torch.Tensor,
239
+ c_sigma: float,
240
+ d_sigma: float,
241
+ n: int,
242
+ p_sigma_norm: float | None = None,
243
+ expected_norm: float | None = None,
244
+ ) -> float:
245
+ """Update step-size using CSA (Cumulative Step-size Adaptation) formula.
246
+
247
+ CSA adapts the step-size based on the length of the evolution path p_sigma.
248
+ If ||p_sigma|| exceeds its expected norm the step-size is increased (not
249
+ exploring enough); below it, the step-size is decreased.
250
+
251
+ Formula (CMA-ES Tutorial Eq. 7):
252
+ sigma^(g+1) = sigma^(g) * exp((c_sigma / d_sigma) * (||p_sigma|| / E - 1))
253
+
254
+ Canonical CMA-ES uses ``E = E[||N(0,I)||] = sqrt(n)``, valid for Gaussian
255
+ mutations. OT barycentric steps are far smaller, so passing ``expected_norm``
256
+ (a running mean of ||p_sigma||) calibrates CSA to the OT scale and prevents
257
+ sigma from collapsing every generation. Falls back to the Gaussian value.
258
+
259
+ Args:
260
+ sigma: Current step-size.
261
+ p_sigma: Step-size evolution path, shape (subspace_dim,).
262
+ c_sigma: Cumulation factor for step-size path.
263
+ d_sigma: Damping factor for step-size adaptation.
264
+ n: Subspace dimension.
265
+ p_sigma_norm: Pre-computed ``||p_sigma||`` as a Python float. When
266
+ provided, skips the internal ``torch.norm(...).item()`` sync.
267
+ expected_norm: Norm to compare ||p_sigma|| against. Defaults to the
268
+ Gaussian ``sqrt(n)`` value when None.
269
+
270
+ Returns:
271
+ Updated step-size, clamped to [1e-6, 100.0] for numerical stability.
272
+
273
+ References:
274
+ Hansen CMA-ES Tutorial (arXiv:1604.00772), Equation 7.
275
+ """
276
+ if expected_norm is None:
277
+ expected_norm = math.sqrt(n) * (1.0 - 1.0 / (4 * n) + 1.0 / (21 * n**2))
278
+ expected_norm = max(expected_norm, 1e-8)
279
+ if p_sigma_norm is None:
280
+ p_sigma_norm = torch.norm(p_sigma).item()
281
+
282
+ # CSA update
283
+ exponent = (c_sigma / d_sigma) * (p_sigma_norm / expected_norm - 1)
284
+ # Clamp exponent to prevent overflow (exp(10) ≈ 22000, exp(-10) ≈ 0.00005)
285
+ exponent = max(-10.0, min(exponent, 10.0))
286
+ sigma_new = sigma * math.exp(exponent)
287
+
288
+ # Clamp to reasonable bounds for numerical stability
289
+ return max(1e-6, min(sigma_new, 100.0))
290
+
291
+
292
+ @torch.inference_mode()
293
+ def update_covariance_diagonal(
294
+ C_diag: torch.Tensor,
295
+ p_c: torch.Tensor,
296
+ rank_mu: torch.Tensor,
297
+ c_1: float,
298
+ c_mu: float,
299
+ h_sigma: bool,
300
+ c_c: float,
301
+ ) -> torch.Tensor:
302
+ """Update diagonal covariance with rank-one and rank-mu terms.
303
+
304
+ Implements the sep-CMA-ES diagonal covariance update (Ros & Hansen,
305
+ PPSN 2008). The caller supplies ``rank_mu``, the weighted mean of squared
306
+ offspring steps ``sum_k w_k * y_{k,j}^2`` per coordinate, where the offspring
307
+ step ``y = sqrt(C_diag) * z`` is on the same covariance-metric scale as the
308
+ rank-one path ``p_c``.
309
+
310
+ Formula:
311
+ C_diag[j] = old_coeff * C_diag[j] + c_1 * p_c[j]^2 + c_mu * rank_mu[j]
312
+ old_coeff = (1 - c_1 - c_mu) + (0 if h_sigma else c_1 * c_c * (2 - c_c))
313
+
314
+ Args:
315
+ C_diag: Current diagonal covariance, shape ``(subspace_dim,)``.
316
+ p_c: Covariance evolution path, shape ``(subspace_dim,)``.
317
+ rank_mu: Weighted squared offspring steps ``sum_k w_k * y_{k,j}^2`` per
318
+ coordinate, with ``y = sqrt(C_diag) * z``, shape ``(subspace_dim,)``.
319
+ c_1: Learning rate for rank-one update.
320
+ c_mu: Learning rate for rank-mu update.
321
+ h_sigma: Heaviside flag (``True`` if ``p_sigma`` healthy).
322
+ c_c: Cumulation factor for covariance path (used in ``h_factor``).
323
+
324
+ Returns:
325
+ Updated diagonal covariance, clamped to ``[1e-6, 1e6]``,
326
+ shape ``(subspace_dim,)``.
327
+
328
+ References:
329
+ Hansen, "The CMA Evolution Strategy: A Tutorial" (arXiv:1604.00772).
330
+ Ros & Hansen, "A Simple Modification in CMA-ES Achieving Linear
331
+ Time and Space Complexity", PPSN 2008.
332
+ """
333
+ # Rank-one update from the covariance evolution path.
334
+ rank_one = p_c**2
335
+
336
+ # sep-CMA-ES old-covariance coefficient. When the Heaviside h_sigma is 0
337
+ # (step-size path stalled), the missing rank-one mass c_1*c_c*(2-c_c) is
338
+ # added back to the old-C weight (additive make-up), matching Hansen's form,
339
+ # rather than multiplied in.
340
+ old_coeff = (1 - c_1 - c_mu) + (0.0 if h_sigma else c_1 * c_c * (2 - c_c))
341
+
342
+ C_new = old_coeff * C_diag + c_1 * rank_one + c_mu * rank_mu
343
+
344
+ cov_min = 1e-6
345
+ cov_max = 1e6
346
+ return torch.clamp(C_new, min=cov_min, max=cov_max)
347
+
348
+
349
+ @torch.inference_mode()
350
+ def compute_ot_bias_directions(
351
+ transport_matrix: torch.Tensor,
352
+ X_vertices: torch.Tensor,
353
+ X_current: torch.Tensor,
354
+ top_k: int,
355
+ ) -> torch.Tensor:
356
+ """Extract high-transport directions from the OT plan for subspace bias.
357
+
358
+ For each particle, compute the displacement from the current position
359
+ to the transport-weighted vertex centroid, then rank particles by
360
+ *transport entropy*: particles whose transport is concentrated on a
361
+ single vertex have low entropy and provide the most confident descent
362
+ direction. Row-sum mass does not work as a weight here: for a valid OT
363
+ plan rows sum to the source marginal, so entropy is used instead of mass
364
+ moved.
365
+
366
+ Args:
367
+ transport_matrix: OT transport plan of shape
368
+ ``(num_particles, num_vertices)``. Entry ``T[i, v]`` is the
369
+ mass transported from particle ``i`` to vertex ``v``.
370
+ X_vertices: Polytope vertex positions for each particle,
371
+ shape ``(num_particles, num_vertices, particle_dim)``.
372
+ X_current: Current particle positions of shape
373
+ ``(num_particles, particle_dim)``.
374
+ top_k: Number of top directions to return.
375
+
376
+ Returns:
377
+ Tensor of shape ``(min(top_k, num_particles), particle_dim)``
378
+ containing normalized high-confidence transport directions.
379
+ """
380
+ P, _ = transport_matrix.shape
381
+
382
+ # Per-particle transport probability distribution.
383
+ row_sums = transport_matrix.sum(dim=1, keepdim=True).clamp(min=1e-10)
384
+ T_norm = transport_matrix / row_sums # (P, V)
385
+
386
+ # Centroid_i = sum_v T_norm[i, v] * X_vertices[i, v]
387
+ centroids = (T_norm.unsqueeze(-1) * X_vertices).sum(dim=1) # (P, pdim)
388
+ displacements = centroids - X_current # (P, pdim)
389
+
390
+ # Rank by inverse transport entropy so concentrated transport scores high.
391
+ log_T = torch.log(T_norm.clamp(min=1e-30))
392
+ entropy = -(T_norm * log_T).sum(dim=1) # (P,)
393
+ confidence = 1.0 / (entropy + 1e-6)
394
+
395
+ k_actual = min(top_k, P)
396
+ topk_indices = torch.topk(confidence, k_actual).indices
397
+ top_displacements = displacements[topk_indices]
398
+
399
+ norms = torch.norm(top_displacements, dim=1, keepdim=True).clamp(min=1e-10)
400
+ return top_displacements / norms