| """WiSER CIR end-to-end CIR set-prediction model. |
| |
| Pipeline (AC-4 + AC-5 split): |
| |
| voxel_level --[scene_encode]--> scene_tokens (no TX, once per scene) |
| scene_tokens --[modulate_scene_tx(tx_emb)]--> tx_mem (per (scene, TX)) |
| tx_mem + rx --[head]--> (exists, delay_ns, peak_db) per query |
| |
| `SparseCsiDetrModel.encode_tx(...)` remains a thin helper for Fourier + MLP. |
| `SparseCsiDetrModel.scene_encode(...)` is the TX-independent backbone |
| forward used by the cache wrapper; `modulate_scene_tx(...)` is the |
| TX-conditioned pass. The trainer's `CachedSceneTxForward` wraps these so |
| `C_enc ≤ U` counts real scene-encoder forwards and `C_txmod ≤ P` counts |
| real TX-modulation forwards. |
| """ |
|
|
| from __future__ import annotations |
|
|
| from dataclasses import dataclass, field |
|
|
| import torch |
| from torch import nn |
|
|
| from ..config import ModelConfig as SharedModelConfig |
| from .backbone import BackboneConfig, build_backbone |
| from .detr_head import CIRPathSetDETRHead, DetrHeadConfig, ContinuousFourierEmbed |
|
|
|
|
| @dataclass(slots=True) |
| class ModelConfig: |
| """Runtime model config. Derive from `config.ModelConfig` via `.from_shared()`.""" |
|
|
| backbone: BackboneConfig = field(default_factory=BackboneConfig) |
| head: DetrHeadConfig = field(default_factory=DetrHeadConfig) |
|
|
| @classmethod |
| def from_shared(cls, shared: SharedModelConfig) -> "ModelConfig": |
| return cls( |
| backbone=BackboneConfig( |
| kind=shared.backbone_kind, |
| channels=shared.backbone_channels, |
| tx_emb_channels=shared.backbone_tx_emb_channels, |
| num_downsample_stages=shared.backbone_downsample_stages, |
| blocks_per_stage=shared.backbone_blocks_per_stage, |
| num_heads=shared.backbone_num_heads, |
| mlp_ratio=shared.backbone_mlp_ratio, |
| ), |
| head=DetrHeadConfig( |
| channels=shared.backbone_channels, |
| num_queries=shared.query_budget, |
| num_decoder_layers=shared.head_num_decoder_layers, |
| num_heads=shared.head_num_heads, |
| dropout=shared.head_dropout, |
| db_low=shared.db_low, |
| db_high=shared.db_high, |
| peak_db_head_arch=getattr(shared, "peak_db_head_arch", "flat"), |
| peak_db_hidden=int(getattr(shared, "peak_db_hidden", 512)), |
| peak_db_init_mean_db=float(getattr(shared, "peak_db_init_mean_db", -55.0)), |
| delay_head_arch=getattr(shared, "delay_head_arch", "flat"), |
| delay_hidden=int(getattr(shared, "delay_hidden", 512)), |
| delay_init_mean_ns=float(getattr(shared, "delay_init_mean_ns", 4.0)), |
| exists_head_arch=getattr(shared, "exists_head_arch", "flat"), |
| exists_hidden=int(getattr(shared, "exists_hidden", 512)), |
| exists_init_prob=float(getattr(shared, "exists_init_prob", 0.2)), |
| ), |
| ) |
|
|
|
|
| class SparseCsiDetrModel(nn.Module): |
| """The importable model class referenced by AC-1's smoke test. |
| |
| Exposes `scene_encode` and `modulate_scene_tx` as two separable stages so |
| the cache wrapper can enforce AC-5 invariants. The backbone itself |
| implements both: for the Round-2 `dense_fallback` kind, `scene_encode` |
| runs every sparse block WITHOUT AdaLN-zero modulation (zero condition |
| vector), and `modulate_scene_tx` adds the per-TX modulation pass. The |
| `trellis2` kind delegates to the vendored `ModulatedSparseTransformerBlock`. |
| """ |
|
|
| def __init__(self, config: ModelConfig | None = None) -> None: |
| super().__init__() |
| self.config = config or ModelConfig() |
| self.backbone = build_backbone(self.config.backbone) |
| self.tx_embed = ContinuousFourierEmbed(in_dim=3, num_bands=8) |
| self.tx_proj = nn.Sequential( |
| nn.Linear(self.tx_embed.out_dim, self.config.backbone.tx_emb_channels), |
| nn.GELU(), |
| nn.Linear(self.config.backbone.tx_emb_channels, self.config.backbone.tx_emb_channels), |
| ) |
| self.head = CIRPathSetDETRHead(self.config.head) |
|
|
| @classmethod |
| def from_shared(cls, shared: SharedModelConfig) -> "SparseCsiDetrModel": |
| return cls(ModelConfig.from_shared(shared)) |
|
|
| def encode_tx(self, tx_xyz_norm: torch.Tensor) -> torch.Tensor: |
| return self.tx_proj(self.tx_embed(tx_xyz_norm)) |
|
|
| def scene_encode(self, voxel_level: dict) -> dict: |
| """TX-independent backbone pass; produces reusable scene tokens. |
| |
| Delegates to `backbone.scene_encode_forward` which runs the TX-FREE |
| block stack (`DensePlainBlock` × S for dense_fallback, or plain |
| `SparseTransformerBlock` × S for trellis2) + the downsample stack. |
| The returned dict is safe to cache per unique scene in the batch. |
| """ |
| return self.backbone.scene_encode_forward(voxel_level) |
|
|
| def modulate_scene_tx(self, scene_tokens: dict, tx_emb: torch.Tensor) -> dict: |
| """Per-(scene, TX) modulation pass on the cached scene tokens. |
| |
| Delegates to `backbone.modulation_forward`: |
| * dense_fallback → `DenseAdaLNBlock × blocks_per_stage` with TX as |
| AdaLN-zero condition (`shift, scale, gate` all derived from TX). |
| * trellis2 → `ModulatedSparseTransformerBlock × blocks_per_stage` |
| with TX projected through `tx_mod_proj` into the vendored |
| AdaLN-zero path. |
| |
| This is the ONLY code path where TX enters the scene tensor; RX never |
| enters here. |
| """ |
| if tx_emb.dim() == 1: |
| tx_emb = tx_emb.unsqueeze(0) |
| return self.backbone.modulation_forward(scene_tokens, tx_emb) |
|
|
| def modulate_scene(self, voxel_level: dict, tx_emb: torch.Tensor) -> dict: |
| """Legacy one-shot: scene_encode + modulate_scene_tx in one call.""" |
| scene = self.scene_encode(voxel_level) |
| return self.modulate_scene_tx(scene, tx_emb) |
|
|
| def head_forward( |
| self, |
| tx_mem: torch.Tensor, |
| rx_xyz_norm: torch.Tensor, |
| tx_mem_key_padding_mask: torch.Tensor | None = None, |
| ) -> dict[str, torch.Tensor]: |
| return self.head(tx_mem, rx_xyz_norm, tx_mem_key_padding_mask) |
|
|
|
|
| __all__ = ["ModelConfig", "SparseCsiDetrModel"] |
|
|