Spaces:
Running on Zero
Running on Zero
| # 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 | |