Download spectral_world_models/models.py from kiruluta/Spectral-World-Models-Reproducibility: direct link, hf CLI and curl.
- Browser
- Download file 23.3 kB
-
https://huggingface.co/kiruluta/Spectral-World-Models-Reproducibility/resolve/main/spectral_world_models/models.py
- Command line
-
hf download hf://kiruluta/Spectral-World-Models-Reproducibility/spectral_world_models/models.py
-
curl -L -o models.py https://huggingface.co/kiruluta/Spectral-World-Models-Reproducibility/resolve/main/spectral_world_models/models.py
23.3 kB
| ''' | |
| models.py defines a configurable spectral world model family and its building blocks. It includes: | |
| text encoders: DCTTextEncoder (fixed DCT basis) and MeanTextEncoder | |
| image encoders/decoders: wavelet, convolutional, and FFT-based variants | |
| transition modules: low-rank spectral, MLP, Koopman, and mini-transformer transitions | |
| WorldModel, which fuses image/text latents, predicts the next latent state, and decodes next-frame images plus text logits | |
| build_model(name, **kwargs) to instantiate named model variants and ablations by aliasing configuration names. | |
| ''' | |
| # Andrew Kiruluta, UC Berkeley, June 2026 | |
| from __future__ import annotations | |
| import math | |
| import torch | |
| import torch.nn as nn | |
| import torch.nn.functional as F | |
| from .haar import HaarWavelet2D | |
| from .data import VOCAB, ACTION_NAMES | |
| class DCTTextEncoder(nn.Module): | |
| """Text-to-spectral-field encoder using a fixed DCT basis over token position.""" | |
| def __init__(self, vocab_size: int, max_len: int, emb_dim: int, out_dim: int): | |
| super().__init__() | |
| self.max_len = max_len | |
| self.emb = nn.Embedding(vocab_size, emb_dim, padding_idx=0) | |
| dct = torch.zeros(max_len, max_len) | |
| for k in range(max_len): | |
| for n in range(max_len): | |
| alpha = math.sqrt(1.0 / max_len) if k == 0 else math.sqrt(2.0 / max_len) | |
| dct[k, n] = alpha * math.cos(math.pi * (n + 0.5) * k / max_len) | |
| self.register_buffer("dct", dct) | |
| self.proj = nn.Sequential( | |
| nn.Linear(max_len * emb_dim, out_dim), | |
| nn.LayerNorm(out_dim), | |
| nn.GELU(), | |
| ) | |
| def forward(self, tokens: torch.Tensor) -> torch.Tensor: | |
| e = self.emb(tokens) # [B, L, E] | |
| modes = torch.einsum("kl,ble->bke", self.dct, e) | |
| return self.proj(modes.flatten(1)) | |
| class MeanTextEncoder(nn.Module): | |
| def __init__(self, vocab_size: int, emb_dim: int, out_dim: int): | |
| super().__init__() | |
| self.emb = nn.Embedding(vocab_size, emb_dim, padding_idx=0) | |
| self.proj = nn.Sequential(nn.Linear(emb_dim, out_dim), nn.LayerNorm(out_dim), nn.GELU()) | |
| def forward(self, tokens: torch.Tensor) -> torch.Tensor: | |
| mask = tokens.ne(0).float().unsqueeze(-1) | |
| e = self.emb(tokens) | |
| pooled = (e * mask).sum(dim=1) / mask.sum(dim=1).clamp_min(1.0) | |
| return self.proj(pooled) | |
| class WaveletImageEncoder(nn.Module): | |
| def __init__(self, image_size: int, levels: int, out_dim: int): | |
| super().__init__() | |
| self.wave = HaarWavelet2D(image_size=image_size, levels=levels) | |
| self.coeff_dim = image_size * image_size | |
| self.net = nn.Sequential( | |
| nn.Linear(self.coeff_dim, out_dim), | |
| nn.LayerNorm(out_dim), | |
| nn.GELU(), | |
| ) | |
| def forward(self, image: torch.Tensor) -> torch.Tensor: | |
| coeffs = self.wave(image) | |
| return self.net(coeffs) | |
| class ConvImageEncoder(nn.Module): | |
| def __init__(self, image_size: int, out_dim: int): | |
| super().__init__() | |
| self.net = nn.Sequential( | |
| nn.Conv2d(1, 16, 3, stride=2, padding=1), nn.GELU(), | |
| nn.Conv2d(16, 32, 3, stride=2, padding=1), nn.GELU(), | |
| nn.Flatten(), | |
| nn.Linear(32 * (image_size // 4) * (image_size // 4), out_dim), | |
| nn.LayerNorm(out_dim), | |
| nn.GELU(), | |
| ) | |
| def forward(self, image: torch.Tensor) -> torch.Tensor: | |
| return self.net(image) | |
| class FFTImageEncoder(nn.Module): | |
| """Small Fourier feature encoder used as a neural-operator-style baseline.""" | |
| def __init__(self, image_size: int, out_dim: int, keep: int = 8): | |
| super().__init__() | |
| self.keep = keep | |
| # rfft width keep, height keep => real+imag features. | |
| in_dim = keep * keep * 2 | |
| self.net = nn.Sequential(nn.Linear(in_dim, out_dim), nn.LayerNorm(out_dim), nn.GELU()) | |
| def forward(self, image: torch.Tensor) -> torch.Tensor: | |
| fft = torch.fft.rfft2(image.squeeze(1), norm="ortho") | |
| x = fft[:, : self.keep, : self.keep] | |
| feats = torch.cat([x.real, x.imag], dim=1).flatten(1) | |
| return self.net(feats) | |
| class WaveletImageDecoder(nn.Module): | |
| def __init__(self, image_size: int, levels: int, latent_dim: int): | |
| super().__init__() | |
| self.wave = HaarWavelet2D(image_size=image_size, levels=levels) | |
| self.net = nn.Sequential( | |
| nn.Linear(latent_dim, 256), nn.GELU(), | |
| nn.Linear(256, image_size * image_size), | |
| ) | |
| def forward(self, z: torch.Tensor) -> torch.Tensor: | |
| coeffs = self.net(z) | |
| return torch.sigmoid(self.wave.inverse(coeffs)) | |
| class MLPImageDecoder(nn.Module): | |
| def __init__(self, image_size: int, latent_dim: int): | |
| super().__init__() | |
| self.image_size = image_size | |
| self.net = nn.Sequential( | |
| nn.Linear(latent_dim, 256), nn.GELU(), | |
| nn.Linear(256, image_size * image_size), nn.Sigmoid(), | |
| ) | |
| def forward(self, z: torch.Tensor) -> torch.Tensor: | |
| return self.net(z).view(z.shape[0], 1, self.image_size, self.image_size) | |
| class LowRankSpectralTransition(nn.Module): | |
| """Action-conditioned sparse/low-rank spectral operator dictionary. | |
| z_{t+1} = tanh(W0 z + sum_r beta_r(a) U_r V_r z + b(a) + nonlinear(z)). | |
| """ | |
| def __init__(self, latent_dim: int, action_dim: int, n_ops: int = 4, rank: int = 16): | |
| super().__init__() | |
| self.latent_dim = latent_dim | |
| self.n_ops = n_ops | |
| self.rank = rank | |
| self.base = nn.Linear(latent_dim, latent_dim, bias=False) | |
| with torch.no_grad(): | |
| self.base.weight.copy_(0.72 * torch.eye(latent_dim) + 0.01 * torch.randn(latent_dim, latent_dim)) | |
| self.U = nn.Parameter(torch.randn(n_ops, latent_dim, rank) * 0.035) | |
| self.V = nn.Parameter(torch.randn(n_ops, rank, latent_dim) * 0.035) | |
| self.beta = nn.Embedding(action_dim, n_ops) | |
| self.bias = nn.Embedding(action_dim, latent_dim) | |
| self.nonlin = nn.Sequential(nn.Linear(latent_dim, latent_dim), nn.GELU(), nn.Linear(latent_dim, latent_dim)) | |
| def forward(self, z: torch.Tensor, action: torch.Tensor) -> torch.Tensor: | |
| out = self.base(z) | |
| beta = torch.tanh(self.beta(action)) | |
| # Low-rank operator dictionary. | |
| for r in range(self.n_ops): | |
| vrz = z @ self.V[r].t() | |
| urvz = vrz @ self.U[r].t() | |
| out = out + beta[:, r : r + 1] * urvz | |
| out = out + self.bias(action) + 0.10 * self.nonlin(z) | |
| return torch.tanh(out) | |
| def stability_penalty(self) -> torch.Tensor: | |
| # Smooth proxy for keeping operator energy bounded. | |
| base_energy = torch.linalg.matrix_norm(self.base.weight, ord="fro") / math.sqrt(self.latent_dim) | |
| op_energy = torch.stack([torch.linalg.matrix_norm(self.U[r] @ self.V[r], ord="fro") / math.sqrt(self.latent_dim) for r in range(self.n_ops)]).mean() | |
| return F.relu(base_energy + op_energy - 0.95) ** 2 | |
| class SpectralNormConstrainedTransition(LowRankSpectralTransition): | |
| """Low-rank action-conditioned transition with a hard spectral-norm cap. | |
| For each action, form A(a)=W0+sum_r beta_r(a) U_r V_r and rescale the | |
| effective matrix when its largest singular value exceeds ``rho``. This | |
| variant intentionally removes the unconstrained nonlinear residual so the | |
| latent transition has a directly testable contraction bound before tanh. | |
| """ | |
| def __init__(self, latent_dim: int, action_dim: int, n_ops: int = 4, rank: int = 16, rho: float = 0.95): | |
| super().__init__(latent_dim, action_dim, n_ops=n_ops, rank=rank) | |
| self.rho = float(rho) | |
| self.action_dim = action_dim | |
| def effective_matrix(self, action_ids: torch.Tensor) -> torch.Tensor: | |
| # [A,D,D] for unique action ids. Embedding coefficients remain trainable. | |
| beta = torch.tanh(self.beta(action_ids)) | |
| uv = torch.einsum("odr,ore->ode", self.U, self.V) | |
| return self.base.weight.unsqueeze(0) + torch.einsum("ao,ode->ade", beta, uv) | |
| def constrained_matrix(self, action_ids: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: | |
| A = self.effective_matrix(action_ids) | |
| sigma = torch.linalg.matrix_norm(A, ord=2, dim=(-2, -1)) | |
| scale = torch.clamp(self.rho / sigma.clamp_min(1e-8), max=1.0) | |
| return A * scale[:, None, None], sigma | |
| def forward(self, z: torch.Tensor, action: torch.Tensor) -> torch.Tensor: | |
| unique, inverse = torch.unique(action, sorted=True, return_inverse=True) | |
| A, _ = self.constrained_matrix(unique) | |
| A_batch = A[inverse] | |
| out = torch.bmm(A_batch, z.unsqueeze(-1)).squeeze(-1) + self.bias(action) | |
| return torch.tanh(out) | |
| def stability_penalty(self) -> torch.Tensor: | |
| # Hard forward constraint already enforces the cap; this auxiliary loss | |
| # discourages repeatedly relying on rescaling. | |
| ids = torch.arange(self.action_dim, device=self.base.weight.device) | |
| raw = self.effective_matrix(ids) | |
| sigma = torch.linalg.matrix_norm(raw, ord=2, dim=(-2, -1)) | |
| return torch.mean(F.relu(sigma - self.rho) ** 2) | |
| def stability_diagnostics(self) -> dict[str, float]: | |
| ids = torch.arange(self.action_dim, device=self.base.weight.device) | |
| A, raw_sigma = self.constrained_matrix(ids) | |
| constrained_sigma = torch.linalg.matrix_norm(A, ord=2, dim=(-2, -1)) | |
| return { | |
| "raw_operator_norm_max": float(raw_sigma.max().cpu()), | |
| "raw_operator_norm_mean": float(raw_sigma.mean().cpu()), | |
| "effective_operator_norm_max": float(constrained_sigma.max().cpu()), | |
| "effective_operator_norm_mean": float(constrained_sigma.mean().cpu()), | |
| "rho": self.rho, | |
| } | |
| class SelectiveSpectralTransition(LowRankSpectralTransition): | |
| """Selective mode regularization for multimodal transport. | |
| Unlike the v3 hard contraction variant, this keeps singular modes at or below | |
| ``max_gain`` untouched and clips only expansive singular values. Near-unit | |
| modes are therefore preserved for persistent/action-conditioned transport. | |
| The nonlinear residual is retained. | |
| """ | |
| def __init__(self, latent_dim: int, action_dim: int, n_ops: int = 4, rank: int = 16, max_gain: float = 1.05): | |
| super().__init__(latent_dim, action_dim, n_ops=n_ops, rank=rank) | |
| self.max_gain = float(max_gain) | |
| self.action_dim = action_dim | |
| def effective_matrix(self, action_ids: torch.Tensor) -> torch.Tensor: | |
| beta = torch.tanh(self.beta(action_ids)) | |
| uv = torch.einsum("odr,ore->ode", self.U, self.V) | |
| return self.base.weight.unsqueeze(0) + torch.einsum("ao,ode->ade", beta, uv) | |
| def selective_matrix(self, action_ids: torch.Tensor): | |
| A = self.effective_matrix(action_ids) | |
| U, S, Vh = torch.linalg.svd(A, full_matrices=False) | |
| Sc = torch.clamp(S, max=self.max_gain) | |
| Ac = (U * Sc.unsqueeze(-2)) @ Vh | |
| return Ac, S, Sc | |
| def forward(self, z: torch.Tensor, action: torch.Tensor) -> torch.Tensor: | |
| unique, inverse = torch.unique(action, sorted=True, return_inverse=True) | |
| A, _, _ = self.selective_matrix(unique) | |
| out = torch.bmm(A[inverse], z.unsqueeze(-1)).squeeze(-1) | |
| out = out + self.bias(action) + 0.10 * self.nonlin(z) | |
| return torch.tanh(out) | |
| def stability_penalty(self) -> torch.Tensor: | |
| ids = torch.arange(self.action_dim, device=self.base.weight.device) | |
| A = self.effective_matrix(ids) | |
| sigma = torch.linalg.svdvals(A) | |
| # Penalize only expansive modes; do not push all modes toward contraction. | |
| return torch.mean(F.relu(sigma - self.max_gain) ** 2) | |
| def stability_diagnostics(self) -> dict[str, float]: | |
| ids = torch.arange(self.action_dim, device=self.base.weight.device) | |
| _, raw, clipped = self.selective_matrix(ids) | |
| return { | |
| "raw_operator_norm_max": float(raw[:, 0].max().cpu()), | |
| "raw_operator_norm_mean": float(raw[:, 0].mean().cpu()), | |
| "effective_operator_norm_max": float(clipped[:, 0].max().cpu()), | |
| "effective_operator_norm_mean": float(clipped[:, 0].mean().cpu()), | |
| "rho": self.max_gain, | |
| "fraction_modes_clipped": float((raw > self.max_gain).float().mean().cpu()), | |
| } | |
| class StructuredSpectralTransportTransition(nn.Module): | |
| """V6 structured action-conditioned transport. | |
| Separates persistent/energy-preserving transport, dissipative spectral modes, | |
| and action-driven low-rank transport instead of asking one generic operator | |
| to learn all three roles. The persistent block is parameterized by a | |
| skew-symmetric generator and a Cayley transform, giving an orthogonal | |
| transport matrix. Dissipation is diagonal in the learned latent basis and | |
| action effects are low rank. Only expansive singular modes of the composed | |
| operator are softly clipped at ``max_gain``. | |
| """ | |
| def __init__(self, latent_dim:int, action_dim:int, rank:int=12, max_gain:float=1.05): | |
| super().__init__(); self.latent_dim=latent_dim; self.action_dim=action_dim; self.rank=rank; self.max_gain=float(max_gain) | |
| self.generator=nn.Parameter(0.01*torch.randn(latent_dim,latent_dim)) | |
| self.logit_keep=nn.Parameter(torch.full((latent_dim,), 2.2)) | |
| self.action_U=nn.Parameter(0.025*torch.randn(action_dim,latent_dim,rank)) | |
| self.action_V=nn.Parameter(0.025*torch.randn(action_dim,rank,latent_dim)) | |
| self.action_gate=nn.Embedding(action_dim,1); nn.init.zeros_(self.action_gate.weight) | |
| self.bias=nn.Embedding(action_dim,latent_dim) | |
| self.residual=nn.Sequential(nn.Linear(latent_dim,latent_dim),nn.GELU(),nn.Linear(latent_dim,latent_dim)) | |
| def transport_matrix(self): | |
| G=0.25*torch.tanh(self.generator); K=0.5*(G-G.t()); I=torch.eye(self.latent_dim,device=K.device,dtype=K.dtype) | |
| return torch.linalg.solve(I-0.5*K, I+0.5*K) | |
| def effective_matrix(self, action_ids): | |
| Q=self.transport_matrix(); keep=0.80+0.20*torch.sigmoid(self.logit_keep) | |
| base=Q*keep.unsqueeze(0) | |
| uv=torch.einsum('adr,are->ade',self.action_U[action_ids],self.action_V[action_ids]) | |
| gate=torch.tanh(self.action_gate(action_ids)).view(-1,1,1) | |
| return base.unsqueeze(0)+gate*uv | |
| def selective_matrix(self, action_ids): | |
| A=self.effective_matrix(action_ids); U,S,Vh=torch.linalg.svd(A.detach(),full_matrices=False); Sc=torch.clamp(S,max=self.max_gain) | |
| Ac=(U*Sc.unsqueeze(-2))@Vh | |
| # Straight-through projection: clipped matrix in the forward pass while | |
| # avoiding unstable SVD gradients during optimization. | |
| return A + (Ac-A.detach()),S,Sc | |
| def forward(self,z,action): | |
| unique,inverse=torch.unique(action,sorted=True,return_inverse=True); A,_,_=self.selective_matrix(unique) | |
| out=torch.bmm(A[inverse],z.unsqueeze(-1)).squeeze(-1)+self.bias(action)+0.05*self.residual(z) | |
| return torch.tanh(out) | |
| def stability_penalty(self): | |
| ids=torch.arange(self.action_dim,device=self.generator.device); A=self.effective_matrix(ids); s=torch.linalg.svdvals(A) | |
| expansive=F.relu(s-self.max_gain).pow(2).mean(); q=self.transport_matrix(); I=torch.eye(self.latent_dim,device=q.device) | |
| orth=(q.t()@q-I).pow(2).mean(); return expansive+0.01*orth | |
| def stability_diagnostics(self): | |
| ids=torch.arange(self.action_dim,device=self.generator.device); _,raw,clip=self.selective_matrix(ids); q=self.transport_matrix(); I=torch.eye(self.latent_dim,device=q.device) | |
| return {'raw_operator_norm_max':float(raw[:,0].max().cpu()),'raw_operator_norm_mean':float(raw[:,0].mean().cpu()), | |
| 'effective_operator_norm_max':float(clip[:,0].max().cpu()),'effective_operator_norm_mean':float(clip[:,0].mean().cpu()), | |
| 'fraction_modes_clipped':float((raw>self.max_gain).float().mean().cpu()),'rho':self.max_gain, | |
| 'transport_orthogonality_error':float(torch.linalg.matrix_norm(q.t()@q-I,ord='fro').cpu()/self.latent_dim**0.5), | |
| 'mean_dissipative_keep':float((0.80+0.20*torch.sigmoid(self.logit_keep)).mean().cpu())} | |
| class MLPTransition(nn.Module): | |
| def __init__(self, latent_dim: int, action_dim: int): | |
| super().__init__() | |
| self.emb = nn.Embedding(action_dim, 24) | |
| self.net = nn.Sequential( | |
| nn.Linear(latent_dim + 24, 192), nn.GELU(), | |
| nn.Linear(192, 192), nn.GELU(), | |
| nn.Linear(192, latent_dim), nn.Tanh(), | |
| ) | |
| def forward(self, z: torch.Tensor, action: torch.Tensor) -> torch.Tensor: | |
| return self.net(torch.cat([z, self.emb(action)], dim=-1)) | |
| def stability_penalty(self) -> torch.Tensor: | |
| return torch.tensor(0.0, device=next(self.parameters()).device) | |
| class KoopmanTransition(nn.Module): | |
| def __init__(self, latent_dim: int, action_dim: int): | |
| super().__init__() | |
| self.A = nn.Parameter(0.75 * torch.eye(latent_dim).unsqueeze(0).repeat(action_dim, 1, 1) + 0.005 * torch.randn(action_dim, latent_dim, latent_dim)) | |
| self.bias = nn.Embedding(action_dim, latent_dim) | |
| def forward(self, z: torch.Tensor, action: torch.Tensor) -> torch.Tensor: | |
| A = self.A[action] | |
| return torch.tanh(torch.bmm(A, z.unsqueeze(-1)).squeeze(-1) + self.bias(action)) | |
| def stability_penalty(self) -> torch.Tensor: | |
| fro = torch.linalg.matrix_norm(self.A, ord="fro", dim=(1, 2)).mean() / math.sqrt(self.A.shape[-1]) | |
| return F.relu(fro - 0.95) ** 2 | |
| class MiniTransformerTransition(nn.Module): | |
| def __init__(self, latent_dim: int, action_dim: int): | |
| super().__init__() | |
| self.action = nn.Embedding(action_dim, latent_dim) | |
| layer = nn.TransformerEncoderLayer(d_model=latent_dim, nhead=4, dim_feedforward=192, batch_first=True, dropout=0.0) | |
| self.encoder = nn.TransformerEncoder(layer, num_layers=1) | |
| self.out = nn.Sequential(nn.Linear(latent_dim, latent_dim), nn.Tanh()) | |
| def forward(self, z: torch.Tensor, action: torch.Tensor) -> torch.Tensor: | |
| seq = torch.stack([z, self.action(action)], dim=1) | |
| h = self.encoder(seq) | |
| return self.out(h[:, 0]) | |
| def stability_penalty(self) -> torch.Tensor: | |
| return torch.tensor(0.0, device=next(self.parameters()).device) | |
| class WorldModel(nn.Module): | |
| def __init__( | |
| self, | |
| model_type: str = "swm", | |
| image_size: int = 32, | |
| max_text_len: int = 5, | |
| vocab_size: int = len(VOCAB), | |
| action_dim: int = len(ACTION_NAMES), | |
| latent_dim: int = 96, | |
| image_latent_dim: int = 48, | |
| text_latent_dim: int = 32, | |
| no_text: bool = False, | |
| ): | |
| super().__init__() | |
| self.model_type = model_type | |
| self.no_text = no_text | |
| self.latent_dim = latent_dim | |
| self.image_size = image_size | |
| if model_type in {"swm", "swm_spectral_norm", "swm_selective", "swm_structured", "swm_no_transition", "swm_no_text", "swm_no_stability"}: | |
| self.image_encoder = WaveletImageEncoder(image_size, levels=2, out_dim=image_latent_dim) | |
| self.text_encoder = DCTTextEncoder(vocab_size, max_text_len, emb_dim=24, out_dim=text_latent_dim) | |
| self.image_decoder = WaveletImageDecoder(image_size, levels=2, latent_dim=latent_dim) | |
| elif model_type == "neural_operator": | |
| self.image_encoder = FFTImageEncoder(image_size, out_dim=image_latent_dim) | |
| self.text_encoder = DCTTextEncoder(vocab_size, max_text_len, emb_dim=24, out_dim=text_latent_dim) | |
| self.image_decoder = MLPImageDecoder(image_size, latent_dim=latent_dim) | |
| else: | |
| self.image_encoder = ConvImageEncoder(image_size, out_dim=image_latent_dim) | |
| self.text_encoder = MeanTextEncoder(vocab_size, emb_dim=24, out_dim=text_latent_dim) | |
| self.image_decoder = MLPImageDecoder(image_size, latent_dim=latent_dim) | |
| if no_text or model_type == "swm_no_text": | |
| text_latent_dim = 0 | |
| self.no_text = True | |
| self.fusion = nn.Sequential( | |
| nn.Linear(image_latent_dim + text_latent_dim, latent_dim), | |
| nn.LayerNorm(latent_dim), | |
| nn.GELU(), | |
| nn.Linear(latent_dim, latent_dim), | |
| nn.LayerNorm(latent_dim), | |
| ) | |
| if model_type == "swm_spectral_norm": | |
| self.transition = SpectralNormConstrainedTransition(latent_dim, action_dim, n_ops=4, rank=12, rho=0.95) | |
| elif model_type == "swm_selective": | |
| self.transition = SelectiveSpectralTransition(latent_dim, action_dim, n_ops=4, rank=12, max_gain=1.05) | |
| elif model_type == "swm_structured": | |
| self.transition = StructuredSpectralTransportTransition(latent_dim, action_dim, rank=12, max_gain=1.05) | |
| elif model_type in {"swm", "swm_no_text", "swm_no_stability"}: | |
| self.transition = LowRankSpectralTransition(latent_dim, action_dim, n_ops=4, rank=12) | |
| elif model_type == "koopman": | |
| self.transition = KoopmanTransition(latent_dim, action_dim) | |
| elif model_type == "transformer": | |
| self.transition = MiniTransformerTransition(latent_dim, action_dim) | |
| else: | |
| self.transition = MLPTransition(latent_dim, action_dim) | |
| self.text_decoder = nn.Linear(latent_dim, max_text_len * vocab_size) | |
| self.max_text_len = max_text_len | |
| self.vocab_size = vocab_size | |
| def encode(self, image: torch.Tensor, tokens: torch.Tensor) -> torch.Tensor: | |
| zi = self.image_encoder(image) | |
| if self.no_text: | |
| fused = zi | |
| else: | |
| zt = self.text_encoder(tokens) | |
| fused = torch.cat([zi, zt], dim=-1) | |
| return self.fusion(fused) | |
| def decode_text(self, z: torch.Tensor) -> torch.Tensor: | |
| return self.text_decoder(z).view(z.shape[0], self.max_text_len, self.vocab_size) | |
| def forward(self, image: torch.Tensor, tokens: torch.Tensor, action: torch.Tensor) -> dict[str, torch.Tensor]: | |
| z = self.encode(image, tokens) | |
| z_next = self.transition(z, action) | |
| return { | |
| "z": z, | |
| "z_pred": z_next, | |
| "image_pred": self.image_decoder(z_next), | |
| "text_logits": self.decode_text(z_next), | |
| } | |
| def stability_penalty(self) -> torch.Tensor: | |
| return self.transition.stability_penalty() | |
| def build_model(name: str, **kwargs) -> WorldModel: | |
| aliases = { | |
| "swm": "swm", | |
| "swm_spectral_norm": "swm_spectral_norm", | |
| "swm_selective": "swm_selective", | |
| "swm_structured": "swm_structured", | |
| "swm_structured_cf": "swm_structured", | |
| "swm_structured_cf_v9": "swm_structured", | |
| "swm_structured_cf_v11": "swm_structured", | |
| "dreamer": "dreamer", | |
| "transformer": "transformer", | |
| "koopman": "koopman", | |
| "neural_operator": "neural_operator", | |
| "no_spectral_state": "dreamer", | |
| "no_spectral_transition": "swm_no_transition", | |
| "no_text_branch": "swm_no_text", | |
| "no_stability_penalty": "swm_no_stability", | |
| } | |
| if name not in aliases: | |
| raise ValueError(f"Unknown model '{name}'. Options: {sorted(aliases)}") | |
| return WorldModel(model_type=aliases[name], **kwargs) | |