File size: 15,303 Bytes
c1e2af3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

import math

import numpy as np
import torch
from scipy import sparse
from scipy.sparse.linalg import splu


class TrajectorySmoother:
    """A class for modifying trajectories to hit specific values at specific frames while respecting
    soft constraints.

    This class modifies a trajectory to hit specific values at specific
    frames, while respecting the following soft constraints:
    * Preserve the original positions
    * Bring the accelerations as close to zero as possible

    The weights of the position soft constraints are specified in pos_weight.

    This is posed as a minimization problem:
    E(x) = pos_weight * |x - x_orig|^2 +
           |A x|^2

    where you minimize E(x) subject to specified values at indices where
    "mask" is equal to 1. A is a matrix that computes the N-2 accelerations
    associated with frames n-1, n and n+1.
    """

    """
    min f(x) + g(z)
    s.t. I x - z = 0

    x --> argmin_x (f(x) + p/2 ||I x - z + u||^2)
    z --> argmin_z (g(z) + p/2 ||I x - z + u||^2)
    u --> u + I x - z

    f(x) = pos_weight * |x - x_orig|^2 + |A x|^2
    g(z) = inf if any(|z-t| > margin) else 0

    x minimization:

    E(x) = wp/2 * |x - x_orig|^2 + 1/2 |A x|^2 + p/2 |I x - z + u|^2
    E(x) = wp/2 * (x - x_orig)^T (x - x_orig) + x^T A^T A x + p/2 (I x - z + u)^T (I x - z + u)
    E(x) = wp/2 * (x^T x - x_orig^T x - x^T x_orig + x_orig^T x_orig) +
           1/2 x^T A^T A x +
           p/2 (x^T I^T I x - x^T I^T z + x^T I^T u - z^T I x + z^T z - z^T u + u^T I x - u^T z + u^T u)

    argmin E(x) = argmin [
            wp/2 * (x^T x - 2 x^T x_orig) +
            1/2 x^T A^T A x +
            p/2 (x^T I^T I x + 2 x^T I^T (u - z) )
    ]
    = argmin [
            x^T wp/2 * I * x - wp * x^T x_orig +
            1/2 x^T A^T A x +
            x^T p/2 I^T I x + p x^T I^T (u - z)
    ]
    = argmin [
            1/2 x^T (wp * I + A^T A + p I^T I) x - x^T (wp * x_orig + p I^T (z - u))
    ]

    x = (wp * I + A^T A + p I)^-1 (wp * x_orig + p (z - u))


    """

    def __init__(
        self,
        margins,
        pos_weight=0.0,
        loop=False,
        admm_iters=100,
        alpha_overrelax=1.0,
        circle_project=False,
    ):
        """Initialize the TrajectorySmoother.

        Args:
            margins: Array of margin values for each frame.
                    margins[i] < 0: unconstrained
                    margins[i] == 0: pinned on this frame
                    margins[i] > 0: can deviate within the margin
            pos_weight: Weight for position preservation
            loop: Whether the trajectory should loop
            admm_iters: Number of ADMM iterations
        """
        self.pos_weight = pos_weight
        self.admm_iters = admm_iters
        self.alpha_overrelax = alpha_overrelax
        self.circle_project = circle_project
        N = len(margins)

        # Store margin information as numpy arrays
        self.margin_vals = margins

        # Build acceleration matrix A
        a_data = []
        a_rows = []
        a_cols = []

        for i in range(1, N - 1):
            scale = 1.0
            a_data.extend([-scale, 2.0 * scale, -scale])
            a_rows.extend([i, i, i])
            a_cols.extend([i - 1, i, i + 1])

        if loop:
            # Add periodic accelerations
            scale = 1.0
            a_data.extend([-scale, 2.0 * scale, -scale])
            a_rows.extend([0, 0, 0])
            a_cols.extend([N - 1, 0, 1])

            scale = 1.0
            a_data.extend([-scale, 2.0 * scale, -scale])
            a_rows.extend([N - 1, N - 1, N - 1])
            a_cols.extend([N - 2, N - 1, 0])

        A = sparse.csr_matrix((a_data, (a_rows, a_cols)), shape=(N, N))

        # Build identity matrix
        identity_matrix = sparse.eye(N)

        # Build system matrix M
        M = pos_weight * identity_matrix + A.T @ A

        # Calculate ADMM step size
        diag_max = max(abs(M.diagonal()))
        self.admm_stepsize = 0.25 * np.sqrt(diag_max)

        M = M + self.admm_stepsize * identity_matrix
        self.system_lu = splu(M.tocsc())

    def smooth(self, targets, x0):
        """Interpolate between reference positions while satisfying constraints.

        Args:
            observations: Target positions for constrained frames (numpy array)
            ref_positions: Reference positions defining original shape
                         (numpy array)

        Returns:
            Interpolated positions (numpy array)
        """
        x_target = targets.copy()
        x = x0.copy()
        z = np.zeros_like(x)
        u = np.zeros_like(x)

        for _ in range(self.admm_iters):
            self.z_update(z, x, x_target, u)
            self.u_update(u, x, z)
            self.x_update(x, z, u, x_target)

        return x

    def x_update(self, x, z, u, x_t):
        """Update x in the ADMM iteration."""
        # x = (wp * I + A^T A + p I)^-1 (wp * x_orig + p (z - u))
        r = self.pos_weight * x_t + self.admm_stepsize * (z - u)
        x[:] = self.system_lu.solve(r)

    def z_update(self, z, x, z_t, u):
        """Update z in the ADMM iteration using vectorized operations."""
        # Compute the difference from target for all margin locations at once
        z[:] = x + u - z_t

        # Check if we need to project back to margin
        z_diff_norms = np.linalg.norm(z, axis=1)
        mask = z_diff_norms > self.margin_vals
        if np.any(mask):
            scale_factors = self.margin_vals[mask] / z_diff_norms[mask]
            z[mask] *= scale_factors[:, np.newaxis]

        # Add back the target
        z[:] += z_t

        if self.circle_project:
            z[:] = z / (np.linalg.norm(z, axis=1, keepdims=True) + 1.0e-6)

    def u_update(self, u, x, z):
        """Update u in the ADMM iteration using vectorized operations."""
        u[:] += self.alpha_overrelax * (x - z)


