ManmohanSharma commited on
Commit
d964245
·
verified ·
1 Parent(s): d6d4411

Upload code/data.py

Browse files
Files changed (1) hide show
  1. code/data.py +407 -0
code/data.py ADDED
@@ -0,0 +1,407 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import math
4
+ import os
5
+ import random
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+ from typing import Any, Iterable
9
+
10
+ import numpy as np
11
+ import torch
12
+ from datasets import load_dataset
13
+ from torch.utils.data import Dataset
14
+
15
+
16
+ REQUIRED_SPECTRUM_KEYS = ("flux", "ivar", "lambda", "mask")
17
+
18
+
19
+ @dataclass
20
+ class SampleStats:
21
+ n_samples: int
22
+ min_len: int
23
+ max_len: int
24
+ median_len: float
25
+ min_lambda: float
26
+ max_lambda: float
27
+ valid_fraction: float
28
+ z_min: float
29
+ z_max: float
30
+ z_median: float
31
+ zwarn_fraction: float
32
+
33
+
34
+ def _as_array(value: Any, dtype: np.dtype) -> np.ndarray:
35
+ arr = np.asarray(value, dtype=dtype)
36
+ return np.ascontiguousarray(arr)
37
+
38
+
39
+ def parse_mmu_example(example: dict[str, Any]) -> dict[str, Any] | None:
40
+ spectrum = example.get("spectrum")
41
+ if not isinstance(spectrum, dict):
42
+ return None
43
+ if any(key not in spectrum for key in REQUIRED_SPECTRUM_KEYS):
44
+ return None
45
+
46
+ flux = _as_array(spectrum["flux"], np.float32)
47
+ ivar = _as_array(spectrum["ivar"], np.float32)
48
+ lam = _as_array(spectrum["lambda"], np.float32)
49
+ bad_mask = _as_array(spectrum["mask"], np.bool_)
50
+ if len(flux) == 0 or not (len(flux) == len(ivar) == len(lam) == len(bad_mask)):
51
+ return None
52
+
53
+ lsf = spectrum.get("lsf_sigma")
54
+ lsf_sigma = _as_array(lsf, np.float32) if lsf is not None and len(lsf) == len(flux) else np.zeros_like(flux)
55
+
56
+ z = float(example.get("Z", np.nan))
57
+ zerr = float(example.get("ZERR", np.nan))
58
+ zwarn = bool(example.get("ZWARN", True))
59
+ if not math.isfinite(z) or z < -0.001:
60
+ return None
61
+
62
+ return {
63
+ "flux": flux,
64
+ "ivar": ivar,
65
+ "lambda": lam,
66
+ "bad_mask": bad_mask,
67
+ "lsf_sigma": lsf_sigma,
68
+ "z": np.float32(max(z, 0.0)),
69
+ "zerr": np.float32(zerr if math.isfinite(zerr) else np.nan),
70
+ "zwarn": zwarn,
71
+ "object_id": str(example.get("object_id", "")),
72
+ }
73
+
74
+
75
+ def collect_mmu_desi(
76
+ cache_file: str | os.PathLike[str],
77
+ max_samples: int,
78
+ dataset_name: str = "MultimodalUniverse/desi",
79
+ split: str = "train",
80
+ seed: int = 17,
81
+ hf_cache_dir: str | None = None,
82
+ refresh: bool = False,
83
+ ) -> list[dict[str, Any]]:
84
+ cache_path = Path(cache_file)
85
+ if cache_path.exists() and not refresh:
86
+ payload = torch.load(cache_path, map_location="cpu", weights_only=False)
87
+ samples = payload["samples"] if isinstance(payload, dict) and "samples" in payload else payload
88
+ if len(samples) >= max_samples:
89
+ return samples[:max_samples]
90
+
91
+ cache_path.parent.mkdir(parents=True, exist_ok=True)
92
+ ds = load_dataset(dataset_name, split=split, streaming=True, cache_dir=hf_cache_dir)
93
+ shuffle_buffer = min(max_samples, 10_000)
94
+ print(f"COLLECT_SHUFFLE_BUFFER {shuffle_buffer}", flush=True)
95
+ ds = ds.shuffle(seed=seed, buffer_size=shuffle_buffer)
96
+ samples: list[dict[str, Any]] = []
97
+ # Deterministic shard order is fine for smoke runs; shuffle after collection.
98
+ for example in ds:
99
+ parsed = parse_mmu_example(example)
100
+ if parsed is None:
101
+ continue
102
+ samples.append(parsed)
103
+ if len(samples) % 4096 == 0:
104
+ print(f"COLLECTED_SAMPLES {len(samples)}/{max_samples}", flush=True)
105
+ if len(samples) >= max_samples:
106
+ break
107
+ rng = random.Random(seed)
108
+ rng.shuffle(samples)
109
+ torch.save({"samples": samples, "dataset_name": dataset_name, "split": split}, cache_path)
110
+ return samples
111
+
112
+
113
+ def compute_sample_stats(samples: Iterable[dict[str, Any]]) -> SampleStats:
114
+ samples = list(samples)
115
+ lengths = np.asarray([len(s["flux"]) for s in samples], dtype=np.int64)
116
+ mins = np.asarray([np.nanmin(s["lambda"]) for s in samples], dtype=np.float32)
117
+ maxs = np.asarray([np.nanmax(s["lambda"]) for s in samples], dtype=np.float32)
118
+ z = np.asarray([s["z"] for s in samples], dtype=np.float32)
119
+ zwarn = np.asarray([s["zwarn"] for s in samples], dtype=np.bool_)
120
+ valid_fracs = []
121
+ for s in samples:
122
+ valid = valid_pixel_mask(s)
123
+ valid_fracs.append(float(valid.mean()) if len(valid) else 0.0)
124
+ return SampleStats(
125
+ n_samples=len(samples),
126
+ min_len=int(lengths.min()),
127
+ max_len=int(lengths.max()),
128
+ median_len=float(np.median(lengths)),
129
+ min_lambda=float(np.nanmin(mins)),
130
+ max_lambda=float(np.nanmax(maxs)),
131
+ valid_fraction=float(np.mean(valid_fracs)),
132
+ z_min=float(np.nanmin(z)),
133
+ z_max=float(np.nanmax(z)),
134
+ z_median=float(np.nanmedian(z)),
135
+ zwarn_fraction=float(np.mean(zwarn)),
136
+ )
137
+
138
+
139
+ def valid_pixel_mask(sample: dict[str, Any]) -> np.ndarray:
140
+ flux = sample["flux"]
141
+ ivar = sample["ivar"]
142
+ lam = sample["lambda"]
143
+ bad = sample["bad_mask"]
144
+ return np.isfinite(flux) & np.isfinite(ivar) & np.isfinite(lam) & (ivar > 0) & (~bad)
145
+
146
+
147
+ class SpectraListDataset(Dataset):
148
+ def __init__(self, samples: list[dict[str, Any]], indices: np.ndarray):
149
+ self.samples = samples
150
+ self.indices = np.asarray(indices, dtype=np.int64)
151
+
152
+ def __len__(self) -> int:
153
+ return int(len(self.indices))
154
+
155
+ def __getitem__(self, idx: int) -> dict[str, Any]:
156
+ return self.samples[int(self.indices[idx])]
157
+
158
+
159
+ def split_indices(n: int, val_fraction: float = 0.15, seed: int = 17) -> tuple[np.ndarray, np.ndarray]:
160
+ rng = np.random.default_rng(seed)
161
+ perm = rng.permutation(n)
162
+ n_val = max(1, int(round(n * val_fraction)))
163
+ return perm[n_val:], perm[:n_val]
164
+
165
+
166
+ @dataclass
167
+ class CollatorConfig:
168
+ num_patches: int = 256
169
+ random_mask_ratio: float = 0.25
170
+ span_mask_prob: float = 0.65
171
+ line_mask_prob: float = 0.45
172
+ arm_dropout_prob: float = 0.25
173
+ min_scale: float = 1e-3
174
+ max_mask_ratio: float = 0.70
175
+ augment_ood: bool = False
176
+
177
+
178
+ class SpectraCollator:
179
+ def __init__(self, cfg: CollatorConfig, train: bool = True, seed: int = 17):
180
+ self.cfg = cfg
181
+ self.train = train
182
+ self.rng = np.random.default_rng(seed)
183
+
184
+ def __call__(self, samples: list[dict[str, Any]]) -> dict[str, torch.Tensor]:
185
+ patched = [self._patch_sample(s) for s in samples]
186
+ bsz = len(patched)
187
+ p = self.cfg.num_patches
188
+ fdim = patched[0]["enc_features"].shape[-1]
189
+ max_visible = max(x["enc_features"].shape[0] for x in patched)
190
+
191
+ enc_features = np.zeros((bsz, max_visible, fdim), dtype=np.float32)
192
+ enc_loglam = np.zeros((bsz, max_visible), dtype=np.float32)
193
+ enc_padding = np.ones((bsz, max_visible), dtype=np.bool_)
194
+
195
+ target_loglam = np.zeros((bsz, p), dtype=np.float32)
196
+ target_aux = np.zeros((bsz, p, 4), dtype=np.float32)
197
+ target_flux = np.zeros((bsz, p), dtype=np.float32)
198
+ loss_mask = np.zeros((bsz, p), dtype=np.bool_)
199
+ valid_patch = np.zeros((bsz, p), dtype=np.bool_)
200
+ corrupt_patch = np.zeros((bsz, p), dtype=np.bool_)
201
+ line_weight = np.ones((bsz, p), dtype=np.float32)
202
+ z = np.zeros((bsz,), dtype=np.float32)
203
+ y = np.zeros((bsz,), dtype=np.float32)
204
+ zwarn = np.zeros((bsz,), dtype=np.bool_)
205
+
206
+ for i, item in enumerate(patched):
207
+ nvis = item["enc_features"].shape[0]
208
+ enc_features[i, :nvis] = item["enc_features"]
209
+ enc_loglam[i, :nvis] = item["enc_loglam"]
210
+ enc_padding[i, :nvis] = False
211
+ target_loglam[i] = item["target_loglam"]
212
+ target_aux[i] = item["target_aux"]
213
+ target_flux[i] = item["target_flux"]
214
+ loss_mask[i] = item["loss_mask"]
215
+ valid_patch[i] = item["valid_patch"]
216
+ corrupt_patch[i] = item["corrupt_patch"]
217
+ line_weight[i] = item["line_weight"]
218
+ z[i] = item["z"]
219
+ y[i] = math.log1p(float(item["z"]))
220
+ zwarn[i] = item["zwarn"]
221
+
222
+ return {
223
+ "enc_features": torch.from_numpy(enc_features),
224
+ "enc_loglam": torch.from_numpy(enc_loglam),
225
+ "enc_padding": torch.from_numpy(enc_padding),
226
+ "target_loglam": torch.from_numpy(target_loglam),
227
+ "target_aux": torch.from_numpy(target_aux),
228
+ "target_flux": torch.from_numpy(target_flux),
229
+ "loss_mask": torch.from_numpy(loss_mask),
230
+ "valid_patch": torch.from_numpy(valid_patch),
231
+ "corrupt_patch": torch.from_numpy(corrupt_patch),
232
+ "line_weight": torch.from_numpy(line_weight),
233
+ "z": torch.from_numpy(z),
234
+ "y": torch.from_numpy(y),
235
+ "zwarn": torch.from_numpy(zwarn),
236
+ }
237
+
238
+ def _patch_sample(self, sample: dict[str, Any]) -> dict[str, Any]:
239
+ sample = self._maybe_augment(sample)
240
+ flux = sample["flux"]
241
+ ivar = sample["ivar"]
242
+ lam = sample["lambda"]
243
+ lsf = sample["lsf_sigma"]
244
+ valid = valid_pixel_mask(sample)
245
+ n = len(flux)
246
+ p = self.cfg.num_patches
247
+ edges = np.linspace(0, n, p + 1, dtype=np.int64)
248
+
249
+ patch_id = np.searchsorted(edges[1:-1], np.arange(n), side="right")
250
+ patch_valid = np.zeros(p, dtype=np.bool_)
251
+ target_loglam = np.zeros(p, dtype=np.float32)
252
+ target_aux = np.zeros((p, 4), dtype=np.float32)
253
+ raw_patch_flux = np.zeros(p, dtype=np.float32)
254
+ raw_patch_ivar = np.zeros(p, dtype=np.float32)
255
+ raw_patch_lsf = np.zeros(p, dtype=np.float32)
256
+ valid_frac = np.zeros(p, dtype=np.float32)
257
+
258
+ safe_loglam = np.log(np.clip(lam.astype(np.float64), 1.0, None)).astype(np.float32)
259
+ for j in range(p):
260
+ lo, hi = int(edges[j]), int(edges[j + 1])
261
+ idx = np.arange(lo, hi)
262
+ if len(idx) == 0:
263
+ continue
264
+ v = valid[idx]
265
+ valid_frac[j] = float(v.mean())
266
+ patch_valid[j] = bool(v.sum() >= max(2, int(0.2 * len(idx))))
267
+ target_loglam[j] = float(np.nanmedian(safe_loglam[idx]))
268
+ width = float(np.nanmax(safe_loglam[idx]) - np.nanmin(safe_loglam[idx])) if len(idx) > 1 else 0.0
269
+ if v.any():
270
+ raw_patch_flux[j] = float(np.nanmedian(flux[idx][v]))
271
+ raw_patch_ivar[j] = float(np.nanmedian(ivar[idx][v]))
272
+ raw_patch_lsf[j] = float(np.nanmedian(lsf[idx][v]))
273
+ target_aux[j] = [valid_frac[j], math.log1p(max(raw_patch_ivar[j], 0.0)), raw_patch_lsf[j], width]
274
+
275
+ corrupt = self._sample_corruption(raw_patch_flux, patch_valid)
276
+ pixel_visible = valid & (~corrupt[patch_id])
277
+ if pixel_visible.sum() < 16:
278
+ pixel_visible = valid
279
+
280
+ center = float(np.nanmedian(flux[pixel_visible])) if pixel_visible.any() else 0.0
281
+ abs_dev = np.abs(flux[pixel_visible] - center) if pixel_visible.any() else np.asarray([1.0], dtype=np.float32)
282
+ scale = float(np.nanmedian(abs_dev) * 1.4826)
283
+ if not math.isfinite(scale) or scale < self.cfg.min_scale:
284
+ scale = max(float(np.nanmedian(np.abs(flux[valid]))) if valid.any() else 1.0, self.cfg.min_scale)
285
+
286
+ norm_patch_flux = np.arcsinh((raw_patch_flux - center) / scale).astype(np.float32)
287
+ norm_ivar = np.log1p(np.maximum(raw_patch_ivar * scale * scale, 0.0)).astype(np.float32)
288
+ line_weight = self._line_weights(norm_patch_flux, patch_valid)
289
+
290
+ visible_patch = patch_valid & (~corrupt)
291
+ # Encoder never receives masked target flux. It receives visible patches only.
292
+ enc_idx = np.where(visible_patch)[0]
293
+ if len(enc_idx) == 0:
294
+ enc_idx = np.where(patch_valid)[0][:1]
295
+
296
+ enc_features = np.stack(
297
+ [
298
+ norm_patch_flux[enc_idx],
299
+ norm_ivar[enc_idx],
300
+ valid_frac[enc_idx],
301
+ raw_patch_lsf[enc_idx],
302
+ np.zeros(len(enc_idx), dtype=np.float32),
303
+ target_aux[enc_idx, 3],
304
+ ],
305
+ axis=-1,
306
+ ).astype(np.float32)
307
+
308
+ loss_mask = patch_valid & corrupt
309
+ return {
310
+ "enc_features": enc_features,
311
+ "enc_loglam": target_loglam[enc_idx],
312
+ "target_loglam": target_loglam,
313
+ "target_aux": target_aux.astype(np.float32),
314
+ "target_flux": norm_patch_flux,
315
+ "loss_mask": loss_mask,
316
+ "valid_patch": patch_valid,
317
+ "corrupt_patch": corrupt,
318
+ "line_weight": line_weight,
319
+ "z": sample["z"],
320
+ "zwarn": sample["zwarn"],
321
+ }
322
+
323
+ def _sample_corruption(self, patch_flux: np.ndarray, valid_patch: np.ndarray) -> np.ndarray:
324
+ p = len(valid_patch)
325
+ corrupt = np.zeros(p, dtype=np.bool_)
326
+ valid_idx = np.where(valid_patch)[0]
327
+ if len(valid_idx) == 0:
328
+ return corrupt
329
+
330
+ ratio = self.cfg.random_mask_ratio if self.train else 0.35
331
+ if ratio <= 0 and self.cfg.span_mask_prob <= 0 and self.cfg.line_mask_prob <= 0 and self.cfg.arm_dropout_prob <= 0:
332
+ return corrupt
333
+ n_rand = max(1, int(round(len(valid_idx) * ratio)))
334
+ corrupt[self.rng.choice(valid_idx, size=min(n_rand, len(valid_idx)), replace=False)] = True
335
+
336
+ if self.train and self.rng.random() < self.cfg.span_mask_prob:
337
+ for _ in range(int(self.rng.integers(1, 4))):
338
+ width = int(self.rng.integers(max(3, p // 80), max(4, p // 18)))
339
+ start = int(self.rng.integers(0, max(1, p - width)))
340
+ corrupt[start : start + width] |= valid_patch[start : start + width]
341
+
342
+ if self.train and self.rng.random() < self.cfg.arm_dropout_prob:
343
+ arm = int(self.rng.integers(0, 4))
344
+ if arm == 0:
345
+ sl = slice(0, p // 3)
346
+ elif arm == 1:
347
+ sl = slice(p // 3, 2 * p // 3)
348
+ elif arm == 2:
349
+ sl = slice(2 * p // 3, p)
350
+ else:
351
+ lo = int(self.rng.integers(p // 5, p // 2))
352
+ hi = min(p, lo + int(self.rng.integers(p // 12, p // 5)))
353
+ sl = slice(lo, hi)
354
+ corrupt[sl] |= valid_patch[sl]
355
+
356
+ if self.train and self.rng.random() < self.cfg.line_mask_prob:
357
+ score = np.zeros(p, dtype=np.float32)
358
+ good = valid_patch & np.isfinite(patch_flux)
359
+ if good.sum() > 4:
360
+ grad = np.abs(np.gradient(np.nan_to_num(patch_flux, nan=0.0)))
361
+ score[good] = grad[good]
362
+ top = np.argsort(score)[-max(2, p // 24) :]
363
+ for j in top:
364
+ lo, hi = max(0, j - 1), min(p, j + 2)
365
+ corrupt[lo:hi] |= valid_patch[lo:hi]
366
+
367
+ max_allowed = max(1, int(valid_patch.sum() * self.cfg.max_mask_ratio))
368
+ cur = np.where(corrupt & valid_patch)[0]
369
+ if len(cur) > max_allowed:
370
+ keep = self.rng.choice(cur, size=max_allowed, replace=False)
371
+ next_corrupt = np.zeros_like(corrupt)
372
+ next_corrupt[keep] = True
373
+ corrupt = next_corrupt
374
+ return corrupt & valid_patch
375
+
376
+ def _line_weights(self, patch_flux: np.ndarray, valid_patch: np.ndarray) -> np.ndarray:
377
+ w = np.ones_like(patch_flux, dtype=np.float32)
378
+ if valid_patch.sum() < 4:
379
+ return w
380
+ grad = np.abs(np.gradient(np.nan_to_num(patch_flux, nan=0.0))).astype(np.float32)
381
+ scale = np.percentile(grad[valid_patch], 90) if valid_patch.any() else 1.0
382
+ if scale > 0:
383
+ w += 2.0 * np.clip(grad / scale, 0.0, 2.0)
384
+ w[~valid_patch] = 1.0
385
+ return np.clip(w, 1.0, 5.0)
386
+
387
+ def _maybe_augment(self, sample: dict[str, Any]) -> dict[str, Any]:
388
+ if not (self.train and self.cfg.augment_ood):
389
+ return sample
390
+ out = {k: v for k, v in sample.items()}
391
+ bad = np.array(sample["bad_mask"], copy=True)
392
+ lam = sample["lambda"]
393
+ n = len(lam)
394
+ if self.rng.random() < 0.45:
395
+ frac = float(self.rng.uniform(0.65, 0.95))
396
+ width = max(32, int(n * frac))
397
+ start = int(self.rng.integers(0, max(1, n - width)))
398
+ keep = np.zeros(n, dtype=np.bool_)
399
+ keep[start : start + width] = True
400
+ bad |= ~keep
401
+ if self.rng.random() < 0.30:
402
+ for _ in range(int(self.rng.integers(1, 4))):
403
+ width = int(self.rng.integers(max(8, n // 200), max(12, n // 45)))
404
+ start = int(self.rng.integers(0, max(1, n - width)))
405
+ bad[start : start + width] = True
406
+ out["bad_mask"] = bad
407
+ return out