| 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]] = [] |
| |
| 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) |
| |
| 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 |
|
|