| """ |
| Future-trajectory interaction graph for LED. |
| |
| Idea (ported from MoFlow's FutureInteractionGraphV6): |
| At each leapfrog denoising step, convert the frozen core denoiser's |
| epsilon prediction into an implied y_0 estimate, then run a small |
| agent-to-agent graph on that future geometry and emit a residual |
| correction added back to epsilon. The module is zero-initialized at |
| the output so it begins as a no-op and only nudges the reverse chain |
| as it learns. |
| """ |
|
|
| import torch |
| import torch.nn as nn |
| import torch.nn.functional as F |
|
|
|
|
| class FutureInteractionGraph(nn.Module): |
| def __init__( |
| self, |
| num_agents: int = 11, |
| future_steps: int = 20, |
| past_steps: int = 10, |
| past_channels: int = 6, |
| node_dim: int = 128, |
| top_n: int = 5, |
| num_denoise_steps: int = 5, |
| use_sigma: bool = False, |
| ): |
| super().__init__() |
| self.A = num_agents |
| self.T = future_steps |
| self.D = node_dim |
| self.top_n = min(top_n, num_agents - 1) |
| self.use_sigma = use_sigma |
|
|
| self.past_proj = nn.Sequential( |
| nn.Linear(past_steps * past_channels, node_dim), |
| nn.ReLU(inplace=True), |
| nn.Linear(node_dim, node_dim), |
| ) |
| self.y0_proj = nn.Sequential( |
| nn.Linear(future_steps * 2, node_dim), |
| nn.ReLU(inplace=True), |
| nn.Linear(node_dim, node_dim), |
| ) |
| self.step_emb = nn.Embedding(num_denoise_steps, node_dim) |
|
|
| if use_sigma: |
| self.sigma_proj = nn.Sequential( |
| nn.Linear(1, node_dim // 4), |
| nn.ReLU(inplace=True), |
| nn.Linear(node_dim // 4, node_dim), |
| ) |
|
|
| |
| edge_in_dim = 6 if use_sigma else 5 |
| self.edge_mlp = nn.Sequential( |
| nn.Linear(edge_in_dim, 32), |
| nn.ReLU(inplace=True), |
| nn.Linear(32, node_dim), |
| ) |
|
|
| self.attn_q = nn.Linear(node_dim, node_dim) |
| self.attn_k = nn.Linear(node_dim * 2, node_dim) |
| self.attn_v = nn.Linear(node_dim * 2, node_dim) |
| self.scale = node_dim ** -0.5 |
|
|
| self.fuse = nn.Sequential( |
| nn.Linear(node_dim * 2, node_dim), |
| nn.ReLU(inplace=True), |
| ) |
|
|
| self.decode = nn.Sequential( |
| nn.Linear(node_dim, node_dim), |
| nn.ReLU(inplace=True), |
| nn.Linear(node_dim, future_steps * 2), |
| ) |
| |
| nn.init.zeros_(self.decode[-1].weight) |
| nn.init.zeros_(self.decode[-1].bias) |
|
|
| def forward( |
| self, |
| y0_hat: torch.Tensor, |
| past: torch.Tensor, |
| step_idx: int, |
| sigma: torch.Tensor = None, |
| ) -> torch.Tensor: |
| BA, K, T, _ = y0_hat.shape |
| A = self.A |
| assert BA % A == 0, f"expected B*A agents with A={A}, got {BA}" |
| B = BA // A |
| device = y0_hat.device |
|
|
| past_feat = self.past_proj(past.reshape(BA, -1)) |
| past_feat = past_feat.unsqueeze(1).expand(BA, K, self.D) |
| y0_feat = self.y0_proj(y0_hat.reshape(BA, K, T * 2)) |
| step = self.step_emb(torch.tensor(step_idx, device=device)) |
| nodes = past_feat + y0_feat + step |
|
|
| if self.use_sigma and sigma is not None: |
| sigma_feat = self.sigma_proj(sigma) |
| nodes = nodes + sigma_feat.unsqueeze(1) |
|
|
| nodes = nodes.view(B, A, K, self.D).permute(0, 2, 1, 3).contiguous() |
| |
|
|
| pos = y0_hat.view(B, A, K, T, 2).permute(0, 2, 1, 3, 4).contiguous() |
| |
|
|
| |
| |
| rel = pos.unsqueeze(2) - pos.unsqueeze(3) |
| mean_rel = rel.mean(dim=-2) |
| std_rel = rel.std(dim=-2) |
| dist = rel.norm(dim=-1) |
| min_dist = dist.min(dim=-1).values.unsqueeze(-1) |
|
|
| if self.use_sigma and sigma is not None: |
| |
| sigma_bka = sigma.view(B, A, 1).permute(0, 2, 1).contiguous() |
| sigma_i = sigma_bka.unsqueeze(3).expand(-1, K, A, A) |
| sigma_j = sigma_bka.unsqueeze(2).expand(-1, K, A, A) |
| sigma_diff = (sigma_i - sigma_j).unsqueeze(-1) |
| edge_raw = torch.cat([mean_rel, std_rel, min_dist, sigma_diff], dim=-1) |
| else: |
| edge_raw = torch.cat([mean_rel, std_rel, min_dist], dim=-1) |
| edge_feat = self.edge_mlp(edge_raw) |
|
|
| |
| score = -min_dist.squeeze(-1) |
| diag = torch.eye(A, dtype=torch.bool, device=device) |
| score = score.masked_fill(diag, float('-inf')) |
| _, top_idx = score.topk(self.top_n, dim=-1) |
|
|
| idx_node = top_idx.unsqueeze(-1).expand(-1, -1, -1, -1, self.D) |
| nodes_j = nodes.unsqueeze(2).expand(-1, -1, A, -1, -1) |
| neigh_nodes = torch.gather(nodes_j, 3, idx_node) |
|
|
| idx_edge = top_idx.unsqueeze(-1).expand(-1, -1, -1, -1, self.D) |
| neigh_edges = torch.gather(edge_feat, 3, idx_edge) |
|
|
| q = self.attn_q(nodes).unsqueeze(-2) |
| kv_in = torch.cat([neigh_nodes, neigh_edges], dim=-1) |
| k = self.attn_k(kv_in) |
| v = self.attn_v(kv_in) |
| attn = (q * k).sum(dim=-1) * self.scale |
| attn = F.softmax(attn, dim=-1) |
| msg = (attn.unsqueeze(-1) * v).sum(dim=-2) |
|
|
| fused = self.fuse(torch.cat([nodes, msg], dim=-1)) |
| residual = self.decode(fused).view(B, K, A, T, 2) |
| residual = residual.permute(0, 2, 1, 3, 4).contiguous().view(BA, K, T, 2) |
| return residual |
|
|