from __future__ import annotations import math import os import random from dataclasses import dataclass from pathlib import Path from typing import Any, Iterable import numpy as np import torch from datasets import load_dataset from torch.utils.data import Dataset REQUIRED_SPECTRUM_KEYS = ("flux", "ivar", "lambda", "mask") @dataclass class SampleStats: n_samples: int min_len: int max_len: int median_len: float min_lambda: float max_lambda: float valid_fraction: float z_min: float z_max: float z_median: float zwarn_fraction: float def _as_array(value: Any, dtype: np.dtype) -> np.ndarray: arr = np.asarray(value, dtype=dtype) return np.ascontiguousarray(arr) def parse_mmu_example(example: dict[str, Any]) -> dict[str, Any] | None: spectrum = example.get("spectrum") if not isinstance(spectrum, dict): return None if any(key not in spectrum for key in REQUIRED_SPECTRUM_KEYS): return None flux = _as_array(spectrum["flux"], np.float32) ivar = _as_array(spectrum["ivar"], np.float32) lam = _as_array(spectrum["lambda"], np.float32) bad_mask = _as_array(spectrum["mask"], np.bool_) if len(flux) == 0 or not (len(flux) == len(ivar) == len(lam) == len(bad_mask)): return None lsf = spectrum.get("lsf_sigma") lsf_sigma = _as_array(lsf, np.float32) if lsf is not None and len(lsf) == len(flux) else np.zeros_like(flux) z = float(example.get("Z", np.nan)) zerr = float(example.get("ZERR", np.nan)) zwarn = bool(example.get("ZWARN", True)) if not math.isfinite(z) or z < -0.001: return None return { "flux": flux, "ivar": ivar, "lambda": lam, "bad_mask": bad_mask, "lsf_sigma": lsf_sigma, "z": np.float32(max(z, 0.0)), "zerr": np.float32(zerr if math.isfinite(zerr) else np.nan), "zwarn": zwarn, "object_id": str(example.get("object_id", "")), } def collect_mmu_desi( cache_file: str | os.PathLike[str], max_samples: int, dataset_name: str = "MultimodalUniverse/desi", split: str = "train", seed: int = 17, hf_cache_dir: str | None = None, refresh: bool = False, ) -> list[dict[str, Any]]: cache_path = Path(cache_file) if cache_path.exists() and not refresh: payload = torch.load(cache_path, map_location="cpu", weights_only=False) samples = payload["samples"] if isinstance(payload, dict) and "samples" in payload else payload if len(samples) >= max_samples: return samples[:max_samples] cache_path.parent.mkdir(parents=True, exist_ok=True) ds = load_dataset(dataset_name, split=split, streaming=True, cache_dir=hf_cache_dir) shuffle_buffer = min(max_samples, 10_000) print(f"COLLECT_SHUFFLE_BUFFER {shuffle_buffer}", flush=True) ds = ds.shuffle(seed=seed, buffer_size=shuffle_buffer) samples: list[dict[str, Any]] = [] # Deterministic shard order is fine for smoke runs; shuffle after collection. for example in ds: parsed = parse_mmu_example(example) if parsed is None: continue samples.append(parsed) if len(samples) % 4096 == 0: print(f"COLLECTED_SAMPLES {len(samples)}/{max_samples}", flush=True) if len(samples) >= max_samples: break rng = random.Random(seed) rng.shuffle(samples) torch.save({"samples": samples, "dataset_name": dataset_name, "split": split}, cache_path) return samples def compute_sample_stats(samples: Iterable[dict[str, Any]]) -> SampleStats: samples = list(samples) lengths = np.asarray([len(s["flux"]) for s in samples], dtype=np.int64) mins = np.asarray([np.nanmin(s["lambda"]) for s in samples], dtype=np.float32) maxs = np.asarray([np.nanmax(s["lambda"]) for s in samples], dtype=np.float32) z = np.asarray([s["z"] for s in samples], dtype=np.float32) zwarn = np.asarray([s["zwarn"] for s in samples], dtype=np.bool_) valid_fracs = [] for s in samples: valid = valid_pixel_mask(s) valid_fracs.append(float(valid.mean()) if len(valid) else 0.0) return SampleStats( n_samples=len(samples), min_len=int(lengths.min()), max_len=int(lengths.max()), median_len=float(np.median(lengths)), min_lambda=float(np.nanmin(mins)), max_lambda=float(np.nanmax(maxs)), valid_fraction=float(np.mean(valid_fracs)), z_min=float(np.nanmin(z)), z_max=float(np.nanmax(z)), z_median=float(np.nanmedian(z)), zwarn_fraction=float(np.mean(zwarn)), ) def valid_pixel_mask(sample: dict[str, Any]) -> np.ndarray: flux = sample["flux"] ivar = sample["ivar"] lam = sample["lambda"] bad = sample["bad_mask"] return np.isfinite(flux) & np.isfinite(ivar) & np.isfinite(lam) & (ivar > 0) & (~bad) class SpectraListDataset(Dataset): def __init__(self, samples: list[dict[str, Any]], indices: np.ndarray): self.samples = samples self.indices = np.asarray(indices, dtype=np.int64) def __len__(self) -> int: return int(len(self.indices)) def __getitem__(self, idx: int) -> dict[str, Any]: return self.samples[int(self.indices[idx])] def split_indices(n: int, val_fraction: float = 0.15, seed: int = 17) -> tuple[np.ndarray, np.ndarray]: rng = np.random.default_rng(seed) perm = rng.permutation(n) n_val = max(1, int(round(n * val_fraction))) return perm[n_val:], perm[:n_val] @dataclass class CollatorConfig: num_patches: int = 256 random_mask_ratio: float = 0.25 span_mask_prob: float = 0.65 line_mask_prob: float = 0.45 arm_dropout_prob: float = 0.25 min_scale: float = 1e-3 max_mask_ratio: float = 0.70 augment_ood: bool = False class SpectraCollator: def __init__(self, cfg: CollatorConfig, train: bool = True, seed: int = 17): self.cfg = cfg self.train = train self.rng = np.random.default_rng(seed) def __call__(self, samples: list[dict[str, Any]]) -> dict[str, torch.Tensor]: patched = [self._patch_sample(s) for s in samples] bsz = len(patched) p = self.cfg.num_patches fdim = patched[0]["enc_features"].shape[-1] max_visible = max(x["enc_features"].shape[0] for x in patched) enc_features = np.zeros((bsz, max_visible, fdim), dtype=np.float32) enc_loglam = np.zeros((bsz, max_visible), dtype=np.float32) enc_padding = np.ones((bsz, max_visible), dtype=np.bool_) target_loglam = np.zeros((bsz, p), dtype=np.float32) target_aux = np.zeros((bsz, p, 4), dtype=np.float32) target_flux = np.zeros((bsz, p), dtype=np.float32) loss_mask = np.zeros((bsz, p), dtype=np.bool_) valid_patch = np.zeros((bsz, p), dtype=np.bool_) corrupt_patch = np.zeros((bsz, p), dtype=np.bool_) line_weight = np.ones((bsz, p), dtype=np.float32) z = np.zeros((bsz,), dtype=np.float32) y = np.zeros((bsz,), dtype=np.float32) zwarn = np.zeros((bsz,), dtype=np.bool_) for i, item in enumerate(patched): nvis = item["enc_features"].shape[0] enc_features[i, :nvis] = item["enc_features"] enc_loglam[i, :nvis] = item["enc_loglam"] enc_padding[i, :nvis] = False target_loglam[i] = item["target_loglam"] target_aux[i] = item["target_aux"] target_flux[i] = item["target_flux"] loss_mask[i] = item["loss_mask"] valid_patch[i] = item["valid_patch"] corrupt_patch[i] = item["corrupt_patch"] line_weight[i] = item["line_weight"] z[i] = item["z"] y[i] = math.log1p(float(item["z"])) zwarn[i] = item["zwarn"] return { "enc_features": torch.from_numpy(enc_features), "enc_loglam": torch.from_numpy(enc_loglam), "enc_padding": torch.from_numpy(enc_padding), "target_loglam": torch.from_numpy(target_loglam), "target_aux": torch.from_numpy(target_aux), "target_flux": torch.from_numpy(target_flux), "loss_mask": torch.from_numpy(loss_mask), "valid_patch": torch.from_numpy(valid_patch), "corrupt_patch": torch.from_numpy(corrupt_patch), "line_weight": torch.from_numpy(line_weight), "z": torch.from_numpy(z), "y": torch.from_numpy(y), "zwarn": torch.from_numpy(zwarn), } def _patch_sample(self, sample: dict[str, Any]) -> dict[str, Any]: sample = self._maybe_augment(sample) flux = sample["flux"] ivar = sample["ivar"] lam = sample["lambda"] lsf = sample["lsf_sigma"] valid = valid_pixel_mask(sample) n = len(flux) p = self.cfg.num_patches edges = np.linspace(0, n, p + 1, dtype=np.int64) patch_id = np.searchsorted(edges[1:-1], np.arange(n), side="right") patch_valid = np.zeros(p, dtype=np.bool_) target_loglam = np.zeros(p, dtype=np.float32) target_aux = np.zeros((p, 4), dtype=np.float32) raw_patch_flux = np.zeros(p, dtype=np.float32) raw_patch_ivar = np.zeros(p, dtype=np.float32) raw_patch_lsf = np.zeros(p, dtype=np.float32) valid_frac = np.zeros(p, dtype=np.float32) safe_loglam = np.log(np.clip(lam.astype(np.float64), 1.0, None)).astype(np.float32) for j in range(p): lo, hi = int(edges[j]), int(edges[j + 1]) idx = np.arange(lo, hi) if len(idx) == 0: continue v = valid[idx] valid_frac[j] = float(v.mean()) patch_valid[j] = bool(v.sum() >= max(2, int(0.2 * len(idx)))) target_loglam[j] = float(np.nanmedian(safe_loglam[idx])) width = float(np.nanmax(safe_loglam[idx]) - np.nanmin(safe_loglam[idx])) if len(idx) > 1 else 0.0 if v.any(): raw_patch_flux[j] = float(np.nanmedian(flux[idx][v])) raw_patch_ivar[j] = float(np.nanmedian(ivar[idx][v])) raw_patch_lsf[j] = float(np.nanmedian(lsf[idx][v])) target_aux[j] = [valid_frac[j], math.log1p(max(raw_patch_ivar[j], 0.0)), raw_patch_lsf[j], width] corrupt = self._sample_corruption(raw_patch_flux, patch_valid) pixel_visible = valid & (~corrupt[patch_id]) if pixel_visible.sum() < 16: pixel_visible = valid center = float(np.nanmedian(flux[pixel_visible])) if pixel_visible.any() else 0.0 abs_dev = np.abs(flux[pixel_visible] - center) if pixel_visible.any() else np.asarray([1.0], dtype=np.float32) scale = float(np.nanmedian(abs_dev) * 1.4826) if not math.isfinite(scale) or scale < self.cfg.min_scale: scale = max(float(np.nanmedian(np.abs(flux[valid]))) if valid.any() else 1.0, self.cfg.min_scale) norm_patch_flux = np.arcsinh((raw_patch_flux - center) / scale).astype(np.float32) norm_ivar = np.log1p(np.maximum(raw_patch_ivar * scale * scale, 0.0)).astype(np.float32) line_weight = self._line_weights(norm_patch_flux, patch_valid) visible_patch = patch_valid & (~corrupt) # Encoder never receives masked target flux. It receives visible patches only. enc_idx = np.where(visible_patch)[0] if len(enc_idx) == 0: enc_idx = np.where(patch_valid)[0][:1] enc_features = np.stack( [ norm_patch_flux[enc_idx], norm_ivar[enc_idx], valid_frac[enc_idx], raw_patch_lsf[enc_idx], np.zeros(len(enc_idx), dtype=np.float32), target_aux[enc_idx, 3], ], axis=-1, ).astype(np.float32) loss_mask = patch_valid & corrupt return { "enc_features": enc_features, "enc_loglam": target_loglam[enc_idx], "target_loglam": target_loglam, "target_aux": target_aux.astype(np.float32), "target_flux": norm_patch_flux, "loss_mask": loss_mask, "valid_patch": patch_valid, "corrupt_patch": corrupt, "line_weight": line_weight, "z": sample["z"], "zwarn": sample["zwarn"], } def _sample_corruption(self, patch_flux: np.ndarray, valid_patch: np.ndarray) -> np.ndarray: p = len(valid_patch) corrupt = np.zeros(p, dtype=np.bool_) valid_idx = np.where(valid_patch)[0] if len(valid_idx) == 0: return corrupt ratio = self.cfg.random_mask_ratio if self.train else 0.35 if ratio <= 0 and self.cfg.span_mask_prob <= 0 and self.cfg.line_mask_prob <= 0 and self.cfg.arm_dropout_prob <= 0: return corrupt n_rand = max(1, int(round(len(valid_idx) * ratio))) corrupt[self.rng.choice(valid_idx, size=min(n_rand, len(valid_idx)), replace=False)] = True if self.train and self.rng.random() < self.cfg.span_mask_prob: for _ in range(int(self.rng.integers(1, 4))): width = int(self.rng.integers(max(3, p // 80), max(4, p // 18))) start = int(self.rng.integers(0, max(1, p - width))) corrupt[start : start + width] |= valid_patch[start : start + width] if self.train and self.rng.random() < self.cfg.arm_dropout_prob: arm = int(self.rng.integers(0, 4)) if arm == 0: sl = slice(0, p // 3) elif arm == 1: sl = slice(p // 3, 2 * p // 3) elif arm == 2: sl = slice(2 * p // 3, p) else: lo = int(self.rng.integers(p // 5, p // 2)) hi = min(p, lo + int(self.rng.integers(p // 12, p // 5))) sl = slice(lo, hi) corrupt[sl] |= valid_patch[sl] if self.train and self.rng.random() < self.cfg.line_mask_prob: score = np.zeros(p, dtype=np.float32) good = valid_patch & np.isfinite(patch_flux) if good.sum() > 4: grad = np.abs(np.gradient(np.nan_to_num(patch_flux, nan=0.0))) score[good] = grad[good] top = np.argsort(score)[-max(2, p // 24) :] for j in top: lo, hi = max(0, j - 1), min(p, j + 2) corrupt[lo:hi] |= valid_patch[lo:hi] max_allowed = max(1, int(valid_patch.sum() * self.cfg.max_mask_ratio)) cur = np.where(corrupt & valid_patch)[0] if len(cur) > max_allowed: keep = self.rng.choice(cur, size=max_allowed, replace=False) next_corrupt = np.zeros_like(corrupt) next_corrupt[keep] = True corrupt = next_corrupt return corrupt & valid_patch def _line_weights(self, patch_flux: np.ndarray, valid_patch: np.ndarray) -> np.ndarray: w = np.ones_like(patch_flux, dtype=np.float32) if valid_patch.sum() < 4: return w grad = np.abs(np.gradient(np.nan_to_num(patch_flux, nan=0.0))).astype(np.float32) scale = np.percentile(grad[valid_patch], 90) if valid_patch.any() else 1.0 if scale > 0: w += 2.0 * np.clip(grad / scale, 0.0, 2.0) w[~valid_patch] = 1.0 return np.clip(w, 1.0, 5.0) def _maybe_augment(self, sample: dict[str, Any]) -> dict[str, Any]: if not (self.train and self.cfg.augment_ood): return sample out = {k: v for k, v in sample.items()} bad = np.array(sample["bad_mask"], copy=True) lam = sample["lambda"] n = len(lam) if self.rng.random() < 0.45: frac = float(self.rng.uniform(0.65, 0.95)) width = max(32, int(n * frac)) start = int(self.rng.integers(0, max(1, n - width))) keep = np.zeros(n, dtype=np.bool_) keep[start : start + width] = True bad |= ~keep if self.rng.random() < 0.30: for _ in range(int(self.rng.integers(1, 4))): width = int(self.rng.integers(max(8, n // 200), max(12, n // 45))) start = int(self.rng.integers(0, max(1, n - width))) bad[start : start + width] = True out["bad_mask"] = bad return out