def smooth_signal(x, margins, pos_weight=0, alpha_overrelax=1.8, admm_iters=500, circle_project=False):
    x_smoothed = x.copy()
    x_smoothed[:] = x.mean(axis=0, keepdims=True)

    # smooth the signal, multigrid style by starting out coarse,
    # doubling the resolution and repeating until we're at the full
    # resolution, using the previous result as the initial guess.
    levels = int(math.floor(math.log2(len(x))))
    levels = max(levels - 4, 1)

    stepsize = 2**levels
    while True:
        # smooth signals at this level:
        num_steps = len(x_smoothed[::stepsize])
        smoother = TrajectorySmoother(
            margins=margins[::stepsize],
            pos_weight=pos_weight,
            alpha_overrelax=alpha_overrelax,
            admm_iters=admm_iters,
            circle_project=circle_project,
        )
        x_smoothed[::stepsize] = smoother.smooth(x[::stepsize], x_smoothed[::stepsize])

        # interpolate to next level:
        next_stepsize = stepsize // 2
        num_interleaved = len(x_smoothed[next_stepsize::stepsize])
        if num_interleaved == num_steps:
            # linearly extrapolate the last value if we have to:
            x_smoothed[next_stepsize::stepsize][-1] = (
                x_smoothed[::stepsize][-1] + (x_smoothed[::stepsize][-1] - x_smoothed[::stepsize][-2]) / 2
            )
            num_interleaved = num_interleaved - 1

        # linearly interpolate the remaining values:
        x_smoothed[next_stepsize::stepsize][:num_interleaved] = (
            x_smoothed[::stepsize][:-1] + x_smoothed[::stepsize][1:]
        ) / 2

        if stepsize == 1:
            break

        stepsize //= 2

    return x_smoothed


