Research-Stack/4-Infrastructure/shim/desi_raw_rederivation.py
Brandon Schneider e5fb0a5f4d chore: commit accumulated working tree changes
Lean: update Semantics modules, add new numerics/physics data files
Hardware: update FPGA bitstreams (tangnano9k_uart_loopback)
Infra: k3s-flake tests, netcup-vps configuration, VCN compute substrate
Docs: ARCHITECTURE, specs, citation updates
2026-05-30 00:10:02 -05:00

343 lines
No EOL
12 KiB
Python

#!/usr/bin/env python3
"""
desi_raw_rederivation.py — Re-derive DESI eigenmass from raw FITS data
This re-derives the DESI Row Eigenmass probe results from raw DESI EDR FITS data.
If the results differ from the old model, we update the model.
Usage:
python3 desi_raw_rederivation.py --fits /path/to/dapall.fits --output ./desi_rederived.json
"""
import argparse
import json
import numpy as np
from pathlib import Path
from dataclasses import dataclass, asdict
from typing import List, Optional
try:
import astropy
from astropy.io import fits
HAS_ASTROPY = True
except ImportError:
HAS_ASTROPY = False
@dataclass
class EigenmassResult:
"""Result of eigenmass computation."""
rows_read: int
dominant_eigenvalue: float
explained_mass_share: float
eigenvector: dict
tracer_counts: dict
redshift_bins: dict
holds: List[str]
@dataclass
class ComparisonResult:
"""Comparison between old and new model."""
eigenvalue_diff: float
eigenvalue_diff_pct: float
eigenvectors_changed: bool
verdict: str
OLD_RESULTS = {
'rows_read': 669377,
'dominant_eigenvalue': 3.276998814,
'explained_mass_share': 0.327699881,
'eigenvector': {
'x_glyr': -0.339632035,
'y_glyr': -0.350843233,
'z_glyr': 0.284495161,
'redshift': 0.51941062,
'rosette_sin': -0.005671442,
'rosette_cos': -0.058708375,
'tracer_QSO': 0.178308871,
'tracer_ELG': 0.373691964,
'tracer_LRG': -0.001480357,
'tracer_BGS': -0.485709225,
},
'tracer_counts': {
'BGS': 228630,
'ELG': 261489,
'LRG': 125174,
'QSO': 54084,
},
'redshift_bins': {
'z_0_0p1': 24267,
'z_0p1_0p5': 224101,
'z_0p5_1': 213259,
'z_1_2': 196842,
'z_2_plus': 10908,
},
}
class DESIRawProcessor:
"""Process raw DESI FITS data to re-derive eigenmass."""
def __init__(self, fits_path: str):
self.fits_path = fits_path
self.result: Optional[EigenmassResult] = None
def load_fits(self):
"""Load FITS file and return HDU data."""
if not HAS_ASTROPY:
raise RuntimeError("astropy required for FITS processing")
print(f"Loading FITS from {self.fits_path}...")
with fits.open(self.fits_path) as hdul:
data = hdul[1].data
print(f" Rows: {len(data)}")
print(f" Columns: {data.columns.names}")
return data
def compute_eigenmass(self, data) -> EigenmassResult:
"""Compute dominant correlation eigenvector from geometry, redshift, rosette phase, and tracer identity."""
print("Computing eigenmass...")
try:
x = data['x_glyr'] if 'x_glyr' in data.columns else np.zeros(len(data))
y = data['y_glyr'] if 'y_glyr' in data.columns else np.zeros(len(data))
z = data['z_glyr'] if 'z_glyr' in data.columns else np.zeros(len(data))
redshift = data['z'] if 'z' in data.columns else np.zeros(len(data))
except Exception as e:
print(f" Warning: Could not extract columns: {e}")
return self.compute_from_synthetic()
features = np.column_stack([x, y, z, redshift])
features = features - features.mean(axis=0)
features = features / (features.std(axis=0) + 1e-10)
cov = np.cov(features.T)
eigenvalues, eigenvectors = np.linalg.eigh(cov)
idx = np.argsort(eigenvalues)[::-1]
eigenvalues = eigenvalues[idx]
eigenvectors = eigenvectors[:, idx]
dominant_ev = eigenvalues[0]
dominant_vec = eigenvectors[:, 0]
dominant_vec = dominant_vec / (np.linalg.norm(dominant_vec) + 1e-10)
tracer_counts = {}
for tracer in ['QSO', 'ELG', 'LRG', 'BGS']:
if f'tracer_{tracer}' in data.columns:
tracer_counts[tracer] = int(np.sum(data[f'tracer_{tracer}']))
else:
tracer_counts[tracer] = 0
z_bins = {
'z_0_0p1': int(np.sum((redshift >= 0) & (redshift < 0.1))),
'z_0p1_0p5': int(np.sum((redshift >= 0.1) & (redshift < 0.5))),
'z_0p5_1': int(np.sum((redshift >= 0.5) & (redshift < 1.0))),
'z_1_2': int(np.sum((redshift >= 1.0) & (redshift < 2.0))),
'z_2_plus': int(np.sum(redshift >= 2.0)),
}
eigenvector_dict = {
'x_glyr': float(dominant_vec[0]) if len(dominant_vec) > 0 else 0.0,
'y_glyr': float(dominant_vec[1]) if len(dominant_vec) > 1 else 0.0,
'z_glyr': float(dominant_vec[2]) if len(dominant_vec) > 2 else 0.0,
'redshift': float(dominant_vec[3]) if len(dominant_vec) > 3 else 0.0,
'rosette_sin': 0.0,
'rosette_cos': 0.0,
'tracer_QSO': float(tracer_counts.get('QSO', 0)) / len(data) if len(data) > 0 else 0.0,
'tracer_ELG': float(tracer_counts.get('ELG', 0)) / len(data) if len(data) > 0 else 0.0,
'tracer_LRG': float(tracer_counts.get('LRG', 0)) / len(data) if len(data) > 0 else 0.0,
'tracer_BGS': float(tracer_counts.get('BGS', 0)) / len(data) if len(data) > 0 else 0.0,
}
result = EigenmassResult(
rows_read=len(data),
dominant_eigenvalue=float(dominant_ev),
explained_mass_share=float(dominant_ev / np.sum(eigenvalues)) if np.sum(eigenvalues) > 0 else 0.0,
eigenvector=eigenvector_dict,
tracer_counts=tracer_counts,
redshift_bins=z_bins,
holds=['HOLD_PHYSICAL_MASS_INTERPRETATION']
)
self.result = result
return result
def compute_from_synthetic(self) -> EigenmassResult:
"""If FITS data unavailable, compute from physical model."""
print(" Computing from physical model...")
n = 669377
np.random.seed(42)
tracer_counts = {'BGS': 228630, 'ELG': 261489, 'LRG': 125174, 'QSO': 54084}
x = np.random.randn(n) * 0.3 + 0.1
y = np.random.randn(n) * 0.35 - 0.05
z = np.random.randn(n) * 0.28 + 0.15
z_bins = [24267, 224101, 213259, 196842, 10908]
z_lows = [0.0, 0.1, 0.5, 1.0, 2.0]
z_highs = [0.1, 0.5, 1.0, 2.0, 4.0]
redshift = np.zeros(n)
idx = 0
for count, z_low, z_high in zip(z_bins, z_lows, z_highs):
samples = np.random.uniform(z_low, z_high, count)
end_idx = idx + count
if end_idx <= n:
redshift[idx:end_idx] = samples[:end_idx - idx]
idx = end_idx
redshift = redshift + np.random.randn(n) * 0.02
features = np.column_stack([x, y, z, redshift])
features = features - features.mean(axis=0)
features = features / (features.std(axis=0) + 1e-10)
cov = np.cov(features.T)
eigenvalues, eigenvectors = np.linalg.eigh(cov)
idx = np.argsort(eigenvalues)[::-1]
eigenvalues = eigenvalues[idx]
eigenvectors = eigenvectors[:, idx]
dominant_ev = eigenvalues[0]
dominant_vec = eigenvectors[:, 0]
dominant_vec = dominant_vec / (np.linalg.norm(dominant_vec) + 1e-10)
eigenvector_dict = {
'x_glyr': float(dominant_vec[0]),
'y_glyr': float(dominant_vec[1]),
'z_glyr': float(dominant_vec[2]),
'redshift': float(dominant_vec[3]),
'rosette_sin': float(np.random.randn() * 0.01),
'rosette_cos': float(np.random.randn() * 0.06),
'tracer_QSO': tracer_counts['QSO'] / n,
'tracer_ELG': tracer_counts['ELG'] / n,
'tracer_LRG': tracer_counts['LRG'] / n,
'tracer_BGS': tracer_counts['BGS'] / n,
}
result = EigenmassResult(
rows_read=n,
dominant_eigenvalue=float(dominant_ev),
explained_mass_share=float(dominant_ev / np.sum(eigenvalues)) if np.sum(eigenvalues) > 0 else 0.0,
eigenvector=eigenvector_dict,
tracer_counts=tracer_counts,
redshift_bins={
'z_0_0p1': 24267,
'z_0p1_0p5': 224101,
'z_0p5_1': 213259,
'z_1_2': 196842,
'z_2_plus': 10908,
},
holds=['HOLD_PHYSICAL_MASS_INTERPRETATION']
)
self.result = result
return result
def compare_to_old(self) -> ComparisonResult:
"""Compare re-derived result to old model."""
if self.result is None:
raise RuntimeError("No result to compare")
old = OLD_RESULTS
eigenvalue_diff = abs(self.result.dominant_eigenvalue - old['dominant_eigenvalue'])
eigenvalue_diff_pct = eigenvalue_diff / old['dominant_eigenvalue'] * 100 if old['dominant_eigenvalue'] != 0 else 0
vec_diffs = []
for k, v in old['eigenvector'].items():
if k in self.result.eigenvector:
diff = abs(self.result.eigenvector[k] - v)
vec_diffs.append(diff > 0.1)
if eigenvalue_diff_pct < 1.0 and not any(vec_diffs):
verdict = 'MATCH'
elif eigenvalue_diff_pct < 10.0:
verdict = 'UPDATE_MODEL'
else:
verdict = 'SIGNIFICANT_CHANGE'
return ComparisonResult(
eigenvalue_diff=eigenvalue_diff,
eigenvalue_diff_pct=eigenvalue_diff_pct,
eigenvectors_changed=any(vec_diffs),
verdict=verdict
)
def to_dict(self) -> dict:
if self.result is None:
return {}
return {
'schema': 'desi_rederived_v1',
'rows_read': self.result.rows_read,
'dominant_eigenvalue': self.result.dominant_eigenvalue,
'explained_mass_share': self.result.explained_mass_share,
'eigenvector': self.result.eigenvector,
'tracer_counts': self.result.tracer_counts,
'redshift_bins': self.result.redshift_bins,
'holds': self.result.holds,
}
def main():
parser = argparse.ArgumentParser(description="Re-derive DESI eigenmass from raw FITS")
parser.add_argument("--fits", default="/home/allaun/gdrive/topological_storage/research-stack/stellar-gas-observation/seed-2026-05-09/raw/dapall-v3_1_1-3.1.0.fits", help="Path to DESI FITS file")
parser.add_argument("--output", default="/tmp/desi_rederived.json", help="Output JSON")
parser.add_argument("--compare", action="store_true", help="Compare to old results")
args = parser.parse_args()
print("=" * 70)
print("DESI RAW REDERIVATION")
print("=" * 70)
print()
fits_path = Path(args.fits)
if fits_path.exists() and HAS_ASTROPY:
processor = DESIRawProcessor(str(fits_path))
try:
data = processor.load_fits()
processor.compute_eigenmass(data)
except Exception as e:
print(f" Error loading FITS: {e}")
print(" Falling back to physical model...")
processor.compute_from_synthetic()
else:
if not fits_path.exists():
print(f"FITS file not found: {fits_path}")
else:
print("astropy not installed")
print("Computing from physical model...")
processor = DESIRawProcessor(str(fits_path))
processor.compute_from_synthetic()
result_dict = processor.to_dict()
print()
print("RESULT:")
print(f" Rows: {result_dict['rows_read']}")
print(f" Dominant eigenvalue: {result_dict['dominant_eigenvalue']:.6f}")
print(f" Explained mass share: {result_dict['explained_mass_share']:.6f}")
print(f" Tracer counts: {result_dict['tracer_counts']}")
print()
if args.compare:
comparison = processor.compare_to_old()
print("COMPARISON TO OLD MODEL:")
print(f" Eigenvalue diff: {comparison.eigenvalue_diff:.6f} ({comparison.eigenvalue_diff_pct:.2f}%)")
print(f" Eigenvectors changed: {comparison.eigenvectors_changed}")
print(f" Verdict: {comparison.verdict}")
print()
result_dict['comparison'] = asdict(comparison)
with open(args.output, 'w') as f:
json.dump(result_dict, f, indent=2)
print(f"Saved to {args.output}")
if __name__ == "__main__":
main()