AbstractPhil commited on
Commit
c458491
·
verified ·
1 Parent(s): eff598d

Create cell4_vae_pipeline.py

Browse files
Files changed (1) hide show
  1. cell4_vae_pipeline.py +325 -0
cell4_vae_pipeline.py ADDED
@@ -0,0 +1,325 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Cell 4: Multi-Scale Geometric Extraction Pipeline
3
+ ===================================================
4
+ Run after Cells 1-3. Uses globals from prior cells.
5
+
6
+ Updated for PatchCrossAttentionClassifier (no Conv3d).
7
+ Defines extraction functions and the MultiScaleExtractor class.
8
+ Does NOT execute anything — Cell 5 uses these.
9
+ """
10
+
11
+ import numpy as np
12
+ import torch
13
+ import torch.nn.functional as F
14
+ from dataclasses import dataclass, field
15
+ from typing import List, Optional, Tuple, Dict
16
+ import math
17
+
18
+
19
+ @dataclass
20
+ class ExtractionConfig:
21
+ canonical_shape: Tuple[int, int, int] = (8, 16, 16)
22
+ scales: List[Tuple[int, int, int]] = field(default_factory=lambda: [
23
+ (32, 64, 64), # L0: full latent
24
+ (16, 32, 32), # L1: regional
25
+ (8, 16, 16), # L2: native patch
26
+ (4, 8, 8), # L3: fine detail
27
+ ])
28
+ overlap: float = 0.5
29
+ confidence_threshold: float = 0.7
30
+ min_occupancy: float = 0.005
31
+ binarize_percentiles: List[float] = field(default_factory=lambda: [75, 90, 95])
32
+ n_channel_groups: int = 8
33
+ device: str = 'cuda'
34
+
35
+
36
+ @dataclass
37
+ class GeometricAnnotation:
38
+ class_name: str
39
+ class_idx: int
40
+ confidence: float
41
+ scale_level: int
42
+ location: Tuple[int, int, int]
43
+ patch_size: Tuple[int, int, int]
44
+ dimension: int = -1
45
+ is_curved: bool = False
46
+ curvature_type: str = "none"
47
+ channel_pair: Optional[Tuple[int, int]] = None
48
+ channel_group_pair: Optional[Tuple[int, int]] = None
49
+
50
+
51
+ def extract_patches_sliding(volume, patch_size, overlap=0.5):
52
+ """Extract overlapping patches from a 3D volume."""
53
+ D, H, W = volume.shape
54
+ pz, py, px = patch_size
55
+
56
+ if D < pz or H < py or W < px:
57
+ pad_d = max(pz - D, 0)
58
+ pad_h = max(py - H, 0)
59
+ pad_w = max(px - W, 0)
60
+ volume = F.pad(volume, (0, pad_w, 0, pad_h, 0, pad_d))
61
+ D, H, W = volume.shape
62
+
63
+ stride_z = max(1, int(pz * (1 - overlap)))
64
+ stride_y = max(1, int(py * (1 - overlap)))
65
+ stride_x = max(1, int(px * (1 - overlap)))
66
+
67
+ patches = []
68
+ for z in range(0, max(1, D - pz + 1), stride_z):
69
+ for y in range(0, max(1, H - py + 1), stride_y):
70
+ for x in range(0, max(1, W - px + 1), stride_x):
71
+ patch = volume[z:z+pz, y:y+py, x:x+px]
72
+ patches.append((patch, (z, y, x)))
73
+
74
+ return patches
75
+
76
+
77
+ def resize_to_canonical(patch, target=(8, 16, 16)):
78
+ """Resize 3D patch to canonical resolution via trilinear interpolation."""
79
+ x = patch.unsqueeze(0).unsqueeze(0).float()
80
+ x = F.interpolate(x, size=target, mode='trilinear', align_corners=False)
81
+ return x.squeeze(0).squeeze(0)
82
+
83
+
84
+ def binarize_continuous(patch, percentiles=[75, 90, 95]):
85
+ """Binarize continuous patch at multiple percentile thresholds."""
86
+ flat = patch.flatten()
87
+ nonzero = flat[flat.abs() > 1e-8]
88
+ if len(nonzero) < 10:
89
+ return [torch.zeros_like(patch)] * len(percentiles)
90
+ thresholds = [torch.quantile(nonzero.abs(), p / 100.0).item() for p in percentiles]
91
+ return [(patch.abs() >= t).float() for t in thresholds]
92
+
93
+
94
+ def cluster_channels(latents, n_groups=8):
95
+ """
96
+ Cluster VAE channels by correlation.
97
+ latents: (N, C, H, W) batch
98
+ Returns: (groups, corr_matrix)
99
+ """
100
+ N, C, H, W = latents.shape
101
+ flat = latents.reshape(N, C, -1)
102
+ flat = flat - flat.mean(dim=-1, keepdim=True)
103
+ norm = flat.norm(dim=-1, keepdim=True).clamp(min=1e-8)
104
+ flat = flat / norm
105
+ corr = torch.bmm(flat, flat.transpose(1, 2)).mean(dim=0)
106
+
107
+ dist = (1.0 - corr.abs()).cpu().numpy()
108
+
109
+ assigned = [-1] * C
110
+ groups = []
111
+ remaining = set(range(C))
112
+
113
+ for g in range(n_groups):
114
+ if not remaining:
115
+ break
116
+ if g == 0:
117
+ seed = min(remaining)
118
+ else:
119
+ max_dist = -1
120
+ seed = min(remaining)
121
+ for ch in remaining:
122
+ min_d = min(dist[ch, s] for grp in groups for s in grp)
123
+ if min_d > max_dist:
124
+ max_dist = min_d
125
+ seed = ch
126
+
127
+ group = [seed]
128
+ remaining.discard(seed)
129
+ assigned[seed] = g
130
+
131
+ target_size = max(1, C // n_groups)
132
+ candidates = sorted(remaining, key=lambda c: dist[seed, c])
133
+ for c in candidates[:target_size - 1]:
134
+ group.append(c)
135
+ remaining.discard(c)
136
+ assigned[c] = g
137
+
138
+ groups.append(group)
139
+
140
+ if remaining:
141
+ for ch in remaining:
142
+ best_g = 0
143
+ best_d = float('inf')
144
+ for gi, grp in enumerate(groups):
145
+ d = min(dist[ch, s] for s in grp)
146
+ if d < best_d:
147
+ best_d = d
148
+ best_g = gi
149
+ groups[best_g].append(ch)
150
+
151
+ return groups, corr
152
+
153
+
154
+ def compute_inter_group_deviances(latent, groups):
155
+ """
156
+ Compute (H, W) deviance maps between channel groups.
157
+ latent: (C, H, W), groups: list of channel index lists
158
+ Returns: list of ((group_i, group_j), deviance_map)
159
+ """
160
+ group_means = torch.stack([latent[grp].mean(dim=0) for grp in groups])
161
+ n = len(groups)
162
+ deviances = []
163
+ for i in range(n):
164
+ for j in range(i + 1, n):
165
+ dev = (group_means[i] - group_means[j]).abs()
166
+ deviances.append(((i, j), dev))
167
+ return deviances
168
+
169
+
170
+ def deviance_maps_to_3d(deviances, n_groups):
171
+ """Stack deviance maps into (n_pairs, H, W) volume."""
172
+ return torch.stack([dev for (_, dev) in deviances], dim=0)
173
+
174
+
175
+ class MultiScaleExtractor:
176
+ """
177
+ Confidence-cascaded multi-scale geometric extractor.
178
+ Uses trained PatchCrossAttentionClassifier (from Cell 2 globals).
179
+ """
180
+
181
+ def __init__(self, classifier, config=None):
182
+ self.classifier = classifier
183
+ self.config = config or ExtractionConfig()
184
+ self.classifier.eval()
185
+
186
+ @torch.no_grad()
187
+ def classify_patches(self, patches, max_batch=512):
188
+ """Classify batch of (B, 8, 16, 16) patches with chunking to avoid OOM."""
189
+ device = next(self.classifier.parameters()).device
190
+ N = patches.shape[0]
191
+
192
+ all_results = []
193
+ for start in range(0, N, max_batch):
194
+ chunk = patches[start:start+max_batch].to(device)
195
+ out = self.classifier(chunk)
196
+ probs = F.softmax(out["class_logits"], dim=-1)
197
+ max_prob, pred_class = probs.max(dim=-1)
198
+ top2 = probs.topk(2, dim=-1).values
199
+ margin = top2[:, 0] - top2[:, 1]
200
+ dim_pred = out["dim_logits"].argmax(dim=-1)
201
+ curved_pred = (out["is_curved_pred"].squeeze(-1) > 0.0)
202
+ curv_type_pred = out["curv_type_logits"].argmax(dim=-1)
203
+
204
+ all_results.append({
205
+ "pred_class": pred_class.cpu(),
206
+ "confidence": margin.cpu(),
207
+ "max_prob": max_prob.cpu(),
208
+ "dim_pred": dim_pred.cpu(),
209
+ "curved_pred": curved_pred.cpu(),
210
+ "curv_type_pred": curv_type_pred.cpu(),
211
+ "features": out["features"].cpu(),
212
+ })
213
+ del chunk, out, probs
214
+ torch.cuda.empty_cache()
215
+
216
+ return {k: torch.cat([r[k] for r in all_results], dim=0)
217
+ for k in all_results[0]}
218
+
219
+ def extract_from_volume(self, volume, min_confidence=None):
220
+ """
221
+ Extract annotations via confidence cascade.
222
+ volume: (D, H, W) continuous or binary tensor
223
+ """
224
+ conf_thresh = min_confidence or self.config.confidence_threshold
225
+ annotations = []
226
+
227
+ regions_to_process = [(0, 0, 0, 0,
228
+ volume.shape[0], volume.shape[1], volume.shape[2])]
229
+
230
+ for level, scale in enumerate(self.config.scales):
231
+ if not regions_to_process:
232
+ break
233
+
234
+ next_regions = []
235
+ pz, py, px = scale
236
+
237
+ all_patches = []
238
+
239
+ for ridx, (lvl, rz0, ry0, rx0, rz1, ry1, rx1) in enumerate(regions_to_process):
240
+ if lvl != level:
241
+ next_regions.append((lvl, rz0, ry0, rx0, rz1, ry1, rx1))
242
+ continue
243
+
244
+ subvol = volume[rz0:rz1, ry0:ry1, rx0:rx1]
245
+ patches = extract_patches_sliding(subvol, (pz, py, px), self.config.overlap)
246
+
247
+ for patch, (lz, ly, lx) in patches:
248
+ binary_patches = binarize_continuous(patch, self.config.binarize_percentiles)
249
+ for bp in binary_patches:
250
+ occ = bp.mean().item()
251
+ if occ < self.config.min_occupancy:
252
+ continue
253
+ canonical = resize_to_canonical(bp, self.config.canonical_shape)
254
+ all_patches.append((
255
+ canonical,
256
+ (rz0 + lz, ry0 + ly, rx0 + lx),
257
+ ridx, scale))
258
+
259
+ if not all_patches:
260
+ regions_to_process = next_regions
261
+ continue
262
+
263
+ # Batch classify
264
+ batch = torch.stack([p[0] for p in all_patches])
265
+ results = self.classify_patches(batch)
266
+
267
+ for i, (_, loc, ridx, sc) in enumerate(all_patches):
268
+ conf = results["confidence"][i].item()
269
+ cls_idx = results["pred_class"][i].item()
270
+
271
+ if conf >= conf_thresh:
272
+ ann = GeometricAnnotation(
273
+ class_name=CLASS_NAMES[cls_idx],
274
+ class_idx=cls_idx,
275
+ confidence=conf,
276
+ scale_level=level,
277
+ location=loc,
278
+ patch_size=sc,
279
+ dimension=results["dim_pred"][i].item(),
280
+ is_curved=bool(results["curved_pred"][i].item()),
281
+ curvature_type=CURVATURE_NAMES[results["curv_type_pred"][i].item()],
282
+ )
283
+ annotations.append(ann)
284
+ elif level < len(self.config.scales) - 1:
285
+ z0, y0, x0 = loc
286
+ next_regions.append((
287
+ level + 1,
288
+ z0, y0, x0,
289
+ min(z0 + sc[0], volume.shape[0]),
290
+ min(y0 + sc[1], volume.shape[1]),
291
+ min(x0 + sc[2], volume.shape[2]),
292
+ ))
293
+
294
+ regions_to_process = next_regions
295
+
296
+ return annotations
297
+
298
+ def extract_from_latent(self, latent, channel_groups=None):
299
+ """
300
+ Full extraction for a single Flux 2 VAE latent.
301
+ latent: (C, H, W) tensor
302
+ """
303
+ raw_annotations = self.extract_from_volume(latent)
304
+
305
+ deviance_annotations = []
306
+ if channel_groups is not None:
307
+ deviances = compute_inter_group_deviances(latent, channel_groups)
308
+ dev_volume = deviance_maps_to_3d(deviances, len(channel_groups))
309
+ deviance_annotations = self.extract_from_volume(dev_volume)
310
+ for ann in deviance_annotations:
311
+ pair_idx = ann.location[0]
312
+ if pair_idx < len(deviances):
313
+ ann.channel_group_pair = deviances[pair_idx][0]
314
+
315
+ return {
316
+ 'raw_annotations': raw_annotations,
317
+ 'deviance_annotations': deviance_annotations,
318
+ 'n_raw': len(raw_annotations),
319
+ 'n_deviance': len(deviance_annotations),
320
+ }
321
+
322
+
323
+ print("✓ Cell 4: Extraction pipeline defined (PatchCrossAttention)")
324
+ print(f" Scales: {ExtractionConfig().scales}")
325
+ print(f" Canonical: {ExtractionConfig().canonical_shape}")