def get_smooth_root_pos(hip_translations):
    root_translations_xz = hip_translations[..., [0, 2]]
    root_translations_y = hip_translations[..., [1]]

    batch_size, nframes = root_translations_xz.shape[:2]
    margins = np.full(root_translations_xz.shape[1], 0.06)

    root_translations_smoothed_xz = []
    for batch in range(batch_size):
        root_translations_smoothed_xz.append(
            smooth_signal(root_translations_xz[batch].detach().cpu().numpy(), margins)[None]
        )

    root_translations_smoothed_xz = torch.tensor(np.concatenate(root_translations_smoothed_xz))

    root_translations = torch.cat(
        [
            root_translations_smoothed_xz.to(root_translations_y.device),
            root_translations_y,
        ],
        dim=-1,
    )[..., [0, 2, 1]]

    return root_translations


def smooth_trajectory_gpu(
    positions: torch.Tensor,
    margin: float = 0.06,
    smoothness_weight: float = 1.0,
    position_weight: float = 0.01,
    num_iters: int = 50,
) -> torch.Tensor:
    """GPU-accelerated trajectory smoothing using gradient descent.

    Args:
        positions: [N, D] tensor of positions to smooth
        margin: Maximum allowed deviation from original positions
        smoothness_weight: Weight for smoothness term (minimizes acceleration)
        position_weight: Weight for position preservation term
        num_iters: Number of optimization iterations

    Returns:
        Smoothed positions [N, D]
    """
    device = positions.device
    n_frames = positions.shape[0]

    if n_frames < 3:
        return positions.clone()

    # Initialize smoothed positions with original
    smoothed = positions.clone().requires_grad_(True)
    optimizer = torch.optim.Adam([smoothed], lr=0.01)

    for _ in range(num_iters):
        optimizer.zero_grad()

        # Smoothness loss: minimize second derivative (acceleration)
        vel = smoothed[1:] - smoothed[:-1]
        acc = vel[1:] - vel[:-1]
        smoothness_loss = smoothness_weight * torch.sum(acc**2)

        # Position preservation loss (soft constraint)
        position_loss = position_weight * torch.sum((smoothed - positions) ** 2)

        # Total loss
        loss = smoothness_loss + position_loss

        loss.backward()
        optimizer.step()

        # Project back to margin constraints
        with torch.no_grad():
            diff = smoothed - positions
            diff_norm = torch.norm(diff, dim=1, keepdim=True)
            mask = diff_norm > margin
            if mask.any():
                scale = margin / (diff_norm[mask] + 1e-8)
                smoothed[mask] = positions[mask] + diff[mask] * scale

    return smoothed.detach()


def smooth_trajectory_gpu_fast(
    positions: torch.Tensor,
    margin: float = 0.06,
    smoothness_weight: float = 100.0,
    position_weight: float = 0.1,
) -> torch.Tensor:
    """
    Fast GPU-accelerated trajectory smoothing using direct least squares solution.
    No iterative optimization - solves in one shot.

    Args:
        positions: [N, D] tensor of positions to smooth
        margin: Maximum allowed deviation from original positions
        smoothness_weight: Weight for smoothness term (minimizes acceleration)
        position_weight: Weight for position preservation term

    Returns:
        Smoothed positions [N, D]
    """
    device = positions.device
    dtype = positions.dtype
    n_frames = positions.shape[0]
    n_dims = positions.shape[1]

    if n_frames < 3:
        return positions.clone()

    # Build acceleration matrix A for second derivative
    # A[i] computes: x[i-1] - 2*x[i] + x[i+1]
    A = torch.zeros((n_frames - 2, n_frames), device=device, dtype=dtype)
    for i in range(n_frames - 2):
        A[i, i] = 1.0
        A[i, i + 1] = -2.0
        A[i, i + 2] = 1.0

    # Solve: minimize smoothness_weight * ||Ax||^2 + position_weight * ||x - x_orig||^2
    # Solution: x = (smoothness_weight * A^T A + position_weight * I)^(-1) * (position_weight * x_orig)

    ATA = A.T @ A  # [N, N]
    M = smoothness_weight * ATA + position_weight * torch.eye(n_frames, device=device, dtype=dtype)

    # Solve for each dimension independently
    smoothed = torch.zeros_like(positions)
    for d in range(n_dims):
        b = position_weight * positions[:, d]  # [N]
        # Use torch.linalg.solve for numerical stability
        smoothed[:, d] = torch.linalg.solve(M, b)

    # Apply margin constraints
    diff = smoothed - positions
    diff_norm = torch.norm(diff, dim=1)  # [N] - no keepdim to avoid shape mismatch
    mask = diff_norm > margin  # [N] boolean mask
    if mask.any():
        scale = margin / (diff_norm[mask] + 1e-8)  # [M] where M = number of True in mask
        # Need to broadcast scale [M] to match diff[mask] which is [M, D]
        smoothed[mask] = positions[mask] + diff[mask] * scale[:, None]

    return smoothed


