| |
| """ |
| Nexar collision prediction models. |
| |
| Two architectures: |
| |
| 1. NexarSimpleHead — MLP on last-window features (fast, good for ≤ 3 windows) |
| Input: [belief(H), tta_mean, tta_var, p_alert] ← last window only |
| Output: collision score ∈ (0, 1) |
| |
| 2. NexarTemporalHead — LSTM over window sequence (captures temporal dynamics) |
| Input: sequence of [proj(belief), tta_mean, tta_var, p_alert] per window |
| Output: collision score ∈ (0, 1) |
| """ |
| from __future__ import annotations |
|
|
| import torch |
| import torch.nn as nn |
| import torch.nn.functional as F |
|
|
|
|
| class NexarSimpleHead(nn.Module): |
| """ |
| MLP classifier on features from the LAST (most recent) temporal window. |
| |
| Input features (per clip): |
| - belief: [H] (SFT hidden state mean-pool) |
| - tta_mean: scalar |
| - tta_var: scalar |
| - p_alert: scalar (PolicyHead P(ALERT)) |
| |
| Total input dim: H + 3 |
| """ |
|
|
| def __init__(self, hidden_dim: int, dropout: float = 0.3): |
| super().__init__() |
| inp = hidden_dim + 3 |
| self.net = nn.Sequential( |
| nn.Linear(inp, 512), |
| nn.LayerNorm(512), |
| nn.ReLU(), |
| nn.Dropout(dropout), |
| nn.Linear(512, 128), |
| nn.LayerNorm(128), |
| nn.ReLU(), |
| nn.Dropout(dropout), |
| nn.Linear(128, 1), |
| ) |
|
|
| def forward( |
| self, |
| beliefs: torch.Tensor, |
| tta_means: torch.Tensor, |
| tta_vars: torch.Tensor, |
| p_alerts: torch.Tensor, |
| ) -> torch.Tensor: |
| x = torch.cat([ |
| beliefs, |
| tta_means.unsqueeze(-1), |
| tta_vars.unsqueeze(-1), |
| p_alerts.unsqueeze(-1), |
| ], dim=-1) |
| return torch.sigmoid(self.net(x)).squeeze(-1) |
|
|
|
|
| class NexarTemporalHead(nn.Module): |
| """ |
| LSTM over temporal window sequence. |
| |
| Per window features projected to proj_dim, then passed through LSTM. |
| The final hidden state feeds a 2-layer classification head. |
| |
| Input: [B, T, H+3] (T = n_windows, H = hidden_dim) |
| Output: [B] collision score ∈ (0, 1) |
| """ |
|
|
| def __init__( |
| self, |
| hidden_dim: int, |
| proj_dim: int = 64, |
| lstm_hidden: int = 128, |
| lstm_layers: int = 2, |
| dropout: float = 0.3, |
| ): |
| super().__init__() |
| feat_dim = hidden_dim + 3 |
| self.proj = nn.Sequential( |
| nn.Linear(feat_dim, proj_dim), |
| nn.LayerNorm(proj_dim), |
| nn.ReLU(), |
| ) |
| self.lstm = nn.LSTM( |
| proj_dim, lstm_hidden, |
| num_layers=lstm_layers, |
| batch_first=True, |
| dropout=dropout if lstm_layers > 1 else 0.0, |
| ) |
| self.head = nn.Sequential( |
| nn.Linear(lstm_hidden, 64), |
| nn.ReLU(), |
| nn.Dropout(dropout), |
| nn.Linear(64, 1), |
| ) |
|
|
| def forward( |
| self, |
| beliefs: torch.Tensor, |
| tta_means: torch.Tensor, |
| tta_vars: torch.Tensor, |
| p_alerts: torch.Tensor, |
| ) -> torch.Tensor: |
| x = torch.cat([ |
| beliefs, |
| tta_means.unsqueeze(-1), |
| tta_vars.unsqueeze(-1), |
| p_alerts.unsqueeze(-1), |
| ], dim=-1) |
| x = self.proj(x) |
| _, (h, _) = self.lstm(x) |
| h_last = h[-1] |
| return torch.sigmoid(self.head(h_last)).squeeze(-1) |
|
|
|
|
| def build_model(hidden_dim: int, arch: str = "temporal", **kwargs) -> nn.Module: |
| """Factory. arch: 'simple' | 'temporal'""" |
| if arch == "simple": |
| return NexarSimpleHead(hidden_dim, **kwargs) |
| if arch == "temporal": |
| return NexarTemporalHead(hidden_dim, **kwargs) |
| raise ValueError(f"Unknown arch: {arch}. Choose 'simple' or 'temporal'.") |
|
|