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.
- polystep/__init__.py +176 -0
- polystep/_compiled.py +334 -0
- polystep/_step_blockwise.py +994 -0
- polystep/_step_momentum.py +128 -0
- polystep/_step_monolithic.py +1070 -0
- polystep/adaptive_subspace.py +762 -0
- polystep/api.py +307 -0
- polystep/ask_tell.py +164 -0
- polystep/benchmarks/__init__.py +62 -0
- polystep/benchmarks/baselines.py +604 -0
- polystep/benchmarks/rl/__init__.py +13 -0
- polystep/benchmarks/rl/cartpole.py +260 -0
- polystep/benchmarks/rl/gym_evaluator.py +304 -0
- polystep/benchmarks/rl/metrics.py +53 -0
- polystep/benchmarks/rl/policies.py +133 -0
- polystep/benchmarks/utils.py +1262 -0
- polystep/blockwise.py +549 -0
- polystep/cma.py +400 -0
- polystep/cma_subspace.py +404 -0
- polystep/cost_nn.py +633 -0
- polystep/costs.py +86 -0
- polystep/dynamics.py +138 -0
- polystep/epsilon.py +217 -0
- polystep/geometry.py +410 -0
- polystep/hybrid_subspace.py +1335 -0
- polystep/layers/__init__.py +16 -0
- polystep/layers/attention.py +243 -0
- polystep/layers/rnn.py +276 -0
- polystep/objectives/__init__.py +25 -0
- polystep/objectives/base.py +77 -0
- polystep/objectives/synthetic.py +232 -0
- polystep/optimizer.py +1405 -0
- polystep/projection/__init__.py +5 -0
- polystep/projection/sparse.py +291 -0
- polystep/py.typed +0 -0
- polystep/quadratic_model.py +285 -0
- polystep/solver.py +654 -0
- polystep/solvers/__init__.py +34 -0
- polystep/solvers/_prelude.py +129 -0
- polystep/solvers/base.py +77 -0
- polystep/solvers/greedy.py +159 -0
- polystep/solvers/kl_softmax.py +200 -0
- polystep/solvers/sinkhorn.py +522 -0
- polystep/solvers/softmax.py +151 -0
- polystep/solvers/tempered_softmax.py +97 -0
- polystep/subspace.py +694 -0
- polystep/transform.py +365 -0
- polystep-0.7.0.dist-info/METADATA +285 -0
- polystep-0.7.0.dist-info/RECORD +52 -0
- polystep-0.7.0.dist-info/WHEEL +5 -0
- polystep-0.7.0.dist-info/licenses/LICENSE +191 -0
- 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
|