def get_smooth_root_pos_gpu(hip_translations: torch.Tensor) -> torch.Tensor:
    """GPU-accelerated version of get_smooth_root_pos.

    Args:
        hip_translations: [B, T, 3] root positions

    Returns:
        Smoothed root positions [B, T, 3]
    """
    device = hip_translations.device
    root_translations_xz = hip_translations[..., [0, 2]]  # [B, T, 2]
    root_translations_y = hip_translations[..., [1]]  # [B, T, 1]

    batch_size = root_translations_xz.shape[0]

    # Smooth XZ components
    root_translations_smoothed_xz = []
    for batch_idx in range(batch_size):
        smoothed_xz = smooth_trajectory_gpu(
            root_translations_xz[batch_idx],  # [T, 2]
            margin=0.06,
            smoothness_weight=1.0,
            position_weight=0.01,
            num_iters=50,
        )
        root_translations_smoothed_xz.append(smoothed_xz[None])  # [1, T, 2]

    root_translations_smoothed_xz = torch.cat(root_translations_smoothed_xz, dim=0)  # [B, T, 2]

    # Combine smoothed XZ with original Y
    root_translations = torch.cat(
        [
            root_translations_smoothed_xz,
            root_translations_y,
        ],
        dim=-1,
    )  # [B, T, 3] with order [x, z, y]

    # Reorder to [x, y, z]
    root_translations = root_translations[..., [0, 2, 1]]

    return root_translations


def get_smooth_root_pos_gpu_fast(hip_translations: torch.Tensor) -> torch.Tensor:
    """Fast GPU-accelerated version of get_smooth_root_pos using direct solver.

    Args:
        hip_translations: [B, T, 3] root positions

    Returns:
        Smoothed root positions [B, T, 3]
    """
    device = hip_translations.device
    root_translations_xz = hip_translations[..., [0, 2]]  # [B, T, 2]
    root_translations_y = hip_translations[..., [1]]  # [B, T, 1]

    batch_size = root_translations_xz.shape[0]

    # Smooth XZ components using fast method
    root_translations_smoothed_xz = []
    for batch_idx in range(batch_size):
        smoothed_xz = smooth_trajectory_gpu_fast(
            root_translations_xz[batch_idx],  # [T, 2]
            margin=0.06,
            smoothness_weight=100.0,
            position_weight=0.1,
        )
        root_translations_smoothed_xz.append(smoothed_xz[None])  # [1, T, 2]

    root_translations_smoothed_xz = torch.cat(root_translations_smoothed_xz, dim=0)  # [B, T, 2]

    # Combine smoothed XZ with original Y
    root_translations = torch.cat(
        [
            root_translations_smoothed_xz,
            root_translations_y,
        ],
        dim=-1,
    )  # [B, T, 3] with order [x, z, y]

    # Reorder to [x, y, z]
    root_translations = root_translations[..., [0, 2, 1]]

    return root_translations