wuff-mann commited on
Commit
af2aed0
·
1 Parent(s): 68d530c

ACSSVAE C2 sync WaveSemanticHybridCodec/V5_ACSSVAE_C2_CausalProtectedReal/V5AwareStructuredSemanticVAE.py

Browse files
WaveSemanticHybridCodec/V5_ACSSVAE_C2_CausalProtectedReal/V5AwareStructuredSemanticVAE.py ADDED
@@ -0,0 +1,622 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ V5-Aware Analytic-Centered Structured Semantic VAE (AC-SSVAE)
3
+ ================================================================
4
+ Stage-2 feature extractor for ICWDS.
5
+
6
+ Design goals
7
+ ------------
8
+ 1. Consume WaveSystemSetParserV5 soft wave-system outputs instead of hard labels.
9
+ 2. Use an analytic physical descriptor as the prior center, and learn only bounded
10
+ semantic corrections.
11
+ 3. Canonicalize each wave system before shape encoding so the free latent does not
12
+ waste capacity on location, direction, scale, or energy.
13
+ 4. Keep a small causal shape latent. The residual decoder is constructed as
14
+ Delta(z, s) = F(z, s) - F(0, s)
15
+ so z=0 exactly returns the semantic base.
16
+ 5. Expose deterministic decoding APIs for quantization, intervention, and later
17
+ 30-byte packet design.
18
+
19
+ Internal semantic vector (9D)
20
+ -----------------------------
21
+ [log_energy,
22
+ peak_frequency_01,
23
+ sin_peak_direction,
24
+ cos_peak_direction,
25
+ frequency_spread_01,
26
+ direction_spread_over_pi,
27
+ f_theta_correlation,
28
+ frequency_skew,
29
+ direction_skew]
30
+
31
+ The packet-facing direction is still one circular quantity. sin/cos are only the
32
+ neural internal representation.
33
+ """
34
+ from __future__ import annotations
35
+
36
+ import math
37
+ from dataclasses import dataclass, asdict
38
+ from typing import Dict, Optional, Tuple
39
+
40
+ import torch
41
+ import torch.nn as nn
42
+ import torch.nn.functional as F
43
+
44
+
45
+ @dataclass
46
+ class V5AwareACSSVAEConfig:
47
+ n_freqs: int = 47
48
+ n_dirs: int = 72
49
+ n_slots: int = 6
50
+ semantic_dim: int = 9
51
+ shape_dim: int = 4
52
+ context_dim: int = 24
53
+
54
+ canonical_h: int = 32
55
+ canonical_w: int = 48
56
+ canonical_f_extent: float = 3.0
57
+ canonical_t_extent: float = 3.0
58
+
59
+ encoder_width: int = 48
60
+ hidden_dim: int = 160
61
+ decoder_width: int = 64
62
+
63
+ # Bounded semantic correction magnitudes around analytic center.
64
+ # [logE, f, sin/cos correction, spreads, rho, skews]
65
+ delta_loge: float = 0.35
66
+ delta_f: float = 0.08
67
+ delta_angle_rad: float = math.radians(18.0)
68
+ delta_log_spread: float = 0.45
69
+ delta_rho_logit: float = 0.75
70
+ delta_skew: float = 0.90
71
+
72
+ min_spread_f: float = 0.010
73
+ max_spread_f: float = 0.45
74
+ min_spread_t: float = 0.015
75
+ max_spread_t: float = 0.95
76
+ max_abs_rho: float = 0.92
77
+ max_abs_skew: float = 3.0
78
+
79
+ shape_log_residual_scale: float = 2.25
80
+ energy_log_den: float = math.log1p(47 * 72)
81
+ eps: float = 1e-6
82
+
83
+ def to_dict(self):
84
+ return asdict(self)
85
+
86
+
87
+ class ConvNormAct(nn.Module):
88
+ def __init__(self, ci: int, co: int, stride: int = 1):
89
+ super().__init__()
90
+ self.conv = nn.Conv2d(ci, co, 3, stride=stride, padding=1, bias=False)
91
+ self.norm = nn.GroupNorm(min(8, co), co)
92
+ self.act = nn.GELU()
93
+
94
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
95
+ return self.act(self.norm(self.conv(x)))
96
+
97
+
98
+ class ResBlock(nn.Module):
99
+ def __init__(self, ch: int):
100
+ super().__init__()
101
+ self.net = nn.Sequential(
102
+ nn.Conv2d(ch, ch, 3, padding=1, bias=False),
103
+ nn.GroupNorm(min(8, ch), ch),
104
+ nn.GELU(),
105
+ nn.Conv2d(ch, ch, 3, padding=1, bias=False),
106
+ nn.GroupNorm(min(8, ch), ch),
107
+ )
108
+ self.act = nn.GELU()
109
+
110
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
111
+ return self.act(x + self.net(x))
112
+
113
+
114
+ def _wrap_angle(x: torch.Tensor) -> torch.Tensor:
115
+ return torch.atan2(torch.sin(x), torch.cos(x))
116
+
117
+
118
+ def _atanh_safe(x: torch.Tensor, eps: float = 1e-5) -> torch.Tensor:
119
+ x = x.clamp(-1 + eps, 1 - eps)
120
+ return 0.5 * (torch.log1p(x) - torch.log1p(-x))
121
+
122
+
123
+ def reparameterize(mu: torch.Tensor, logvar: torch.Tensor, stochastic: bool) -> torch.Tensor:
124
+ if not stochastic:
125
+ return mu
126
+ std = torch.exp(0.5 * logvar)
127
+ return mu + std * torch.randn_like(std)
128
+
129
+
130
+ @torch.no_grad()
131
+ def analytic_semantics(
132
+ energy: torch.Tensor,
133
+ mask_prob: torch.Tensor,
134
+ core_prob: Optional[torch.Tensor] = None,
135
+ eps: float = 1e-6,
136
+ energy_log_den: Optional[float] = None,
137
+ ) -> torch.Tensor:
138
+ """Compute stable 9D physical descriptors from soft wave-system masks.
139
+
140
+ Parameters
141
+ ----------
142
+ energy: [B,1,H,W] or [B,H,W], non-negative display-domain energy.
143
+ mask_prob: [B,K,H,W].
144
+ core_prob: optional [B,K,H,W]. Core weighting sharpens peak estimation.
145
+ """
146
+ if energy.ndim == 3:
147
+ energy = energy[:, None]
148
+ B, K, H, W = mask_prob.shape
149
+ dtype, device = energy.dtype, energy.device
150
+ if energy_log_den is None:
151
+ energy_log_den = math.log1p(H * W)
152
+
153
+ E = torch.nan_to_num(energy, nan=0.0, posinf=1.0, neginf=0.0).clamp_min(0)
154
+ M = torch.nan_to_num(mask_prob, nan=0.0).clamp(0, 1)
155
+ part = E * M
156
+ mass = part.sum(dim=(-2, -1)).clamp_min(eps)
157
+ loge = torch.log1p(mass) / float(energy_log_den)
158
+
159
+ fgrid = torch.linspace(0, 1, H, device=device, dtype=dtype).view(1, 1, H, 1)
160
+ theta = torch.linspace(0, 2 * math.pi, W + 1, device=device, dtype=dtype)[:W]
161
+ tgrid = theta.view(1, 1, 1, W)
162
+
163
+ peak_src = part
164
+ if core_prob is not None:
165
+ core = torch.nan_to_num(core_prob, nan=0.0).clamp(0, 1)
166
+ peak_src = part * (0.25 + 0.75 * core)
167
+
168
+ # Smooth soft-peak estimator. It is more stable than hard argmax on noisy spectra.
169
+ pnorm = peak_src / peak_src.amax(dim=(-2, -1), keepdim=True).clamp_min(eps)
170
+ peak_w = torch.softmax((12.0 * pnorm).flatten(2), dim=-1).view(B, K, H, W)
171
+ fp = (peak_w * fgrid).sum(dim=(-2, -1))
172
+ sx = (peak_w * torch.sin(tgrid)).sum(dim=(-2, -1))
173
+ cx = (peak_w * torch.cos(tgrid)).sum(dim=(-2, -1))
174
+ theta_p = torch.atan2(sx, cx)
175
+ sinp = torch.sin(theta_p)
176
+ cosp = torch.cos(theta_p)
177
+
178
+ w = part / mass[:, :, None, None]
179
+ df = fgrid - fp[:, :, None, None]
180
+ dt = _wrap_angle(tgrid - theta_p[:, :, None, None]) / math.pi
181
+
182
+ sf = torch.sqrt((w * df.square()).sum(dim=(-2, -1)).clamp_min(eps))
183
+ st = torch.sqrt((w * dt.square()).sum(dim=(-2, -1)).clamp_min(eps))
184
+
185
+ zf = df / sf[:, :, None, None].clamp_min(1e-3)
186
+ zt = dt / st[:, :, None, None].clamp_min(1e-3)
187
+ rho = (w * zf * zt).sum(dim=(-2, -1)).clamp(-0.95, 0.95)
188
+ skew_f = (w * zf.pow(3)).sum(dim=(-2, -1)).clamp(-4.0, 4.0)
189
+ skew_t = (w * zt.pow(3)).sum(dim=(-2, -1)).clamp(-4.0, 4.0)
190
+
191
+ return torch.stack([
192
+ loge, fp, sinp, cosp, sf, st, rho, skew_f, skew_t
193
+ ], dim=-1)
194
+
195
+
196
+ class Canonicalizer(nn.Module):
197
+ """Differentiably center/scale each slot in frequency-direction coordinates."""
198
+ def __init__(self, cfg: V5AwareACSSVAEConfig):
199
+ super().__init__()
200
+ self.cfg = cfg
201
+ u = torch.linspace(-cfg.canonical_f_extent, cfg.canonical_f_extent, cfg.canonical_h)
202
+ v = torch.linspace(-cfg.canonical_t_extent, cfg.canonical_t_extent, cfg.canonical_w)
203
+ self.register_buffer("u", u.view(1, 1, cfg.canonical_h, 1))
204
+ self.register_buffer("v", v.view(1, 1, 1, cfg.canonical_w))
205
+
206
+ def forward(
207
+ self,
208
+ energy: torch.Tensor,
209
+ mask_prob: torch.Tensor,
210
+ core_prob: torch.Tensor,
211
+ support_prob: torch.Tensor,
212
+ semantics: torch.Tensor,
213
+ ) -> torch.Tensor:
214
+ if energy.ndim == 3:
215
+ energy = energy[:, None]
216
+ B, K, H, W = mask_prob.shape
217
+ eps = self.cfg.eps
218
+
219
+ E = energy[:, None].expand(B, K, 1, H, W)
220
+ M = mask_prob[:, :, None]
221
+ C = core_prob[:, :, None]
222
+ S = support_prob[:, :, None]
223
+ part = E * M
224
+ cpart = E * C
225
+ spart = E * S
226
+
227
+ peak = part.amax(dim=(-2, -1), keepdim=True).clamp_min(eps)
228
+ channels = torch.cat([
229
+ part / peak,
230
+ cpart / peak,
231
+ spart / peak,
232
+ M,
233
+ C,
234
+ S,
235
+ ], dim=2).reshape(B * K, 6, H, W)
236
+
237
+ sem = semantics.reshape(B * K, -1)
238
+ fp = sem[:, 1].clamp(0, 1)
239
+ theta = torch.atan2(sem[:, 2], sem[:, 3]) % (2 * math.pi)
240
+ sf = sem[:, 4].clamp(self.cfg.min_spread_f, self.cfg.max_spread_f)
241
+ st = sem[:, 5].clamp(self.cfg.min_spread_t, self.cfg.max_spread_t)
242
+
243
+ uf = self.u.to(channels.dtype)
244
+ vt = self.v.to(channels.dtype)
245
+ f_sample = fp[:, None, None, None] + sf[:, None, None, None] * uf
246
+ theta_sample = theta[:, None, None, None] + (math.pi * st[:, None, None, None]) * vt
247
+ theta_frac = torch.remainder(theta_sample, 2 * math.pi) / (2 * math.pi)
248
+
249
+ # Tile direction three times and sample from the middle copy.
250
+ tiled = torch.cat([channels, channels, channels], dim=-1)
251
+ y = 2.0 * f_sample - 1.0
252
+ xpix = (1.0 + theta_frac) * W - 0.5
253
+ x = 2.0 * xpix / max(3 * W - 1, 1) - 1.0
254
+ grid = torch.stack([
255
+ x.expand(-1, -1, self.cfg.canonical_h, self.cfg.canonical_w).squeeze(1),
256
+ y.expand(-1, -1, self.cfg.canonical_h, self.cfg.canonical_w).squeeze(1),
257
+ ], dim=-1)
258
+ out = F.grid_sample(
259
+ tiled,
260
+ grid,
261
+ mode="bilinear",
262
+ padding_mode="zeros",
263
+ align_corners=True,
264
+ )
265
+ return out.reshape(B, K, 6, self.cfg.canonical_h, self.cfg.canonical_w)
266
+
267
+
268
+ class SlotEncoder(nn.Module):
269
+ def __init__(self, cfg: V5AwareACSSVAEConfig):
270
+ super().__init__()
271
+ w = cfg.encoder_width
272
+ self.image_net = nn.Sequential(
273
+ ConvNormAct(6, w, 1),
274
+ ResBlock(w),
275
+ ConvNormAct(w, w * 2, 2),
276
+ ResBlock(w * 2),
277
+ ConvNormAct(w * 2, w * 3, 2),
278
+ ResBlock(w * 3),
279
+ ConvNormAct(w * 3, w * 4, 2),
280
+ nn.AdaptiveAvgPool2d(1),
281
+ nn.Flatten(),
282
+ )
283
+ self.context_net = nn.Sequential(
284
+ nn.Linear(cfg.context_dim + cfg.semantic_dim + 1, cfg.hidden_dim // 2),
285
+ nn.LayerNorm(cfg.hidden_dim // 2),
286
+ nn.GELU(),
287
+ nn.Linear(cfg.hidden_dim // 2, cfg.hidden_dim // 2),
288
+ nn.GELU(),
289
+ )
290
+ image_dim = w * 4
291
+ self.fuse = nn.Sequential(
292
+ nn.Linear(image_dim + cfg.hidden_dim // 2, cfg.hidden_dim),
293
+ nn.LayerNorm(cfg.hidden_dim),
294
+ nn.GELU(),
295
+ nn.Linear(cfg.hidden_dim, cfg.hidden_dim),
296
+ nn.GELU(),
297
+ )
298
+ self.sem_mu = nn.Linear(cfg.hidden_dim, cfg.semantic_dim)
299
+ self.sem_logvar = nn.Linear(cfg.hidden_dim, cfg.semantic_dim)
300
+ self.shape_mu = nn.Linear(cfg.hidden_dim, cfg.shape_dim)
301
+ self.shape_logvar = nn.Linear(cfg.hidden_dim, cfg.shape_dim)
302
+
303
+ # Start from the analytic center and nearly-zero free shape code.
304
+ nn.init.zeros_(self.sem_mu.weight)
305
+ nn.init.zeros_(self.sem_mu.bias)
306
+ nn.init.constant_(self.sem_logvar.bias, -4.0)
307
+ nn.init.normal_(self.shape_mu.weight, std=0.01)
308
+ nn.init.zeros_(self.shape_mu.bias)
309
+ nn.init.constant_(self.shape_logvar.bias, -3.0)
310
+
311
+ def forward(
312
+ self,
313
+ canonical: torch.Tensor,
314
+ analytic_sem: torch.Tensor,
315
+ context: torch.Tensor,
316
+ exist_prob: torch.Tensor,
317
+ ) -> Dict[str, torch.Tensor]:
318
+ B, K = canonical.shape[:2]
319
+ img = self.image_net(canonical.reshape(B * K, *canonical.shape[2:]))
320
+ ctx_in = torch.cat([
321
+ context.reshape(B * K, -1),
322
+ analytic_sem.reshape(B * K, -1),
323
+ exist_prob.reshape(B * K, 1),
324
+ ], dim=-1)
325
+ ctx = self.context_net(ctx_in)
326
+ h = self.fuse(torch.cat([img, ctx], dim=-1))
327
+ sem_mu = self.sem_mu(h).reshape(B, K, -1)
328
+ sem_logvar = self.sem_logvar(h).clamp(-8.0, 3.0).reshape(B, K, -1)
329
+ shape_mu = self.shape_mu(h).reshape(B, K, -1)
330
+ shape_logvar = self.shape_logvar(h).clamp(-8.0, 3.0).reshape(B, K, -1)
331
+ return {
332
+ "sem_delta_mu": sem_mu,
333
+ "sem_delta_logvar": sem_logvar,
334
+ "shape_mu": shape_mu,
335
+ "shape_logvar": shape_logvar,
336
+ }
337
+
338
+
339
+ class SemanticRenderer(nn.Module):
340
+ """Parameter-free generalized elliptical renderer from corrected semantics."""
341
+ def __init__(self, cfg: V5AwareACSSVAEConfig):
342
+ super().__init__()
343
+ self.cfg = cfg
344
+ f = torch.linspace(0, 1, cfg.n_freqs).view(1, 1, cfg.n_freqs, 1)
345
+ t = torch.linspace(0, 2 * math.pi, cfg.n_dirs + 1)[:cfg.n_dirs].view(1, 1, 1, cfg.n_dirs)
346
+ self.register_buffer("fgrid", f)
347
+ self.register_buffer("tgrid", t)
348
+
349
+ def forward(self, sem: torch.Tensor) -> torch.Tensor:
350
+ cfg = self.cfg
351
+ eps = cfg.eps
352
+ loge, fp = sem[..., 0], sem[..., 1]
353
+ theta = torch.atan2(sem[..., 2], sem[..., 3])
354
+ sf = sem[..., 4].clamp(cfg.min_spread_f, cfg.max_spread_f)
355
+ st = sem[..., 5].clamp(cfg.min_spread_t, cfg.max_spread_t)
356
+ rho = sem[..., 6].clamp(-cfg.max_abs_rho, cfg.max_abs_rho)
357
+ skew_f = sem[..., 7].clamp(-cfg.max_abs_skew, cfg.max_abs_skew)
358
+ skew_t = sem[..., 8].clamp(-cfg.max_abs_skew, cfg.max_abs_skew)
359
+
360
+ df = (self.fgrid - fp[:, :, None, None]) / sf[:, :, None, None].clamp_min(1e-3)
361
+ dt = _wrap_angle(self.tgrid - theta[:, :, None, None])
362
+ dt = dt / (math.pi * st[:, :, None, None].clamp_min(1e-3))
363
+ den = (1.0 - rho.square()).clamp_min(0.08)
364
+ q = (df.square() + dt.square() - 2.0 * rho[:, :, None, None] * df * dt) / den[:, :, None, None]
365
+ shape = torch.exp(-0.5 * q.clamp_max(60.0))
366
+ asym = torch.exp(
367
+ 0.28 * skew_f[:, :, None, None] * torch.tanh(df)
368
+ + 0.28 * skew_t[:, :, None, None] * torch.tanh(dt)
369
+ ).clamp(0.15, 6.0)
370
+ shape = shape * asym
371
+ shape = shape / shape.sum(dim=(-2, -1), keepdim=True).clamp_min(eps)
372
+ mass = torch.expm1(loge * cfg.energy_log_den).clamp_min(0.0)
373
+ return shape * mass[:, :, None, None]
374
+
375
+
376
+ class ShapeResidualNet(nn.Module):
377
+ def __init__(self, cfg: V5AwareACSSVAEConfig):
378
+ super().__init__()
379
+ self.cfg = cfg
380
+ w = cfg.decoder_width
381
+ self.fc = nn.Sequential(
382
+ nn.Linear(cfg.semantic_dim + cfg.shape_dim, cfg.hidden_dim),
383
+ nn.GELU(),
384
+ nn.Linear(cfg.hidden_dim, w * 6 * 9),
385
+ nn.GELU(),
386
+ )
387
+ self.net = nn.Sequential(
388
+ ConvNormAct(w, w, 1),
389
+ ResBlock(w),
390
+ nn.Upsample(size=(12, 18), mode="bilinear", align_corners=False),
391
+ ConvNormAct(w, w, 1),
392
+ ResBlock(w),
393
+ nn.Upsample(size=(24, 36), mode="bilinear", align_corners=False),
394
+ ConvNormAct(w, w // 2, 1),
395
+ ResBlock(w // 2),
396
+ nn.Upsample(size=(48, 72), mode="bilinear", align_corners=False),
397
+ ConvNormAct(w // 2, w // 2, 1),
398
+ nn.Conv2d(w // 2, 1, 3, padding=1),
399
+ )
400
+
401
+ def field(self, sem: torch.Tensor, zshape: torch.Tensor) -> torch.Tensor:
402
+ B, K = sem.shape[:2]
403
+ x = torch.cat([sem, zshape], dim=-1).reshape(B * K, -1)
404
+ h = self.fc(x).view(B * K, self.cfg.decoder_width, 6, 9)
405
+ out = self.net(h)[:, :, : self.cfg.n_freqs, : self.cfg.n_dirs]
406
+ return out.reshape(B, K, self.cfg.n_freqs, self.cfg.n_dirs)
407
+
408
+ def forward(self, sem: torch.Tensor, zshape: torch.Tensor) -> torch.Tensor:
409
+ raw = self.field(sem, zshape)
410
+ zero = self.field(sem, torch.zeros_like(zshape))
411
+ return self.cfg.shape_log_residual_scale * torch.tanh(raw - zero)
412
+
413
+
414
+ class V5AwareStructuredSemanticVAE(nn.Module):
415
+ def __init__(self, cfg: Optional[V5AwareACSSVAEConfig] = None):
416
+ super().__init__()
417
+ self.cfg = cfg or V5AwareACSSVAEConfig()
418
+ self.canonicalizer = Canonicalizer(self.cfg)
419
+ self.encoder = SlotEncoder(self.cfg)
420
+ self.semantic_renderer = SemanticRenderer(self.cfg)
421
+ self.shape_decoder = ShapeResidualNet(self.cfg)
422
+
423
+ def apply_semantic_delta(self, base: torch.Tensor, delta: torch.Tensor) -> torch.Tensor:
424
+ c = self.cfg
425
+ out = base.clone()
426
+ out[..., 0] = (base[..., 0] + c.delta_loge * torch.tanh(delta[..., 0])).clamp_min(0.0)
427
+ out[..., 1] = (base[..., 1] + c.delta_f * torch.tanh(delta[..., 1])).clamp(0.0, 1.0)
428
+
429
+ theta0 = torch.atan2(base[..., 2], base[..., 3])
430
+ # Use both sin/cos correction channels to form one stable tangent-angle correction.
431
+ dtheta = c.delta_angle_rad * torch.tanh(0.7071 * (delta[..., 2] - delta[..., 3]))
432
+ theta = theta0 + dtheta
433
+ out[..., 2] = torch.sin(theta)
434
+ out[..., 3] = torch.cos(theta)
435
+
436
+ out[..., 4] = (
437
+ base[..., 4].clamp_min(c.min_spread_f)
438
+ * torch.exp(c.delta_log_spread * torch.tanh(delta[..., 4]))
439
+ ).clamp(c.min_spread_f, c.max_spread_f)
440
+ out[..., 5] = (
441
+ base[..., 5].clamp_min(c.min_spread_t)
442
+ * torch.exp(c.delta_log_spread * torch.tanh(delta[..., 5]))
443
+ ).clamp(c.min_spread_t, c.max_spread_t)
444
+ out[..., 6] = torch.tanh(
445
+ _atanh_safe(base[..., 6], c.eps)
446
+ + c.delta_rho_logit * torch.tanh(delta[..., 6])
447
+ ).clamp(-c.max_abs_rho, c.max_abs_rho)
448
+ out[..., 7] = (
449
+ base[..., 7] + c.delta_skew * torch.tanh(delta[..., 7])
450
+ ).clamp(-c.max_abs_skew, c.max_abs_skew)
451
+ out[..., 8] = (
452
+ base[..., 8] + c.delta_skew * torch.tanh(delta[..., 8])
453
+ ).clamp(-c.max_abs_skew, c.max_abs_skew)
454
+ return out
455
+
456
+ def decode_from_codes(
457
+ self,
458
+ semantics: torch.Tensor,
459
+ shape_code: torch.Tensor,
460
+ exist_prob: Optional[torch.Tensor] = None,
461
+ ) -> Dict[str, torch.Tensor]:
462
+ base = self.semantic_renderer(semantics)
463
+ residual_log = self.shape_decoder(semantics, shape_code)
464
+ # Multiplicative residual in log domain, followed by exact energy renormalization.
465
+ part_hat = (base + self.cfg.eps) * torch.exp(residual_log)
466
+ target_mass = torch.expm1(semantics[..., 0] * self.cfg.energy_log_den).clamp_min(0.0)
467
+ part_hat = part_hat * (
468
+ target_mass[:, :, None, None]
469
+ / part_hat.sum(dim=(-2, -1), keepdim=True).clamp_min(self.cfg.eps)
470
+ )
471
+ if exist_prob is not None:
472
+ part_hat = part_hat * exist_prob[:, :, None, None]
473
+ base = base * exist_prob[:, :, None, None]
474
+ return {
475
+ "semantic_base": base,
476
+ "shape_residual_log": residual_log,
477
+ "part_hat": part_hat,
478
+ }
479
+
480
+ def forward(
481
+ self,
482
+ energy: torch.Tensor,
483
+ mask_prob: torch.Tensor,
484
+ core_prob: torch.Tensor,
485
+ support_prob: torch.Tensor,
486
+ exist_prob: torch.Tensor,
487
+ slot_context: torch.Tensor,
488
+ stochastic: bool = True,
489
+ shape_code_override: Optional[torch.Tensor] = None,
490
+ semantic_delta_override: Optional[torch.Tensor] = None,
491
+ ) -> Dict[str, torch.Tensor]:
492
+ analytic = analytic_semantics(
493
+ energy,
494
+ mask_prob,
495
+ core_prob=core_prob,
496
+ eps=self.cfg.eps,
497
+ energy_log_den=self.cfg.energy_log_den,
498
+ )
499
+ canonical = self.canonicalizer(
500
+ energy,
501
+ mask_prob,
502
+ core_prob,
503
+ support_prob,
504
+ analytic,
505
+ )
506
+ enc = self.encoder(canonical, analytic, slot_context, exist_prob)
507
+ sem_delta = (
508
+ semantic_delta_override
509
+ if semantic_delta_override is not None
510
+ else reparameterize(enc["sem_delta_mu"], enc["sem_delta_logvar"], stochastic)
511
+ )
512
+ zshape = (
513
+ shape_code_override
514
+ if shape_code_override is not None
515
+ else reparameterize(enc["shape_mu"], enc["shape_logvar"], stochastic)
516
+ )
517
+ corrected = self.apply_semantic_delta(analytic, sem_delta)
518
+ dec = self.decode_from_codes(corrected, zshape, exist_prob=exist_prob)
519
+ return {
520
+ "analytic_semantics": analytic,
521
+ "corrected_semantics": corrected,
522
+ "semantic_delta": sem_delta,
523
+ "shape_code": zshape,
524
+ "canonical": canonical,
525
+ **enc,
526
+ **dec,
527
+ }
528
+
529
+ @property
530
+ def token_dim_per_wave(self) -> int:
531
+ return self.cfg.semantic_dim + self.cfg.shape_dim
532
+
533
+ def num_params(self) -> int:
534
+ return sum(p.numel() for p in self.parameters())
535
+
536
+
537
+ def kl_standard_normal(mu: torch.Tensor, logvar: torch.Tensor, weight: Optional[torch.Tensor] = None) -> torch.Tensor:
538
+ kl = -0.5 * (1.0 + logvar - mu.square() - logvar.exp()).sum(dim=-1)
539
+ if weight is None:
540
+ return kl.mean()
541
+ w = weight.float()
542
+ return (kl * w).sum() / w.sum().clamp_min(1.0)
543
+
544
+
545
+ def covariance_penalty(x: torch.Tensor, weight: Optional[torch.Tensor] = None) -> torch.Tensor:
546
+ """Off-diagonal covariance penalty for [B,K,D] latent tensors."""
547
+ D = x.shape[-1]
548
+ flat = x.reshape(-1, D)
549
+ if weight is not None:
550
+ w = weight.reshape(-1).float()
551
+ keep = w > 0.25
552
+ flat = flat[keep]
553
+ if flat.shape[0] < max(4, D):
554
+ return x.new_zeros(())
555
+ flat = flat - flat.mean(dim=0, keepdim=True)
556
+ cov = flat.T @ flat / max(flat.shape[0] - 1, 1)
557
+ off = cov - torch.diag(torch.diag(cov))
558
+ return off.square().mean()
559
+
560
+
561
+ def semantic_distance(pred: torch.Tensor, target: torch.Tensor) -> torch.Tensor:
562
+ """Per-slot interpretable semantic distance with circular direction handling."""
563
+ d = []
564
+ d.append((pred[..., 0] - target[..., 0]).abs())
565
+ d.append(2.0 * (pred[..., 1] - target[..., 1]).abs())
566
+ tp = torch.atan2(pred[..., 2], pred[..., 3])
567
+ tt = torch.atan2(target[..., 2], target[..., 3])
568
+ d.append(_wrap_angle(tp - tt).abs() / math.pi)
569
+ d.append(2.0 * (pred[..., 4] - target[..., 4]).abs())
570
+ d.append(1.5 * (pred[..., 5] - target[..., 5]).abs())
571
+ d.append(0.5 * (pred[..., 6] - target[..., 6]).abs())
572
+ d.append(0.15 * (pred[..., 7] - target[..., 7]).abs())
573
+ d.append(0.15 * (pred[..., 8] - target[..., 8]).abs())
574
+ return torch.stack(d, dim=-1).mean(dim=-1)
575
+
576
+
577
+ def marginal_l1(pred: torch.Tensor, target: torch.Tensor, eps: float = 1e-6) -> Tuple[torch.Tensor, torch.Tensor]:
578
+ p = pred.clamp_min(0)
579
+ t = target.clamp_min(0)
580
+ pf = p.sum(dim=-1); tf = t.sum(dim=-1)
581
+ pt = p.sum(dim=-2); tt = t.sum(dim=-2)
582
+ pf = pf / pf.sum(dim=-1, keepdim=True).clamp_min(eps)
583
+ tf = tf / tf.sum(dim=-1, keepdim=True).clamp_min(eps)
584
+ pt = pt / pt.sum(dim=-1, keepdim=True).clamp_min(eps)
585
+ tt = tt / tt.sum(dim=-1, keepdim=True).clamp_min(eps)
586
+ return (pf - tf).abs().mean(), (pt - tt).abs().mean()
587
+
588
+
589
+ def reconstruction_terms(pred: torch.Tensor, target: torch.Tensor, weight: torch.Tensor, eps: float = 1e-6) -> Dict[str, torch.Tensor]:
590
+ w = weight[:, :, None, None].float()
591
+ denom = w.sum().clamp_min(1.0)
592
+ mse_slot = (pred - target).square().mean(dim=(-2, -1))
593
+ log_slot = (
594
+ torch.log1p(20.0 * pred.clamp_min(0))
595
+ - torch.log1p(20.0 * target.clamp_min(0))
596
+ ).abs().mean(dim=(-2, -1))
597
+ mass_p = pred.sum(dim=(-2, -1))
598
+ mass_t = target.sum(dim=(-2, -1))
599
+ energy_slot = (mass_p - mass_t).abs() / mass_t.clamp_min(eps)
600
+ mse = (mse_slot * weight).sum() / denom
601
+ log_l1 = (log_slot * weight).sum() / denom
602
+ energy = (energy_slot * weight).sum() / denom
603
+ freq, direction = marginal_l1(pred * w, target * w, eps=eps)
604
+ return {
605
+ "mse": mse,
606
+ "log_l1": log_l1,
607
+ "energy": energy,
608
+ "freq": freq,
609
+ "direction": direction,
610
+ }
611
+
612
+
613
+ __all__ = [
614
+ "V5AwareACSSVAEConfig",
615
+ "V5AwareStructuredSemanticVAE",
616
+ "analytic_semantics",
617
+ "kl_standard_normal",
618
+ "covariance_penalty",
619
+ "semantic_distance",
620
+ "reconstruction_terms",
621
+ "marginal_l1",
622
+ ]