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/geometry.py
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
"""Geometry module: polytope templates, rotation matrices, and probe generation.
|
|
2
|
+
|
|
3
|
+
Combines polytope vertex generators (orthoplex, simplex, cube), random rotation
|
|
4
|
+
via QR decomposition (Mezzadri method) or analytical 2D formula, and deterministic
|
|
5
|
+
probe point generation into a single module for PolyStep exploration directions.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import math
|
|
9
|
+
from typing import Callable, Dict, Optional, Tuple
|
|
10
|
+
|
|
11
|
+
import torch
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# ---------------------------------------------------------------------------
|
|
15
|
+
# Section 1: Polytope vertex templates
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_orthoplex_vertices(
|
|
20
|
+
dim: int,
|
|
21
|
+
*,
|
|
22
|
+
device: Optional[torch.device] = None,
|
|
23
|
+
dtype: Optional[torch.dtype] = None,
|
|
24
|
+
radius: float = 1.0,
|
|
25
|
+
**kwargs,
|
|
26
|
+
) -> torch.Tensor:
|
|
27
|
+
"""Generate orthoplex (cross-polytope) vertices centered at the origin.
|
|
28
|
+
|
|
29
|
+
Produces 2*dim vertices: the positive and negative unit vectors along each axis.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
dim: Dimensionality of the vertices.
|
|
33
|
+
device: Target device for the output tensor.
|
|
34
|
+
dtype: Target dtype for the output tensor.
|
|
35
|
+
radius: Scaling radius.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Vertices of shape (2*dim, dim).
|
|
39
|
+
"""
|
|
40
|
+
eye = torch.eye(dim, dtype=dtype, device=device)
|
|
41
|
+
points = torch.cat([eye, -eye], dim=0)
|
|
42
|
+
return points * radius
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_simplex_vertices(
|
|
46
|
+
dim: int,
|
|
47
|
+
*,
|
|
48
|
+
device: Optional[torch.device] = None,
|
|
49
|
+
dtype: Optional[torch.dtype] = None,
|
|
50
|
+
radius: float = 1.0,
|
|
51
|
+
**kwargs,
|
|
52
|
+
) -> torch.Tensor:
|
|
53
|
+
"""Generate regular simplex vertices centered at the origin.
|
|
54
|
+
|
|
55
|
+
Produces dim+1 vertices forming a regular simplex.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
dim: Dimensionality of the vertices.
|
|
59
|
+
device: Target device for the output tensor.
|
|
60
|
+
dtype: Target dtype for the output tensor.
|
|
61
|
+
radius: Scaling radius.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
Vertices of shape (dim+1, dim).
|
|
65
|
+
"""
|
|
66
|
+
points = math.sqrt(1 + 1 / dim) * torch.eye(dim, dtype=dtype, device=device)
|
|
67
|
+
points = points - ((math.sqrt(dim + 1) + 1) / math.sqrt(dim**3))
|
|
68
|
+
|
|
69
|
+
last_vertex = (1 / math.sqrt(dim)) * torch.ones(1, dim, dtype=dtype, device=device)
|
|
70
|
+
points = torch.cat([points, last_vertex], dim=0)
|
|
71
|
+
|
|
72
|
+
# Center simplex at origin (unlike orthoplex/cube, simplex is not inherently symmetric)
|
|
73
|
+
centroid = points.mean(dim=0, keepdim=True)
|
|
74
|
+
points = points - centroid
|
|
75
|
+
|
|
76
|
+
return points * radius
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def get_cube_vertices(
|
|
80
|
+
dim: int,
|
|
81
|
+
*,
|
|
82
|
+
device: Optional[torch.device] = None,
|
|
83
|
+
dtype: Optional[torch.dtype] = None,
|
|
84
|
+
radius: float = 1.0,
|
|
85
|
+
**kwargs,
|
|
86
|
+
) -> torch.Tensor:
|
|
87
|
+
"""Generate hypercube vertices using bitwise logic.
|
|
88
|
+
|
|
89
|
+
Produces 2^dim vertices: all sign combinations normalized by sqrt(dim).
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
dim: Dimensionality of the vertices.
|
|
93
|
+
device: Target device for the output tensor.
|
|
94
|
+
dtype: Target dtype for the output tensor.
|
|
95
|
+
radius: Scaling radius.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Vertices of shape (2^dim, dim).
|
|
99
|
+
"""
|
|
100
|
+
n_vertices = 2**dim
|
|
101
|
+
|
|
102
|
+
indices = torch.arange(n_vertices, dtype=torch.int32, device=device).unsqueeze(1)
|
|
103
|
+
shifts = torch.arange(dim, dtype=torch.int32, device=device).unsqueeze(0)
|
|
104
|
+
|
|
105
|
+
bits = (indices >> shifts) & 1
|
|
106
|
+
# Resolve dtype for the float conversion (default to float32 if None)
|
|
107
|
+
float_dtype = dtype if dtype is not None else torch.float32
|
|
108
|
+
signs = 1.0 - 2.0 * bits.to(float_dtype)
|
|
109
|
+
|
|
110
|
+
points = signs / math.sqrt(dim)
|
|
111
|
+
return points * radius
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
POLYTOPE_MAP: Dict[str, Callable] = {
|
|
115
|
+
"cube": get_cube_vertices,
|
|
116
|
+
"orthoplex": get_orthoplex_vertices,
|
|
117
|
+
"simplex": get_simplex_vertices,
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
POLYTOPE_NUM_VERTICES_MAP: Dict[str, Callable[[int], int]] = {
|
|
121
|
+
"cube": lambda dim: 2**dim,
|
|
122
|
+
"orthoplex": lambda dim: 2 * dim,
|
|
123
|
+
"simplex": lambda dim: dim + 1,
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# ---------------------------------------------------------------------------
|
|
128
|
+
# Section 2: Rotation matrices
|
|
129
|
+
# ---------------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def get_rotation_matrix_2d(theta: torch.Tensor) -> torch.Tensor:
|
|
133
|
+
"""Create 2x2 rotation matrices from angles using analytical formula.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
theta: Angles tensor of shape (...).
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
Rotation matrices of shape (..., 2, 2).
|
|
140
|
+
"""
|
|
141
|
+
c = torch.cos(theta)
|
|
142
|
+
s = torch.sin(theta)
|
|
143
|
+
row1 = torch.stack([c, -s], dim=-1)
|
|
144
|
+
row2 = torch.stack([s, c], dim=-1)
|
|
145
|
+
return torch.stack([row1, row2], dim=-2)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def get_random_rotation_matrices(
|
|
149
|
+
batch: int,
|
|
150
|
+
dim: int,
|
|
151
|
+
device: Optional[torch.device] = None,
|
|
152
|
+
dtype: Optional[torch.dtype] = None,
|
|
153
|
+
generator: Optional[torch.Generator] = None,
|
|
154
|
+
) -> torch.Tensor:
|
|
155
|
+
"""Generate batched uniformly random rotation matrices on SO(dim).
|
|
156
|
+
|
|
157
|
+
For dim=2, uses fast analytical rotation. For dim>2, uses batched QR
|
|
158
|
+
decomposition with Mezzadri sign correction for Haar measure.
|
|
159
|
+
|
|
160
|
+
Ref: F. Mezzadri, "How to generate random matrices from the
|
|
161
|
+
classical compact groups" (arXiv:math-ph/0609050).
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
batch: Number of rotation matrices to generate.
|
|
165
|
+
dim: Dimension of each rotation matrix.
|
|
166
|
+
device: Target device.
|
|
167
|
+
dtype: Target dtype.
|
|
168
|
+
generator: Optional torch.Generator for reproducibility.
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
Rotation matrices of shape (batch, dim, dim) with det(R) = +1.
|
|
172
|
+
"""
|
|
173
|
+
if dim == 2:
|
|
174
|
+
angles = torch.empty(batch, device=device, dtype=dtype)
|
|
175
|
+
angles.uniform_(0, 2 * math.pi, generator=generator)
|
|
176
|
+
return get_rotation_matrix_2d(angles)
|
|
177
|
+
|
|
178
|
+
# Batched QR decomposition for dim > 2
|
|
179
|
+
# QR decomposition requires FP32 on CPU (BF16 not supported for geqrf_cpu)
|
|
180
|
+
# Generate in FP32, compute QR, then convert to target dtype
|
|
181
|
+
# Resolve None device/dtype to concrete values for comparison
|
|
182
|
+
resolved_device = device if device is not None else torch.device("cpu")
|
|
183
|
+
resolved_dtype = dtype if dtype is not None else torch.float32
|
|
184
|
+
device_type = resolved_device.type if hasattr(resolved_device, "type") else str(resolved_device)
|
|
185
|
+
is_cpu = device_type == "cpu"
|
|
186
|
+
needs_fp32_qr = resolved_dtype == torch.bfloat16 and is_cpu
|
|
187
|
+
compute_dtype = torch.float32 if needs_fp32_qr else resolved_dtype
|
|
188
|
+
compute_device = "cpu" if needs_fp32_qr else resolved_device
|
|
189
|
+
|
|
190
|
+
# Generator device must match tensor device for ``randn``. If the user
|
|
191
|
+
# supplied a CUDA generator but we have to run QR on CPU (bfloat16
|
|
192
|
+
# fallback), sample on the generator's device first, then move the
|
|
193
|
+
# result to the QR compute device. This preserves reproducibility.
|
|
194
|
+
gen_for_randn = generator
|
|
195
|
+
sample_device = compute_device
|
|
196
|
+
needs_post_move = False
|
|
197
|
+
if generator is not None and hasattr(generator, "device"):
|
|
198
|
+
gen_device_type = generator.device.type if hasattr(generator.device, "type") else str(generator.device)
|
|
199
|
+
compute_device_type = "cpu" if needs_fp32_qr else device_type
|
|
200
|
+
if gen_device_type != compute_device_type:
|
|
201
|
+
sample_device = generator.device
|
|
202
|
+
needs_post_move = True
|
|
203
|
+
|
|
204
|
+
Z = torch.randn(batch, dim, dim, device=sample_device, dtype=compute_dtype, generator=gen_for_randn)
|
|
205
|
+
if needs_post_move:
|
|
206
|
+
Z = Z.to(device=compute_device)
|
|
207
|
+
Q, R = torch.linalg.qr(Z)
|
|
208
|
+
|
|
209
|
+
# Sign correction for Haar measure (Mezzadri method). Use a nonzero sign
|
|
210
|
+
# (sign(0) := +1) so an underflowed zero R-diagonal can't null a column.
|
|
211
|
+
d = torch.diagonal(R, dim1=-2, dim2=-1) # (batch, dim)
|
|
212
|
+
phases = torch.where(d == 0, torch.ones_like(d), torch.sign(d))
|
|
213
|
+
Q = Q * phases.unsqueeze(-2) # (batch, 1, dim) * (batch, dim, dim)
|
|
214
|
+
|
|
215
|
+
# Ensure det = +1 (SO(n) not just O(n)): flip first column where det < 0.
|
|
216
|
+
# Q is a fresh tensor (product above), so the in-place column flip is safe.
|
|
217
|
+
flip = torch.where(torch.det(Q) < 0, -1.0, 1.0).to(Q.dtype) # (batch,)
|
|
218
|
+
Q[:, :, 0] = Q[:, :, 0] * flip.unsqueeze(-1)
|
|
219
|
+
|
|
220
|
+
# Convert to target dtype and device
|
|
221
|
+
Q = Q.to(device=device, dtype=dtype)
|
|
222
|
+
|
|
223
|
+
return Q
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def apply_biased_rotation(rot_mats: torch.Tensor, bias_dir_norm: torch.Tensor) -> torch.Tensor:
|
|
227
|
+
"""Bias the first rotation axis toward ``bias_dir_norm`` and re-orthonormalize.
|
|
228
|
+
|
|
229
|
+
Replaces column 0 with the unit bias direction, re-orthonormalizes via QR,
|
|
230
|
+
and flips the last column where needed so det = +1 (a proper rotation).
|
|
231
|
+
Batch elements where QR is non-finite fall back to the input rotation.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
rot_mats: Rotation matrices of shape (batch, dim, dim).
|
|
235
|
+
bias_dir_norm: Unit bias directions of shape (batch, dim).
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
Biased rotation matrices of shape (batch, dim, dim), det = +1.
|
|
239
|
+
"""
|
|
240
|
+
if rot_mats.shape[-1] == 1:
|
|
241
|
+
# SO(1) = {[[1]]}: the only proper 1D rotation is the identity, so
|
|
242
|
+
# aligning axis 0 with the bias is not representable (a sign flip is a
|
|
243
|
+
# reflection). Return identity instead of flipping the aligned axis back.
|
|
244
|
+
return torch.ones_like(rot_mats)
|
|
245
|
+
biased = rot_mats.clone()
|
|
246
|
+
biased[:, :, 0] = bias_dir_norm
|
|
247
|
+
# geqrf has no bf16/fp16 CPU kernel; run QR in fp32, cast back.
|
|
248
|
+
qr_in = biased.float() if biased.dtype in (torch.bfloat16, torch.float16) else biased
|
|
249
|
+
Q, _ = torch.linalg.qr(qr_in)
|
|
250
|
+
Q = Q.to(biased.dtype)
|
|
251
|
+
valid = torch.isfinite(Q).all(dim=-1).all(dim=-1) # (batch,)
|
|
252
|
+
out = torch.where(valid[:, None, None], Q, rot_mats)
|
|
253
|
+
# QR fixes column 0 only up to sign; realign it with the bias so the search
|
|
254
|
+
# points toward descent, not ascent (sign 0 -> +1).
|
|
255
|
+
dot0 = (out[:, :, 0] * bias_dir_norm).sum(dim=1) # (batch,)
|
|
256
|
+
sign0 = torch.where(dot0 < 0, -1.0, 1.0).to(out.dtype)
|
|
257
|
+
out[:, :, 0] = out[:, :, 0] * sign0.unsqueeze(-1)
|
|
258
|
+
# Flip the last column to keep det = +1 (det has no bf16 CPU kernel, use fp32).
|
|
259
|
+
flip = (torch.det(out.float()) < 0).unsqueeze(-1)
|
|
260
|
+
out[:, :, -1] = torch.where(flip, -out[:, :, -1], out[:, :, -1])
|
|
261
|
+
return out
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def get_sobol_rotation_matrices(
|
|
265
|
+
batch: int,
|
|
266
|
+
dim: int,
|
|
267
|
+
device: torch.device = torch.device("cpu"),
|
|
268
|
+
dtype: torch.dtype = torch.float32,
|
|
269
|
+
engine: Optional[torch.quasirandom.SobolEngine] = None,
|
|
270
|
+
) -> torch.Tensor:
|
|
271
|
+
"""Generate rotation matrices from Sobol quasi-random sequences.
|
|
272
|
+
|
|
273
|
+
Low-discrepancy sequences ensure better coverage of SO(d) over
|
|
274
|
+
multiple calls than purely random rotations.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
batch: Number of rotation matrices.
|
|
278
|
+
dim: Dimension of the rotation.
|
|
279
|
+
device: Target device.
|
|
280
|
+
dtype: Target dtype.
|
|
281
|
+
engine: Optional pre-initialized SobolEngine (for stateful sequencing).
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
Rotation matrices of shape (batch, dim, dim) in SO(dim).
|
|
285
|
+
"""
|
|
286
|
+
if dim == 2:
|
|
287
|
+
# 2D: Sobol angle in [0, 2pi)
|
|
288
|
+
if engine is None:
|
|
289
|
+
engine = torch.quasirandom.SobolEngine(dimension=1, scramble=True)
|
|
290
|
+
uniform = engine.draw(batch).to(device=device, dtype=dtype) # (batch, 1)
|
|
291
|
+
angles = uniform[:, 0] * 2 * math.pi
|
|
292
|
+
return get_rotation_matrix_2d(angles)
|
|
293
|
+
|
|
294
|
+
# Higher dimensions: Sobol-driven QR (Mezzadri method with quasi-random input)
|
|
295
|
+
sobol_dim = dim * dim
|
|
296
|
+
if engine is None:
|
|
297
|
+
engine = torch.quasirandom.SobolEngine(dimension=sobol_dim, scramble=True)
|
|
298
|
+
uniform = engine.draw(batch) # (batch, dim*dim) in [0, 1]
|
|
299
|
+
|
|
300
|
+
# Transform uniform to normal via inverse CDF (erfinv)
|
|
301
|
+
uniform = uniform.clamp(1e-6, 1 - 1e-6)
|
|
302
|
+
normal = torch.erfinv(2 * uniform - 1) * math.sqrt(2)
|
|
303
|
+
normal = normal.to(device=device, dtype=dtype)
|
|
304
|
+
|
|
305
|
+
Z = normal.reshape(batch, dim, dim)
|
|
306
|
+
Q, R = torch.linalg.qr(Z)
|
|
307
|
+
|
|
308
|
+
# Mezzadri sign correction (sign(0) := +1 to keep columns nonzero)
|
|
309
|
+
d = torch.diagonal(R, dim1=-2, dim2=-1)
|
|
310
|
+
phases = torch.where(d == 0, torch.ones_like(d), torch.sign(d))
|
|
311
|
+
Q = Q * phases.unsqueeze(-2)
|
|
312
|
+
|
|
313
|
+
# Ensure det = +1 (Q is a fresh tensor, so the in-place flip is safe)
|
|
314
|
+
flip = torch.where(torch.det(Q) < 0, -1.0, 1.0).to(Q.dtype)
|
|
315
|
+
Q[:, :, 0] = Q[:, :, 0] * flip.unsqueeze(-1)
|
|
316
|
+
|
|
317
|
+
return Q
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
# ---------------------------------------------------------------------------
|
|
321
|
+
# Section 3: Probe point generation
|
|
322
|
+
# ---------------------------------------------------------------------------
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def get_probe_points(
|
|
326
|
+
origin: torch.Tensor,
|
|
327
|
+
directions: torch.Tensor,
|
|
328
|
+
scales: torch.Tensor,
|
|
329
|
+
probe_radius: float = 2.0,
|
|
330
|
+
) -> torch.Tensor:
|
|
331
|
+
"""Generate probe points at fixed scale intervals along directions.
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
origin: Center positions of shape (batch, dim).
|
|
335
|
+
directions: Direction vectors of shape (batch, num_points, dim).
|
|
336
|
+
scales: Scalar coefficients of shape (num_probe,).
|
|
337
|
+
probe_radius: Maximum distance multiplier.
|
|
338
|
+
|
|
339
|
+
Returns:
|
|
340
|
+
Probe points of shape (batch, num_points, num_probe, dim).
|
|
341
|
+
"""
|
|
342
|
+
# origin: (batch, 1, 1, dim)
|
|
343
|
+
origin_exp = origin[:, None, None, :]
|
|
344
|
+
# directions: (batch, num_points, 1, dim)
|
|
345
|
+
directions_exp = directions[:, :, None, :]
|
|
346
|
+
# scales: (1, 1, num_probe, 1)
|
|
347
|
+
scales_exp = scales[None, None, :, None]
|
|
348
|
+
|
|
349
|
+
return origin_exp + (directions_exp * probe_radius) * scales_exp
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
# ---------------------------------------------------------------------------
|
|
353
|
+
# Section 4: Main sampling pipeline
|
|
354
|
+
# ---------------------------------------------------------------------------
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def get_sampled_polytope_vertices(
|
|
358
|
+
origin: torch.Tensor,
|
|
359
|
+
probes: torch.Tensor,
|
|
360
|
+
polytope_vertices: torch.Tensor,
|
|
361
|
+
step_radius: float = 1.0,
|
|
362
|
+
probe_radius: float = 2.0,
|
|
363
|
+
generator: Optional[torch.Generator] = None,
|
|
364
|
+
**kwargs,
|
|
365
|
+
) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
|
366
|
+
"""Rotate a polytope template and generate deterministic probes.
|
|
367
|
+
|
|
368
|
+
Applies a random rotation to the polytope vertices for each particle,
|
|
369
|
+
translates by the step radius, and generates probe points at multiple
|
|
370
|
+
radii along each direction.
|
|
371
|
+
|
|
372
|
+
Args:
|
|
373
|
+
origin: Particle positions of shape (batch, dim) or (dim,).
|
|
374
|
+
probes: Scalar probe scales of shape (num_probe,).
|
|
375
|
+
polytope_vertices: Template vertices of shape (num_vertices, dim).
|
|
376
|
+
step_radius: Step distance multiplier.
|
|
377
|
+
probe_radius: Probe distance multiplier.
|
|
378
|
+
generator: Optional torch.Generator for reproducibility.
|
|
379
|
+
|
|
380
|
+
Returns:
|
|
381
|
+
Tuple of (step_points, probe_points, rotated_vertices):
|
|
382
|
+
- step_points: (batch, num_verts, dim) vertex positions after rotation + translation
|
|
383
|
+
- probe_points: (batch, num_verts, num_probe, dim) probe points along each direction
|
|
384
|
+
- rotated_vertices: (batch, num_verts, dim) rotated vertices before translation
|
|
385
|
+
"""
|
|
386
|
+
if origin.dim() == 1:
|
|
387
|
+
origin = origin.unsqueeze(0)
|
|
388
|
+
batch, dim = origin.shape
|
|
389
|
+
|
|
390
|
+
# 1. Generate rotation matrices (batch, dim, dim)
|
|
391
|
+
rot_mats = get_random_rotation_matrices(
|
|
392
|
+
batch,
|
|
393
|
+
dim,
|
|
394
|
+
device=origin.device,
|
|
395
|
+
dtype=origin.dtype,
|
|
396
|
+
generator=generator,
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
# 2. Apply rotation: R @ v for each vertex
|
|
400
|
+
# rot_mats: (batch, dim, dim), polytope_vertices: (num_verts, dim)
|
|
401
|
+
# Result: (batch, num_verts, dim)
|
|
402
|
+
rotated_vertices = torch.einsum("bji, ni -> bnj", rot_mats, polytope_vertices)
|
|
403
|
+
|
|
404
|
+
# 3. Translate step points
|
|
405
|
+
step_points = rotated_vertices * step_radius + origin.unsqueeze(1)
|
|
406
|
+
|
|
407
|
+
# 4. Generate probes (deterministic scaling)
|
|
408
|
+
probe_points = get_probe_points(origin, rotated_vertices, probes, probe_radius)
|
|
409
|
+
|
|
410
|
+
return step_points, probe_points, rotated_vertices
|