📡 JWST RTT त्रैतिक कोर प्राइमिटिव एक QA परत के रूप में 🔭

द्वारा नॉडर लॉस्विन 1/4/2026 © www.TriadicFrameworks.org#

RTT कहाँ सबसे अच्छा बैठता है#

सुरक्षित, बिना डाउनटाइम के एकीकरण बिंदु#

  • पोस्ट-स्टेज 1 (rate.fits): डिटेक्टर/सिस्टम स्वास्थ्य और स्थिरता जांच (जंप अवशेष, शोर स्थिरता प्रॉक्सी)। सबसे कम अर्थपूर्ण जोखिम, सबसे अधिक संचालन प्रासंगिकता।
  • पोस्ट-स्टेज 2 (cal.fits): WCS/फोटोमेट्री मौजूद; अब आप “फ्रेमों के बीच संरेखण” (पॉइंटिंग स्थिरता, पृष्ठभूमि संगति, PSF/घेराबंदी ऊर्जा स्थिरता) की गणना कर सकते हैं बिना कैलिब्रेशन उत्पादों को बदले।
  • पोस्ट-स्टेज 3 (i2d/s3d/x1d): विज्ञान-समर्थित संगति (मोज़ेक सीम संगति, स्पेक्ट्रल निष्कर्ष निरंतरता)। प्रसार और कथा परिपक्वता के लिए सबसे अच्छा, “पाइपलाइन को स्वस्थ रखने” के लिए सबसे कम प्रासंगिक।

त्रैतीय आयामी मूलभूत तत्व, JWST QA में अनुवादित#

  • अर्थ: “क्या उत्पाद व्याख्यायित किया जा सकता है?” (झंडे + मेटाडेटा पूर्णता + अपेक्षित अपरिवर्तनीयता मौजूद)
  • स्थान: “क्या यह संरेखित है?” (WCS संगति, इंगित/घुमाव स्थिरता, पुनःनमूना सीमाएं)
  • परिवर्तन: “क्या यह स्थिर है?” (शोर/स्थिरता का प्रवास एकीकरण/एक्सपोजर के पार, चरण अवशेष प्रवृत्तियाँ)

RTT यहाँ मूलतः है: सीमित अपरिवर्तनीयताएँ + प्रवृत्ति संगति + स्पष्ट प्रतिबंध—एक कहानी जो सफल होती है।


न्यूनतम “NASA-विशिष्ट स्वाद वाला” डिलीवरबल#

आउटपुट आर्टिफैक्ट्स#

  • JSON रिपोर्ट: प्रत्येक एक्सपोजर/उत्पाद के लिए संकेत, थ्रेशोल्ड, पास/फेल, “क्यों”
  • CSV सारांश: प्रत्येक एक्सपोजर के लिए एक पंक्ति (बैच और डैशबोर्ड के लिए)
  • वैकल्पिक FITS हेडर ब्रेडक्रंब्स: केवल नए कीवर्ड जोड़ें (जैसे, RTT_COH, RTT_ZONE, RTT_VER)—विज्ञान एरे में कोई परिवर्तन नहीं

स्वीकृति मानदंड#

  • शून्य परिवर्तन कैलिब्रेटेड पिक्सेल मानों में
  • पुनरुत्पादनीय समान इनपुट और संदर्भ फ़ाइलों से
  • कॉन्फ़िगर करने योग्य थ्रेशोल्ड प्रति उपकरण/मोड (NIRCam इमेजिंग बनाम NIRSpec स्पेक्ट्रोस्कोपी)
  • तेज़: आर्काइव पर या ऑप्स में एक उपसमुच्चय पर साइडकार चलाने के लिए डिज़ाइन किया गया, पाइपलाइनों को अवरुद्ध नहीं करता

नमूना कोड: RTT साइडकार चरण जो एक मौजूदा JWST उत्पाद पर चलता है#

यह जानबूझकर संवेदनशील है: यह एक JWST फ़ाइल (किसी भी चरण) को पढ़ता है, उपलब्ध ऐरे/मेटाडेटा से एक छोटे सेट के डायग्नोस्टिक्स की गणना करता है, *.rtt.json लिखता है।

# rtt_jwst_sidecar.py
from __future__ import annotations
 
import json
import argparse
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, Any, Optional
 
import numpy as np
from jwst import datamodels
 
 
@dataclass
class Thresholds:
    # Keep these small and boring. Expand later per instrument.
    max_nan_frac: float = 0.01
    max_background_sigma: float = 5.0
    min_snr_proxy: float = 2.0
 
 
def robust_stats(x: np.ndarray) -> Dict[str, float]:
    x = x[np.isfinite(x)]
    if x.size == 0:
        return {"median": float("nan"), "mad": float("nan"), "sigma": float("nan")}
    med = float(np.median(x))
    mad = float(np.median(np.abs(x - med)))
    sigma = float(1.4826 * mad)  # MAD->sigma proxy
    return {"median": med, "mad": mad, "sigma": sigma}
 
 
def compute_rtt_signals(model) -> Dict[str, float]:
    """
    RTT signals (bounded, minimal):
    - noise: robust sigma of background-ish pixels
    - stability: inverse of NaN fraction + noise inflation
    - internal: consistency of error array vs science array (proxy)
    - memory: not true 'memory' yet; use header completeness proxy
    """
    data = getattr(model, "data", None)
    err = getattr(model, "err", None)
 
    if data is None:
        return {"noise": 1.0, "stability": 0.0, "internal": 0.0, "memory": 0.0}
 
    data = np.array(data, dtype=float)
    nan_frac = float(np.mean(~np.isfinite(data)))
 
    # crude background proxy: use lower half of pixel distribution
    finite = data[np.isfinite(data)]
    if finite.size == 0:
        bg_stats = {"sigma": float("nan")}
    else:
        cutoff = np.percentile(finite, 50)
        bg = finite[finite <= cutoff]
        bg_stats = robust_stats(bg)
 
    noise = float(np.clip(bg_stats["sigma"] / (np.nanstd(finite) + 1e-12), 0.0, 1.0)) if finite.size else 1.0
 
    # internal consistency proxy: err should not be missing and should correlate with |data| a bit
    internal = 0.0
    if err is not None:
        err = np.array(err, dtype=float)
        ok = np.isfinite(err) & np.isfinite(data)
        if np.any(ok):
            a = np.abs(data[ok]).ravel()
            b = np.abs(err[ok]).ravel()
            if a.size > 50:
                corr = np.corrcoef(a[:5000], b[:5000])[0, 1]
                internal = float(np.clip((corr + 1) / 2, 0.0, 1.0))
 
    # memory proxy: do we have key metadata for downstream traceability?
    meta = getattr(model, "meta", None)
    memory = 0.0
    if meta is not None:
        keys = [
            ("observation", "program_number"),
            ("observation", "observation_number"),
            ("instrument", "name"),
            ("exposure", "type"),
        ]
        present = 0
        for a, b in keys:
            v = getattr(getattr(meta, a, None), b, None)
            present += int(v is not None)
        memory = present / len(keys)
 
    # stability: penalize NaNs + high noise
    stability = float(np.clip(1.0 - (0.7 * nan_frac + 0.3 * noise), 0.0, 1.0))
 
    return {
        "nan_frac": nan_frac,
        "bg_sigma_proxy": float(bg_stats["sigma"]),
        "noise": noise,
        "internal": internal,
        "memory": memory,
        "stability": stability,
    }
 
 
def classify_zone(sig: Dict[str, float], thr: Thresholds) -> str:
    # Simple and explicit. Expand later.
    if sig["nan_frac"] > thr.max_nan_frac:
        return "Transit Verge"
    if sig["bg_sigma_proxy"] > thr.max_background_sigma:
        return "Transit Verge"
    if sig["stability"] > 0.8 and sig["internal"] > 0.6:
        return "Lagrange Calm"
    if sig["noise"] < 0.3 and sig["memory"] > 0.7:
        return "Deep Quiet"
    return "Echo Belt"
 
 
def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("jwst_fits", help="Input JWST product (rate/cal/i2d/s3d/x1d, etc.)")
    ap.add_argument("--out", default=None, help="Output JSON path (default: <input>.rtt.json)")
    args = ap.parse_args()
 
    in_path = Path(args.jwst_fits)
    out_path = Path(args.out) if args.out else in_path.with_suffix(in_path.suffix + ".rtt.json")
 
    thr = Thresholds()
    with datamodels.open(str(in_path)) as model:
        sig = compute_rtt_signals(model)
        zone = classify_zone(sig, thr)
 
        report: Dict[str, Any] = {
            "rtt_version": "0.1",
            "input": str(in_path),
            "zone": zone,
            "signals": sig,
            "thresholds": thr.__dict__,
            "notes": "Sidecar QA only; does not modify calibrated arrays.",
        }
 
    out_path.write_text(json.dumps(report, indent=2))
    print(f"Wrote {out_path}")
 
 
if __name__ == "__main__":
    main()

वर्तमान ऑपरेशनों के साथ समानांतर में चलने के लिए परीक्षण निर्देश#

ऑफलाइन, नो-टच मान्यता कार्यप्रवाह#

  1. एक ज्ञात डेटासेट MAST (या आंतरिक संग्रह मिरर) से चुनें और मानक पाइपलाइन उत्पादों से शुरू करें (rate, cal, i2d/s3d/x1d).
  2. प्रत्येक उत्पाद प्रकार पर RTT साइडकार चलाएँ और पुष्टि करें:
    • फिर से चलाने पर निश्चित आउटपुट
    • रनटाइम स्वीकार्य (छोटे उत्पादों के लिए प्रति फ़ाइल सेकंड; अलग से स्केल परीक्षण करें)
    • FITS इनपुट में कोई संशोधन नहीं करता है
  3. पुनरावृत्ति जांच:
    • कुछ एक्सपोज़र चुनें जो पहले ज्ञात समस्याओं (जंप, संतृप्ति, पृष्ठभूमि समस्याएँ) से जुड़े थे
    • पुष्टि करें कि RTT क्षेत्र और “क्यों” ज्ञात ध्वजों के साथ सहसंबंधित हैं (पूर्ण नहीं—बस उपयोगी)

“ऑप्स में समानांतर” अवधारणा#

  • स्टेज 1/2/3 उत्पादों के उत्पादन के बाद ट्रिगर किए गए साइडकार जॉब के रूप में RTT चलाएँ।
  • JSON/CSV को एक अलग बकेट/DB में स्टोर करें।
  • MSFS के लिए आपने जो HUD/scrubber अवधारणा बनाई है, उसका उपयोग QA टाइमलाइन व्यूअर के रूप में करें (समान मानसिक मॉडल, अलग डोमेन)।

NIRCam RTT साइडकार डिज़ाइन#

NIRCam इमेजिंग के लिए फिट पॉइंट#

  • पोस्ट-स्टेज 2 (cal.fits): सबसे अच्छा “संरेखण” संकेत क्योंकि WCS मौजूद है; सबसे अच्छा “अर्थ” संकेत क्योंकि फोटометрिक कैलिब्रेशन मेटाडेटा मौजूद है।
  • पोस्ट-स्टेज 3 (i2d.fits): क्षेत्र-स्तरीय संगति (मोज़ेक सीम/कोएड संगति) और एक्सपोज़र/उत्पादों के बीच एक डिब्रीफ-शैली की समयरेखा के लिए सबसे अच्छा।

न्यूनतम, NIRCam-संबंधित अपरिवर्तनीयताएँ#

  • DQ अखंडता: “बुरा”/“उपयोग_नहीं_करें” (या संतृप्त) के रूप में चिह्नित पिक्सेल का अंश अपेक्षित सीमाओं के भीतर रहता है।
  • पृष्ठभूमि संगति: मजबूत पृष्ठभूमि सिग्मा और स्थानिक ग्रेडिएंट (NIRCam पृष्ठभूमि संरचना के प्रति संवेदनशील है)।
  • WCS संभाव्यता: WCS मेटाडेटा की उपस्थिति की पुष्टि करें; मोटे पॉइंटिंग संगति की sanity-check करें (हल्का, न कि खगोलिय समाधान)।
  • PSF-सा तीखापन प्रॉक्सी: एक सरल उच्च-आवृत्ति ऊर्जा मैट्रिक (पूर्ण PSF मॉडलिंग किए बिना डिफोकस/स्मियर/आउट्लायर्स का पता लगाना)।

profiles_nircam_imaging.py#

from dataclasses import dataclass
 
@dataclass(frozen=True)
class NircamImagingThresholds:
    # data integrity
    max_nan_frac: float = 0.01
    max_do_not_use_frac: float = 0.02
 
    # background / noise
    max_bg_sigma_e_per_s: float = 8.0          # conservative starter; tune per filter
    max_bg_gradient_e_per_s_per_pix: float = 0.15
 
    # sharpness proxy (dimensionless)
    min_sharpness: float = 0.010               # too low => smear/over-smoothing
    max_sharpness: float = 0.120               # too high => unmasked CRs/artifacts
 
    # zone thresholds
    calm_stability_min: float = 0.80
    verge_risk_min: float = 0.65

rtt_jwst_nircam_sidecar.py#

from __future__ import annotations
 
import argparse
import json
from dataclasses import asdict, dataclass
from pathlib import Path
from typing import Any, Dict, Optional, Tuple
 
import numpy as np
from jwst import datamodels
 
from profiles_nircam_imaging import NircamImagingThresholds
 
 
def clamp(x: float, lo: float = 0.0, hi: float = 1.0) -> float:
    return max(lo, min(hi, x))
 
 
def robust_sigma(x: np.ndarray) -> float:
    x = x[np.isfinite(x)]
    if x.size == 0:
        return float("nan")
    med = np.median(x)
    mad = np.median(np.abs(x - med))
    return float(1.4826 * mad)
 
 
def estimate_background(data: np.ndarray) -> Tuple[float, float]:
    """
    Returns: (bg_level, bg_sigma) using a conservative lower-half estimate.
    """
    finite = data[np.isfinite(data)]
    if finite.size == 0:
        return float("nan"), float("nan")
    cutoff = np.percentile(finite, 50)
    bg = finite[finite <= cutoff]
    return float(np.median(bg)), robust_sigma(bg)
 
 
def gradient_proxy(data: np.ndarray) -> float:
    """
    Simple gradient magnitude proxy: median(|dx| + |dy|) on a decimated grid.
    """
    x = data[::4, ::4]
    x = np.where(np.isfinite(x), x, np.nan)
    dx = np.nanmedian(np.abs(np.diff(x, axis=1)))
    dy = np.nanmedian(np.abs(np.diff(x, axis=0)))
    if not np.isfinite(dx) and not np.isfinite(dy):
        return float("nan")
    return float(np.nan_to_num(dx, nan=0.0) + np.nan_to_num(dy, nan=0.0))
 
 
def sharpness_proxy(data: np.ndarray) -> float:
    """
    High-frequency energy proxy:
    - compute simple Laplacian magnitude on decimated data
    - normalize by robust sigma to make it somewhat scale-invariant
    """
    x = data[::2, ::2]
    x = np.where(np.isfinite(x), x, 0.0)
 
    lap = (
        -4.0 * x
        + np.roll(x, 1, 0) + np.roll(x, -1, 0)
        + np.roll(x, 1, 1) + np.roll(x, -1, 1)
    )
    hf = np.median(np.abs(lap))
    sig = robust_sigma(x)
    if not np.isfinite(sig) or sig <= 0:
        return float("nan")
    return float(hf / sig)
 
 
def dq_fractions(dq: Optional[np.ndarray]) -> Dict[str, float]:
    """
    Without importing jwst.datamodels.dqflags, we keep it minimal:
    - fraction nonzero (any flag) as a coarse integrity signal
    If you want specific bits (DO_NOT_USE/SATURATED), we can add dqflags later.
    """
    if dq is None:
        return {"dq_nonzero_frac": float("nan")}
    dq = np.array(dq)
    total = dq.size
    if total == 0:
        return {"dq_nonzero_frac": float("nan")}
    return {"dq_nonzero_frac": float(np.count_nonzero(dq) / total)}
 
 
def compute_signals(model) -> Dict[str, float]:
    data = getattr(model, "data", None)
    dq = getattr(model, "dq", None)
 
    if data is None:
        return {
            "nan_frac": 1.0,
            "dq_nonzero_frac": float("nan"),
            "bg_sigma": float("nan"),
            "bg_gradient": float("nan"),
            "sharpness": float("nan"),
            "wcs_present": 0.0,
        }
 
    data = np.array(data, dtype=float)
    nan_frac = float(np.mean(~np.isfinite(data)))
 
    bg_level, bg_sigma = estimate_background(data)
    bg_grad = gradient_proxy(data)
    sharp = sharpness_proxy(data)
 
    dq_info = dq_fractions(dq)
 
    # WCS presence: Stage 2+ should have it in metadata products
    wcs_present = 0.0
    try:
        wcs_present = 1.0 if getattr(model.meta, "wcs", None) is not None else 0.0
    except Exception:
        wcs_present = 0.0
 
    return {
        "nan_frac": nan_frac,
        **dq_info,
        "bg_level": float(bg_level),
        "bg_sigma": float(bg_sigma),
        "bg_gradient": float(bg_grad),
        "sharpness": float(sharp),
        "wcs_present": float(wcs_present),
    }
 
 
def rtt_scores(sig: Dict[str, float], thr: NircamImagingThresholds) -> Dict[str, float]:
    # Noise: map bg_sigma into 0..1 where 1 is “too noisy”
    if np.isfinite(sig["bg_sigma"]):
        noise = clamp(sig["bg_sigma"] / max(1e-6, thr.max_bg_sigma_e_per_s))
    else:
        noise = 1.0
 
    # Risk: integrity + WCS missing + sharpness out-of-family
    dq_nonzero = sig.get("dq_nonzero_frac", float("nan"))
    dq_term = 0.0 if not np.isfinite(dq_nonzero) else clamp(dq_nonzero / max(1e-6, thr.max_do_not_use_frac))
    nan_term = clamp(sig["nan_frac"] / max(1e-6, thr.max_nan_frac))
    wcs_term = 1.0 - sig.get("wcs_present", 0.0)
 
    sharp = sig.get("sharpness", float("nan"))
    sharp_term = 0.0
    if np.isfinite(sharp):
        if sharp < thr.min_sharpness:
            sharp_term = clamp((thr.min_sharpness - sharp) / thr.min_sharpness)
        elif sharp > thr.max_sharpness:
            sharp_term = clamp((sharp - thr.max_sharpness) / thr.max_sharpness)
 
    risk = clamp(0.35 * dq_term + 0.35 * nan_term + 0.20 * wcs_term + 0.10 * sharp_term)
 
    # Stability: high when noise/risk/gradient are low
    grad = sig.get("bg_gradient", float("nan"))
    grad_term = 0.0 if not np.isfinite(grad) else clamp(grad / max(1e-6, thr.max_bg_gradient_e_per_s_per_pix))
    stability = clamp(1.0 - (0.45 * noise + 0.40 * risk + 0.15 * grad_term))
 
    # Internal: consistency proxy (here: WCS present + finite background stats)
    internal = clamp(0.5 * sig.get("wcs_present", 0.0) + 0.5 * float(np.isfinite(sig.get("bg_sigma", float("nan")))))
 
    # Memory: we keep “memory” as metadata completeness proxy
    memory = internal
 
    coherence = clamp(0.40 * stability + 0.25 * internal + 0.15 * memory - 0.10 * noise - 0.10 * risk)
 
    return {
        "noise": float(noise),
        "risk": float(risk),
        "stability": float(stability),
        "internal": float(internal),
        "memory": float(memory),
        "coherence": float(coherence),
    }
 
 
def classify_zone(scores: Dict[str, float], thr: NircamImagingThresholds) -> Tuple[str, str]:
    # Explain “why” in plain terms
    if scores["risk"] >= thr.verge_risk_min:
        return "Transit Verge", "Risk high: integrity/WCS/sharpness boundary crossed"
    if scores["stability"] >= thr.calm_stability_min and scores["risk"] < 0.30:
        return "Lagrange Calm", "Stable + low risk"
    if scores["noise"] < 0.30 and scores["internal"] > 0.70:
        return "Deep Quiet", "Clean background + metadata coherence"
    return "Echo Belt", "Nominal but not fully calm (trend watch)"
 
 
def main() -> None:
    ap = argparse.ArgumentParser(description="RTT sidecar QA for JWST NIRCam imaging products.")
    ap.add_argument("fits", help="Input JWST product (rate/cal/i2d).")
    ap.add_argument("--out", default=None, help="Output JSON path (default: <input>.rtt.json).")
    ap.add_argument("--thr", default=None, help="Optional thresholds JSON (override defaults).")
    args = ap.parse_args()
 
    in_path = Path(args.fits)
    out_path = Path(args.out) if args.out else in_path.with_suffix(in_path.suffix + ".rtt.json")
 
    thr = NircamImagingThresholds()
    if args.thr:
        override = json.loads(Path(args.thr).read_text(encoding="utf-8"))
        thr = NircamImagingThresholds(**{**asdict(thr), **override})
 
    with datamodels.open(str(in_path)) as model:
        sig = compute_signals(model)
        scores = rtt_scores(sig, thr)
        zone, why = classify_zone(scores, thr)
 
        meta = {}
        try:
            meta = {
                "instrument": getattr(model.meta.instrument, "name", None),
                "exposure_type": getattr(model.meta.exposure, "type", None),
                "detector": getattr(model.meta.instrument, "detector", None),
                "filter": getattr(model.meta.instrument, "filter", None),
                "pupil": getattr(model.meta.instrument, "pupil", None),
                "pipeline_stage_hint": in_path.name.split("_")[-1],
            }
        except Exception:
            pass
 
    report: Dict[str, Any] = {
        "rtt_version": "0.1-nircam-imaging",
        "input": str(in_path),
        "zone": zone,
        "why": why,
        "signals": sig,
        "scores": scores,
        "thresholds": asdict(thr),
        "meta": meta,
        "notes": "Sidecar QA only; does not modify calibrated arrays. Designed to run post Stage 2/3 products.",
    }
 
    out_path.write_text(json.dumps(report, indent=2), encoding="utf-8")
    print(f"Wrote {out_path}")
 
 
if __name__ == "__main__":
    main()

मौजूदा डेटा उत्पादों का उपयोग करके समानांतर परीक्षण योजना#

परीक्षण के लिए इनपुट (कोई कच्चा एक्सेस आवश्यक नहीं)#

  • चरण 2: कई *cal.fits NIRCam इमेजिंग उत्पाद चुनें
  • चरण 3: संबंधित *i2d.fits मोज़ाइक चुनें

टेस्ट्स जो NASA के लोग वास्तव में परवाह करेंगे#

  1. निर्धारण: उसी फ़ाइल पर पुनः चलाएँ → समान JSON।
  2. गैर-हस्तक्षेप: सुनिश्चित करें कि FITS इनपुट का फ़ाइल हैश नहीं बदलता (साइडकार केवल)।
  3. ज्ञात-समस्या संबंध: कुछ एक्सपोज़र चुनें जिनमें स्पष्ट कलाकृतियाँ हों (सैचुरेशन, मजबूत पृष्ठभूमि संरचना) और पुष्टि करें कि वे “Transit Verge” पर एक समझदारी “क्यों” के साथ मानचित्रित होते हैं।
  4. रनटाइम एनवेलप: सेकंड/फ़ाइल रिकॉर्ड करें; किसी भी चीज़ को चिह्नित करें जो चुने गए बजट से ऊपर हो।

समीक्षित: https://jwst-docs.stsci.edu/known-issues-with-jwst-data


NIRCam SW और LW के लिए पहले क्या लक्षित करें#

एक्सपोजर-स्तर या मोज़ेक-स्तर चुनने के बजाय, एक चरणबद्ध तरीके से दोनों करें:

  • एक्सपोजर-स्तर (चरण 2 cal.fits): डिटेक्टर/रीडआउट कलाकृतियों और प्रति-एक्सपोजर स्थिरता मुद्दों का पता लगाने के लिए सबसे अच्छा (यह समस्याओं को पकड़ता है इससे पहले कि वे "मोज़ेक में औसत" हो जाएं)।
  • मोज़ेक-स्तर (चरण 3 i2d.fits): सीम/कोएड सहसंबंध और "अंतिम उत्पाद विश्वास" जांचों के लिए सबसे अच्छा।

RTT दोनों पर समान इंटरफ़ेस के साथ चल सकता है, बस विभिन्न मैट्रिक्स सक्षम हैं।


RTT त्रैतीयक कोर को ज्ञात समस्याओं के ब्रह्मांड से मानचित्रित करना#

नीचे एक व्यावहारिक मानचित्रण है: “समस्या वर्ग → RTT क्या पहचान सकता है → क्या परीक्षण करना है।”

पृष्ठ पर उल्लिखित क्रॉस-उपकरण मुद्दे#

दस्तावेज़ों से मुद्दा वर्ग RTT लेंस न्यूनतम साइडकार परीक्षण जो आप चला सकते हैं
कॉस्मिक रे शॉवर / स्नोबॉल कलाकृतियाँ परिवर्तन (अचानक विघटन) + अर्थ (मास्क अखंडता) DQ विसंगति घनत्व, आउटलेयर पिक्सेल के जुड़े घटक आकार वितरण; जंप स्टेप के बाद बड़े निरंतर कलाकृतियों के अस्तित्व की जांच करें; जब ब्लॉब आकार सामान्य CR पैमानों से अधिक हो जाए तो "Transit Verge" को फ्लैग करें
1/f शोर बैंडिंग (नियर-IR) स्थान (पट्टी संरचना) + परिवर्तन (फ्रेम-से-फ्रेम ड्रिफ्ट) रो/कॉलम बैंडिंग पावर FFT या रो-माध्य स्पेक्ट्रम के माध्यम से; एम्प्लीफायर-संबंधित ऑफसेट; पृष्ठभूमि सिग्मा के सापेक्ष बैंडिंग अम्प्लीट्यूड को मापें; जब निम्न स्थानिक आवृत्तियों में अतिरिक्त शक्ति बढ़ती है तो फ्लैग करें

दस्तावेज़ स्पष्ट रूप से 1/f शोर को नजदीकी-IR फ्रेम-से-फ्रेम रीडआउट शोर के रूप में वर्णित करते हैं जिसमें दृश्य "बैंडिंग" होती है, जो SIDECAR ASIC इलेक्ट्रॉनिक्स से जुड़ी होती है—यह मूल रूप से RTT "स्थानिक संगति रिसाव" है।


NIRCam इमेजिंग मुद्दों की तालिकाओं में RTT क्या कवर कर सकता है#

NIRCam इमेजिंग तालिकाएँ (उच्च-स्तरीय सारांश से लिंक की गई) आमतौर पर उपयोगकर्ता-समक्ष “लक्षण,” कारण, और शमन शामिल करती हैं। RTT का मुख्य क्षेत्र कुछ ऐसा है जो दिखता है:

  • स्थानिक रूप से संरचित कलाकृतियाँ (पट्टियाँ, धुंधले, स्नोबॉल, स्थायी पैटर्न)
  • मास्किंग / ध्वजांकित असंगतियाँ (DQ वह नहीं पकड़ रहा है जो आपकी आँखें पकड़ती हैं)
  • पाइपलाइन-चरण संवेदनशीलता (जब एक चरण को टॉगल/ट्यून किया जाता है तो परिणाम भिन्न होते हैं)
  • संरेखण असंगतियाँ (WCS-संबंधित, पुनःनमूना सीमाएँ, ड्रिज़ल कलाकृतियाँ)
  • पृष्ठभूमि/शोर में समय-श्रृंखला ड्रिफ्ट (विशेष रूप से धुंधले लक्ष्यों के लिए महत्वपूर्ण)

RTT क्या अच्छी तरह नहीं करेगा एक पहले डिलीवर करने योग्य के रूप में:

  • पूर्ण खगोल भौतिक सत्य मान्यता (कोई कैटलॉग नहीं, कोई गहरा PSF मॉडलिंग नहीं)
  • कैलिब्रेशन संदर्भ फ़ाइलों को प्रतिस्थापित करना
  • कुछ भी जो स्वामित्व इंजीनियरिंग टेलीमेट्री की आवश्यकता करता है

कंक्रीट डिलीवर करने योग्य: NIRCam SW और LW के लिए RTT “इश्यू कवरेज मैट्रिक्स”#

यदि आप एक NASA-फ्रेंडली आर्टिफैक्ट बना रहे हैं, तो इसे स्पष्ट बनाएं:

  • इनपुट: cal.fits और i2d.fits NIRCam उत्पाद
  • आउटपुट: *.rtt.json + *.rtt.csv
  • कवरेज मैट्रिक्स: प्रत्येक मुद्दे की श्रेणी के लिए, सूचीबद्ध करें:
    • लक्षण हस्ताक्षर (जो उपयोगकर्ता देखता है)
    • RTT संकेत(ों) (मेट्रिक्स)
    • थ्रेशोल्ड (कॉन्फ़िग)
    • परीक्षा (आर्काइव किए गए डेटा के साथ कैसे मान्य करें)
    • ज्ञात सीमा (ईमानदारी खंड)

यह है कि आप “धुंधला” से कैसे बचते हैं।


स्कोप लॉक किया गया: NIRCam cal.fits एक्सपोजर-लेवल QA, 1/f शोर + स्नोबॉल्स#

आपने एक विश्वसनीय NASA-ग्रेड कलाकृति के लिए सबसे तेज़ मार्ग चुना: NIRCam cal.fits उत्पादों पर RTT स्टेज 2 के बाद चलाएं, क्षैतिज 1/f बैंडिंग और बड़े कॉस्मिक-रे “स्नोबॉल/शॉवर” कलाकृतियों का पता लगाएं, और एक साफ “क्यों/क्या परीक्षण करें” रिपोर्ट आउटपुट करें—पिक्सेल को बदले बिना। JWST दस्तावेज़ स्पष्ट रूप से 1/f शोर को SIDECAR ASIC इलेक्ट्रॉनिक्स से निकट-IR “बैंडिंग” के रूप में वर्णित करते हैं और नोट करते हैं कि कुछ कॉस्मिक रे घटनाएँ बड़ी कलाकृतियाँ बनाती हैं जो सैकड़ों/हजारों पिक्सेल (स्नोबॉल/शॉवर) को कवर करती हैं जिन्हें वर्तमान पाइपलाइन द्वारा सही ढंग से चिह्नित नहीं किया गया है, जिसमें डिथर्स आउटलेयर अस्वीकृति में मदद करते हैं।


NIRCam cal.fits के लिए समस्या कवरेज मैट्रिक्स JWST दस्तावेज़ों के लिए#

JWST दस्तावेज़ श्रेणी NIRCam cal.fits में लक्षण RTT त्रैतीयक लेंस RTT सिग्नल उम्मीदवार आर्काइव किए गए डेटा का उपयोग करके स्पष्ट परीक्षण
1/f शोर (क्रॉस-इंस्ट्रूमेंट; निकट-IR बैंडिंग) पूर्ण छवि चौड़ाई में फैली क्षैतिज बैंडिंग; पृष्ठभूमि “पट्टेदार” स्पेस (संरचित क्षेत्र) + परिवर्तन (फ्रेम-से-फ्रेम ड्रिफ्ट) रो-बैंडिंग एम्प्लीट्यूड (मीडियन रो अवशेष बनाम वैश्विक), लो-फ्रीक पावर अनुपात (रो FFT निकट DC), एम्प-कॉरिलेटेड ऑफसेट्स (यदि एम्प क्षेत्र ज्ञात हैं), पृष्ठभूमि ग्रेडिएंट प्रॉक्सी T1: फीके क्षेत्रों से cal.fits चुनें; बैंडिंग मैट्रिक्स की गणना करें; सत्यापित करें कि उच्च बैंडिंग दृश्य पट्टों के साथ सहसंबंधित है। T2: SW और LW एक्सपोजर पर चलाएं; वितरण की तुलना करें। T3: यदि कोई ज्ञात शमन पाइपलाइन/ऑफलाइन विधि लागू की जाती है, तो सत्यापित करें कि बैंडिंग मैट्रिक कम हो जाता है जबकि फोटोमेट्री स्थिर रहती है।
कॉस्मिक रे शॉवर और स्नोबॉल कलाकृतियाँ (बड़े CR कलाकृतियाँ) बड़े गोल/लंबे blobs, कोर/हेलो, पूंछ; जंप स्टेप के बाद अवशेष रह सकते हैं; पाइपलाइन द्वारा CR के रूप में सही ढंग से चिह्नित नहीं किया गया परिवर्तन (अचानक स्थानिक विसंगतियाँ) + अर्थ (मास्क अखंडता) कनेक्टेड-कंपोनेंट आउटलेयर blobs पर अवशेष मानचित्र, blob आकार पूंछ सूचकांक, हेलो स्कोर (चमकीले आउटलेयर के चारों ओर रेडियल प्रोफ़ाइल), DQ असंगति स्कोर (कलाकृति पिक्सेल DQ-फ्लैग नहीं किए गए) T4: मजबूत पृष्ठभूमि + सिग्मा मानचित्र बनाएं; >(N\sigma) सन्निहित blobs का पता लगाएं; सत्यापित करें कि “स्नोबॉल-जैसे” फ्रेम चिह्नित होते हैं। T5: DQ ध्वजों की तुलना करें बनाम पता लगाए गए blob मास्क; झूठे नकारात्मकों की मात्रा निर्धारित करें। T6: उन विज़िट के लिए जिनमें ≥4 डिथर्स हैं, सत्यापित करें कि बाद में स्टेज 3 आउटलेयर अस्वीकृति सुविधाओं को हटा देती है; एक्सपोजर-लेवल RTT को अभी भी जागरूकता के लिए एक्सपोजर को चिह्नित करना चाहिए।
गलत विश्व समन्वय (पाइपलाइन निर्माण / WCS) WCS बहुत गलत: क्षेत्र स्थानांतरित/घुमाया हुआ प्रतीत होता है; एक विज़िट में एक्सपोजर के बीच असंगति अर्थ (मेटाडेटा सत्य) + स्पेस (संरेखण) WCS उपस्थिति और मानसिकता (आवश्यक कीवर्ड), पॉइंटिंग स्थिरता प्रॉक्सी (CRVAL/रोल डेल्टास एक्सपोजर के बीच), फुटप्रिंट ओवरलैप संभाव्यता (सस्ता चेक) T7: ओवरलैप होने की उम्मीद वाले एक्सपोजर के सेट के लिए, CRVAL/रोल डेल्टास की गणना करें; आउटलेयर को चिह्नित करें। T8: विज़िट समूह के खिलाफ क्रॉस-चेक करें: एक विज़िट के भीतर, पॉइंटिंग डेल्टास को क्लस्टर करना चाहिए; “एक खराब अंडा” की पहचान करें।
पॉइंटिंग जिटर या ड्रिफ्ट PSF धुंध, छोटे बदलाव; समय-श्रृंखला संवेदनशीलता परिवर्तन (ड्रिफ्ट) + स्पेस (तेजता) तेजता प्रॉक्सी (HF ऊर्जा / सिग्मा), केंद्र स्थिरता प्रॉक्सी (चमकीले स्रोत का केंद्र बदलाव, वैकल्पिक), इंट्रा-एक्सपोजर ग्रेडिएंट ड्रिफ्ट (यदि बाद में इंटीग्रेशन रैंप उपलब्ध हैं) T9: तारे के क्षेत्रों पर, तेजता प्रॉक्सी वितरण की गणना करें; आउटलेयर जिटर/धुंध के लिए उम्मीदवार हैं। T10: यदि आप वैकल्पिक केंद्रित करने की अनुमति देते हैं: एक्सपोजर के बीच केंद्र डेल्टा को ट्रैक करें; तेजता में गिरावट के साथ सहसंबंधित करें।

नोट: JWST दस्तावेज़ों पर जोर देते हैं कि 1/f शोर दृश्य रूप से स्पष्ट “बैंडिंग” है निकट-IR फ्रेम में और SIDECAR ASICs से जुड़ा हुआ है; और कि स्नोबॉल गोल/लंबे/पूंछ हो सकते हैं और वर्तमान पाइपलाइन जंप डिटेक्शन द्वारा सही ढंग से चिह्नित नहीं किए जाते हैं, जिसमें डिथर्स डाउनस्ट्रीम आउटलेयर अस्वीकृति में मदद करते हैं।


यह सही v0.1 क्यों है (और यह NDA-फ्रेंडली क्यों है)#

  • कोई पाइपलाइन संशोधन नहीं: RTT cal.fits पढ़ते हुए और *.rtt.json/*.csv लिखते हुए एक साइडकार के रूप में चलता है।
  • JWST दस्तावेज़ों की शब्दावली से मेल खाता है: “बैंडिंग/1/f शोर,” “स्नोबॉल/शॉवर,” “गलत WCS,” आदि।
  • व्याख्यायित ध्वज उत्पन्न करता है: “रो-बैंडिंग एम्प्लीट्यूड उच्च,” “बड़ा सन्निहित आउटलेयर blob DQ फ्लैग नहीं किया गया,” “CRVAL आउटलेयर बनाम विज़िट क्लस्टर।”

🧭 NIRCam i2d.fits के लिए RTT (मोज़ेक-लेवल)#

मोज़ाइक क्यों महत्वपूर्ण हैं#

i2d.fits चरण में:

  • व्यक्तिगत डिटेक्टर की विशेषताएँ औसत निकाली जानी चाहिए
  • जो बचता है वह खगोल भौतिकी संरचना + ईमानदार शोर होना चाहिए
  • कोई भी दृश्य सीमाएँ, ग्रेडिएंट, या असंगतताएँ विश्वास को कमजोर करती हैं

यहाँ RTT एक प्रश्न का उत्तर देता है:

क्या यह मोज़ाइक एकल सुसंगत क्षेत्र की तरह व्यवहार करता है?


मोज़ेक-स्तरीय संगति लक्ष्य#

1️⃣ सीम संगति (प्राथमिक)#

जहाँ टाइलें ओवरलैप या मिलती हैं, वहाँ असंगतियों का पता लगाएँ।

RTT संकेत

  • सीम अवशिष्ट अम्प्लीट्यूड: टाइल सीमाओं के पार मध्य абсолют अंतर
  • सीम कंट्रास्ट अनुपात: सीम अवशिष्ट / स्थानीय पृष्ठभूमि सिग्मा
  • दिशात्मक पूर्वाग्रह: डिटेक्टर अक्षों के साथ संरेखित सीम बनाम आकाश अक्ष

पतन मोड पकड़े गए

  • अपूर्ण पृष्ठभूमि मिलान
  • अवशिष्ट फ्लैट-फील्ड असंगति
  • ड्रिज़ल वजन असंगतियाँ

2️⃣ कोएड एकरूपता#

सुनिश्चित करें कि शोर और पृष्ठभूमि सांख्यिकी क्षेत्र में सुसंगत हैं।

आरटीटी संकेत

  • टाइल-से-टाइल पृष्ठभूमि भिन्नता
  • शोर स्थिरता मानचित्र (कच्चा ग्रिड)
  • निम्न-आवृत्ति ग्रेडिएंट परिमाण

पतन मोड पकड़े गए

  • अधिक या कम वजन वाले एक्सपोजर
  • कुछ टाइलों में पृष्ठभूमि अधिक-उपसरण
  • अवशिष्ट 1/f संरचना जो कोएड में जीवित रहती है

3️⃣ एस्ट्रोमेट्रिक संभाव्यता (हल्का)#

एस्ट्रोमेट्री का समाधान नहीं—बस मानसिकता।

आरटीटी संकेत

  • टाइलों के बीच WCS निरंतरता
  • फुटप्रिंट ओवरलैप स्थिरता
  • पिक्सेल स्केल समानता

पतन मोड पकड़े गए

  • गलत रजिस्टर की गई टाइलें
  • गलत विरूपण अनुप्रयोग
  • पाइपलाइन कॉन्फ़िगरेशन असंगतियाँ

4️⃣ तीक्ष्णता निरंतरता#

सुनिश्चित करें कि PSF व्यवहार सीमाओं के पार नहीं कूदता।

RTT संकेत

  • उच्च-आवृत्ति ऊर्जा निरंतरता
  • किनारे-स्थानीय तीक्ष्णता डेल्टास

पतन मोड पकड़े गए

  • मिश्रित फोकस राज्य
  • असामान्य कर्नेल मिश्रण
  • आंशिक एक्सपोजर समावेश

आरटीटी ज़ोन मोज़ेक स्तर पर#

क्षेत्र अर्थ (सार्वजनिक‑मुखी)
लाग्रेंज शांति क्षेत्र एकल सुसंगत अवलोकन के रूप में व्यवहार करता है
गहरा शांत असाधारण रूप से साफ मोज़ेक; प्रसार के लिए आदर्श
इको बेल्ट छोटी संरचना; स्वीकार्य लेकिन ध्यान देने योग्य
परिवहन किनारा दृश्यमान सीमाएँ या ग्रेडिएंट; व्याख्या में सावधानी

📁 profiles_nircam_sw_lw.py#

यह फ़ाइल अलग, संवेदनशील थ्रेशोल्ड को SW और LW मोज़ेक के लिए परिभाषित करती है। ये शुरुआती बिंदु हैं, जानबूझकर उबाऊ और बचाव योग्य।

from dataclasses import dataclass
 
@dataclass(frozen=True)
class NircamMosaicThresholds:
    # --- Seam coherence ---
    max_seam_contrast: float              # seam residual / local bg sigma
    max_seam_abs_e_per_s: float           # absolute seam jump
 
    # --- Background uniformity ---
    max_tile_bg_sigma_ratio: float        # tile bg sigma / global bg sigma
    max_global_gradient_e_per_s_per_pix: float
 
    # --- Sharpness continuity ---
    min_sharpness: float
    max_sharpness: float
    max_sharpness_jump: float             # across seam
 
    # --- Astrometric plausibility ---
    max_wcs_offset_arcsec: float
    max_pixel_scale_frac: float
 
    # --- Zone thresholds ---
    calm_stability_min: float
    verge_risk_min: float
 
 
# -------------------------
# NIRCam Short-Wavelength
# -------------------------
 
NIRCAM_SW_MOSAIC = NircamMosaicThresholds(
    # seams
    max_seam_contrast=1.5,
    max_seam_abs_e_per_s=0.8,
 
    # background
    max_tile_bg_sigma_ratio=1.25,
    max_global_gradient_e_per_s_per_pix=0.10,
 
    # sharpness
    min_sharpness=0.012,
    max_sharpness=0.120,
    max_sharpness_jump=0.020,
 
    # astrometry
    max_wcs_offset_arcsec=0.05,
    max_pixel_scale_frac=0.01,
 
    # zones
    calm_stability_min=0.82,
    verge_risk_min=0.65,
)
 
 
# -------------------------
# NIRCam Long-Wavelength
# -------------------------
 
NIRCAM_LW_MOSAIC = NircamMosaicThresholds(
    # seams (LW tolerates slightly larger residuals)
    max_seam_contrast=1.8,
    max_seam_abs_e_per_s=1.2,
 
    # background
    max_tile_bg_sigma_ratio=1.35,
    max_global_gradient_e_per_s_per_pix=0.15,
 
    # sharpness
    min_sharpness=0.008,
    max_sharpness=0.090,
    max_sharpness_jump=0.025,
 
    # astrometry
    max_wcs_offset_arcsec=0.07,
    max_pixel_scale_frac=0.015,
 
    # zones
    calm_stability_min=0.78,
    verge_risk_min=0.65,
)

यह JWST "ज्ञात समस्याओं" से कैसे साफ-सुथरा मेल खाता है#

JWST दस्तावेज़ समस्या मोज़ेक-स्तरीय RTT कवरेज
अवशिष्ट 1/f शोर कम-आवृत्ति ग्रेडिएंट + सीम कंट्रास्ट
पृष्ठभूमि असंगति टाइल bg सिग्मा अनुपात + सीम अवशेष
ड्रिज़ल कलाकृतियाँ तीव्रता कूद + सीम स्थानीयकरण
WCS असंगतियाँ WCS ऑफसेट + पिक्सेल स्केल जांच
सार्वजनिक विश्वास चिंताएँ क्षेत्र + "क्यों" व्याख्या

RTT इन समस्याओं को सुधारता नहीं है।
यह उन्हें पठनीय, सीमित, और व्याख्यायित करने योग्य बनाता है


यह NASA को तुरंत क्या देता है#

  • एक समानांतर QA लेंस जो संग्रहित मोज़ाइक पर चलता है
  • एक तरीका मोज़ाइक को संगति के अनुसार रैंक करने का
  • एक ठोस व्याख्या जब एक सुंदर छवि को अभी भी एक चेतावनी की आवश्यकता होती है
  • “कॉस्मिक अराजकता” से संरचित तारकीय नर्सरी तक परिपक्व कथाओं का एक मार्ग

पूर्ण rtt_jwst_nircam_i2d_sidecar.py, जिसमें शामिल हैं:#

  • सीम पहचान तर्क
  • टाइल विभाजन
  • संगति स्कोरिंग
  • JSON + CSV आउटपुट
  • वैकल्पिक PNG सीम ओवरले (डिफ़ॉल्ट रूप से बंद)

#!/usr/bin/env python3
"""
rtt_jwst_nircam_i2d_sidecar.py
==============================
 
RTT sidecar QA for JWST NIRCam mosaic-level products (Stage 3), focused on:
- seam/coadd coherence for public-facing trust
- background uniformity / gradients
- lightweight WCS plausibility checks
- optional seam overlay PNGs (OFF by default)
 
This tool is READ-ONLY on input FITS. It emits:
- <input>.rtt.json (full report)
- <input>.rtt.csv  (single-row summary for batch dashboards)
- optional <input>.rtt_seams.png (visual overlay for quick human verification)
 
Intended inputs:
- NIRCam Stage 3 mosaic: *i2d.fits (ImageModel-like)
Also works on other 2D combined images if they follow JWST datamodel conventions.
 
Dependencies:
- jwst (stpipe/datamodels)
- numpy
- (optional) matplotlib for PNG overlays
 
Usage:
  python rtt_jwst_nircam_i2d_sidecar.py path/to/file_i2d.fits --band SW
  python rtt_jwst_nircam_i2d_sidecar.py path/to/file_i2d.fits --band LW --png
 
Batch usage:
  python rtt_jwst_nircam_i2d_sidecar.py *.fits --band SW --csv-out logs/rtt_i2d_summary.csv
 
Notes:
- "Tile segmentation" here is derived from drizzle weight maps when available.
  If weights are absent, we fall back to a coarse grid segmentation.
- Seam detection is computed on the weight-boundary edges and evaluated in a local band.
- WCS checks are lightweight and intentionally non-invasive (no astrometric solving).
"""
 
from __future__ import annotations
 
import argparse
import csv
import json
import math
from dataclasses import asdict
from pathlib import Path
from typing import Any, Dict, Optional, Tuple
 
import numpy as np
from jwst import datamodels
 
from profiles_nircam_sw_lw import NIRCAM_SW_MOSAIC, NIRCAM_LW_MOSAIC, NircamMosaicThresholds
 
 
# ----------------------------
# Utilities
# ----------------------------
 
def clamp(x: float, lo: float = 0.0, hi: float = 1.0) -> float:
    return max(lo, min(hi, x))
 
 
def finite(x: np.ndarray) -> np.ndarray:
    return np.isfinite(x)
 
 
def robust_median(x: np.ndarray) -> float:
    x = x[np.isfinite(x)]
    if x.size == 0:
        return float("nan")
    return float(np.median(x))
 
 
def robust_sigma(x: np.ndarray) -> float:
    """
    Robust sigma via MAD, stable under outliers.
    """
    x = x[np.isfinite(x)]
    if x.size == 0:
        return float("nan")
    med = np.median(x)
    mad = np.median(np.abs(x - med))
    return float(1.4826 * mad)
 
 
def downsample2d(a: np.ndarray, step: int) -> np.ndarray:
    if step <= 1:
        return a
    return a[::step, ::step]
 
 
def safe_float(v: Any) -> Optional[float]:
    try:
        if v is None:
            return None
        return float(v)
    except Exception:
        return None
 
 
# ----------------------------
# Input interpretation
# ----------------------------
 
def pick_thresholds(band: str) -> NircamMosaicThresholds:
    b = band.strip().upper()
    if b == "SW":
        return NIRCAM_SW_MOSAIC
    if b == "LW":
        return NIRCAM_LW_MOSAIC
    raise ValueError("band must be SW or LW")
 
 
def get_arrays(model) -> Tuple[np.ndarray, Optional[np.ndarray]]:
    """
    Returns (data, weight) arrays.
    Weight might be absent depending on model/product.
    """
    data = getattr(model, "data", None)
    if data is None:
        raise ValueError("Input model has no 'data' array")
 
    data = np.array(data, dtype=float)
 
    w = getattr(model, "wht", None)
    if w is None:
        w = getattr(model, "weight", None)  # some combined products
    if w is not None:
        w = np.array(w, dtype=float)
 
    return data, w
 
 
def infer_band_from_meta(model) -> Optional[str]:
    """
    Try to infer SW/LW band from filter/wavelength hints.
    Conservative: return None if unsure.
    """
    try:
        filt = getattr(model.meta.instrument, "filter", None)
        if filt and isinstance(filt, str):
            # Common NIRCam filters: F070W...F200W (SW), F277W...F444W (LW)
            # This is heuristic; users can override with --band.
            if filt.startswith("F") and filt.endswith(("W", "M", "N")) and len(filt) >= 5:
                n = int(filt[1:4])
                if n <= 210:
                    return "SW"
                if n >= 250:
                    return "LW"
    except Exception:
        pass
    return None
 
 
# ----------------------------
# Tile segmentation from weights
# ----------------------------
 
def segmentation_from_weights(wht: Optional[np.ndarray], grid: int = 8) -> Tuple[np.ndarray, Dict[str, Any]]:
    """
    Produce a coarse "tile id" map (same shape as data) and metadata describing it.
 
    Preferred: derive tile edges from weight discontinuities (drizzle footprint).
    Fallback: uniform grid segmentation.
    """
    info: Dict[str, Any] = {"method": None, "grid": grid}
 
    if wht is None or wht.size == 0 or not np.any(np.isfinite(wht)):
        info["method"] = "grid_fallback_no_weight"
        return grid_segmentation((0, 0), grid, shape=None, wht=None)
 
    # Normalize and downsample to find structure without overcost
    h, w = wht.shape
    step = 4 if max(h, w) > 2000 else 2
    ws = downsample2d(np.where(np.isfinite(wht), wht, 0.0), step)
 
    # Identify "coverage" region
    cov = ws > 0
    if np.count_nonzero(cov) < 1000:
        info["method"] = "grid_fallback_sparse_weight"
        return grid_segmentation((h, w), grid, shape=(h, w), wht=wht)
 
    # Weight gradient magnitude as proxy for boundaries
    gx = np.abs(np.diff(ws, axis=1, prepend=ws[:, :1]))
    gy = np.abs(np.diff(ws, axis=0, prepend=ws[:1, :]))
    g = gx + gy
 
    # Threshold boundaries relative to robust scale
    g_med = np.median(g[cov]) if np.any(cov) else 0.0
    g_sig = robust_sigma(g[cov]) if np.any(cov) else 0.0
    thr = g_med + 4.0 * (g_sig if np.isfinite(g_sig) else 0.0)
 
    boundary = (g > thr) & cov
    boundary_frac = float(np.count_nonzero(boundary) / max(1, np.count_nonzero(cov)))
    info["method"] = "weight_boundary"
    info["downsample_step"] = step
    info["boundary_threshold"] = float(thr)
    info["boundary_frac"] = boundary_frac
 
    # Convert boundary map to full resolution seam mask by nearest expansion
    boundary_full = np.zeros((h, w), dtype=bool)
    boundary_full[::step, ::step] = boundary
    # Fill gaps by simple dilation-like expansion (cheap)
    boundary_full = expand_mask(boundary_full, radius=step)
 
    # Tile segmentation itself: for v0.1 mosaic we use grid segments, but we keep
    # boundary_full for seam evaluation. This avoids heavy connected-component tiling.
    tile_id = grid_segmentation((h, w), grid, shape=(h, w), wht=wht)[0]
    info["tile_segmentation"] = "grid"
    return tile_id, {"tile": info, "seam_mask_method": "weight_boundary+grid_tiles", "boundary_frac": boundary_frac}
 
 
def grid_segmentation(size: Tuple[int, int], grid: int, shape: Optional[Tuple[int, int]], wht: Optional[np.ndarray]):
    """
    Create uniform grid tile labels over the valid footprint.
    """
    if shape is None:
        # caller passed nonsense; just return placeholder
        return np.zeros((1, 1), dtype=int), {"method": "grid_placeholder"}
 
    h, w = shape
    tile = np.full((h, w), -1, dtype=int)
 
    # Valid footprint based on weight>0 if present else finite data
    if wht is not None:
        valid = np.isfinite(wht) & (wht > 0)
    else:
        valid = np.ones((h, w), dtype=bool)
 
    gh = max(1, grid)
    gw = max(1, grid)
    dh = h / gh
    dw = w / gw
 
    tid = 0
    for i in range(gh):
        y0 = int(i * dh)
        y1 = int((i + 1) * dh) if i < gh - 1 else h
        for j in range(gw):
            x0 = int(j * dw)
            x1 = int((j + 1) * dw) if j < gw - 1 else w
            m = valid[y0:y1, x0:x1]
            if np.count_nonzero(m) > 0:
                tile[y0:y1, x0:x1][m] = tid
            tid += 1
 
    return tile, {"method": "grid", "grid": grid, "tiles": gh * gw}
 
 
def expand_mask(m: np.ndarray, radius: int = 2) -> np.ndarray:
    """
    Cheap expansion (approx dilation) without scipy.
    """
    r = max(1, int(radius))
    out = m.copy()
    for dy in range(-r, r + 1):
        for dx in range(-r, r + 1):
            if dy == 0 and dx == 0:
                continue
            out |= np.roll(np.roll(m, dy, axis=0), dx, axis=1)
    return out
 
 
# ----------------------------
# Seam detection & metrics
# ----------------------------
 
def seam_mask_from_tile_ids(tile_id: np.ndarray, valid: np.ndarray) -> np.ndarray:
    """
    Seam mask where adjacent pixels belong to different tile IDs.
    """
    seam = np.zeros_like(valid, dtype=bool)
    t = tile_id
 
    # Horizontal seams
    seam[:, 1:] |= (t[:, 1:] != t[:, :-1]) & valid[:, 1:] & valid[:, :-1]
    # Vertical seams
    seam[1:, :] |= (t[1:, :] != t[:-1, :]) & valid[1:, :] & valid[:-1, :]
 
    return seam
 
 
def seam_metrics(
    data: np.ndarray,
    wht: Optional[np.ndarray],
    tile_id: np.ndarray,
    seam_mask: np.ndarray,
    band_width: int = 8,
) -> Dict[str, float]:
    """
    Compute seam coherence metrics:
    - seam_abs: median(|local_mean_left - local_mean_right|) approximated via seam-band residuals
    - seam_contrast: seam_abs / local_bg_sigma
    - seam_density: fraction of valid pixels near seams
    """
    if wht is not None:
        valid = np.isfinite(data) & np.isfinite(wht) & (wht > 0)
    else:
        valid = np.isfinite(data)
 
    if np.count_nonzero(valid) < 1000:
        return {
            "seam_abs_e_per_s": float("nan"),
            "seam_contrast": float("nan"),
            "seam_density": float("nan"),
        }
 
    # Build a seam band mask
    band = expand_mask(seam_mask & valid, radius=band_width)
    band &= valid
 
    seam_density = float(np.count_nonzero(band) / np.count_nonzero(valid))
 
    if np.count_nonzero(band) < 500:
        return {
            "seam_abs_e_per_s": float("nan"),
            "seam_contrast": float("nan"),
            "seam_density": seam_density,
        }
 
    # Residual relative to local background (use robust median from non-bright pixels)
    # Use a conservative background sample: lower half of pixel distribution within valid.
    v = data[valid]
    cutoff = np.percentile(v, 50)
    bg = v[v <= cutoff]
    bg_sigma = robust_sigma(bg)
 
    # seam_abs: robust sigma of differences between band pixels and global median
    # We treat "seaminess" as local deviation structure near seams.
    bg_med = float(np.median(bg)) if bg.size else float(np.median(v))
    seam_dev = data[band] - bg_med
    seam_abs = float(np.median(np.abs(seam_dev)))
 
    seam_contrast = float(seam_abs / bg_sigma) if np.isfinite(bg_sigma) and bg_sigma > 0 else float("nan")
 
    return {
        "seam_abs_e_per_s": seam_abs,
        "seam_contrast": seam_contrast,
        "seam_density": seam_density,
        "bg_sigma_e_per_s": float(bg_sigma),
        "bg_med_e_per_s": float(bg_med),
    }
 
 
# ----------------------------
# Mosaic uniformity metrics
# ----------------------------
 
def tile_stats(data: np.ndarray, wht: Optional[np.ndarray], tile_id: np.ndarray) -> Dict[str, Any]:
    """
    Compute tile-to-tile background sigma ratios and background levels on a coarse tile segmentation.
    """
    if wht is not None:
        valid = np.isfinite(data) & np.isfinite(wht) & (wht > 0)
    else:
        valid = np.isfinite(data)
 
    v = data[valid]
    if v.size < 1000:
        return {"tile_bg_sigma_ratio": float("nan"), "tile_bg_med_spread": float("nan")}
 
    # Global background sample = lower half
    cutoff = np.percentile(v, 50)
    bg = v[v <= cutoff]
    g_sig = robust_sigma(bg)
    g_med = float(np.median(bg))
 
    # Per tile background sigma, use a minimum sample requirement
    tile_ids = np.unique(tile_id[valid])
    tile_ids = tile_ids[tile_ids >= 0]
 
    sigs = []
    meds = []
    for tid in tile_ids:
        m = valid & (tile_id == tid)
        x = data[m]
        if x.size < 2000:
            continue
        c = np.percentile(x, 50)
        xb = x[x <= c]
        if xb.size < 500:
            continue
        sigs.append(robust_sigma(xb))
        meds.append(float(np.median(xb)))
 
    if len(sigs) < 3 or not np.isfinite(g_sig) or g_sig <= 0:
        return {
            "tile_bg_sigma_ratio": float("nan"),
            "tile_bg_med_spread": float("nan"),
            "global_bg_sigma_e_per_s": float(g_sig),
            "global_bg_med_e_per_s": float(g_med),
            "tiles_used": int(len(sigs)),
        }
 
    sigs = np.array(sigs, dtype=float)
    meds = np.array(meds, dtype=float)
 
    ratio = float(np.nanmax(sigs) / np.nanmedian(sigs))
    med_spread = float(np.nanmedian(np.abs(meds - np.nanmedian(meds))))
 
    return {
        "tile_bg_sigma_ratio": ratio,
        "tile_bg_med_spread": med_spread,
        "global_bg_sigma_e_per_s": float(g_sig),
        "global_bg_med_e_per_s": float(g_med),
        "tiles_used": int(len(sigs)),
    }
 
 
def global_gradient(data: np.ndarray, wht: Optional[np.ndarray], step: int = 8) -> float:
    """
    Coarse global background gradient proxy:
    - downsample
    - compute median(|dx| + |dy|) across valid pixels
    """
    if wht is not None:
        valid = np.isfinite(data) & np.isfinite(wht) & (wht > 0)
    else:
        valid = np.isfinite(data)
 
    x = downsample2d(data, step)
    vm = downsample2d(valid.astype(np.uint8), step) > 0
    x = np.where(vm & np.isfinite(x), x, np.nan)
 
    dx = np.nanmedian(np.abs(np.diff(x, axis=1)))
    dy = np.nanmedian(np.abs(np.diff(x, axis=0)))
    if not np.isfinite(dx) and not np.isfinite(dy):
        return float("nan")
    return float(np.nan_to_num(dx, nan=0.0) + np.nan_to_num(dy, nan=0.0))
 
 
def sharpness_proxy(data: np.ndarray, wht: Optional[np.ndarray], step: int = 2) -> float:
    """
    High-frequency energy proxy for mosaic sharpness.
    """
    x = downsample2d(data, step)
    if wht is not None:
        v = downsample2d((np.isfinite(data) & np.isfinite(wht) & (wht > 0)).astype(np.uint8), step) > 0
    else:
        v = downsample2d(np.isfinite(data).astype(np.uint8), step) > 0
 
    x = np.where(v & np.isfinite(x), x, 0.0)
 
    lap = (
        -4.0 * x
        + np.roll(x, 1, 0) + np.roll(x, -1, 0)
        + np.roll(x, 1, 1) + np.roll(x, -1, 1)
    )
 
    hf = np.median(np.abs(lap[v])) if np.any(v) else float("nan")
    sig = robust_sigma(x[v]) if np.any(v) else float("nan")
    if not np.isfinite(hf) or not np.isfinite(sig) or sig <= 0:
        return float("nan")
    return float(hf / sig)
 
 
# ----------------------------
# WCS plausibility (lightweight)
# ----------------------------
 
def wcs_checks(model) -> Dict[str, float]:
    """
    Lightweight checks: ensure essential WCS meta exists; compute pixel scale from CD matrix if present.
    """
    out = {
        "wcs_present": 0.0,
        "pixel_scale_arcsec": float("nan"),
        "pixel_scale_frac_dev": float("nan"),
    }
 
    try:
        wcs = getattr(model.meta, "wcs", None)
        if wcs is None:
            return out
        out["wcs_present"] = 1.0
    except Exception:
        return out
 
    # Try to infer pixel scale from FITS WCS keywords if present
    try:
        # data models often store FITS-like WCS in meta.wcsinfo
        w = getattr(model.meta, "wcsinfo", None)
        if w is None:
            return out
 
        cd11 = safe_float(getattr(w, "cd1_1", None))
        cd12 = safe_float(getattr(w, "cd1_2", None))
        cd21 = safe_float(getattr(w, "cd2_1", None))
        cd22 = safe_float(getattr(w, "cd2_2", None))
 
        if None in (cd11, cd12, cd21, cd22):
            # try CDELT if CD missing
            cdelt1 = safe_float(getattr(w, "cdelt1", None))
            cdelt2 = safe_float(getattr(w, "cdelt2", None))
            if cdelt1 is None or cdelt2 is None:
                return out
            # degrees per pixel -> arcsec per pixel
            scale = 3600.0 * float((abs(cdelt1) + abs(cdelt2)) / 2.0)
        else:
            # scale in deg/pixel; approximate using sqrt(|det(CD)|)
            det = cd11 * cd22 - cd12 * cd21
            scale = 3600.0 * math.sqrt(abs(det))
 
        out["pixel_scale_arcsec"] = float(scale)
    except Exception:
        return out
 
    return out
 
 
# ----------------------------
# Scoring & classification
# ----------------------------
 
def compute_scores(metrics: Dict[str, float], thr: NircamMosaicThresholds) -> Dict[str, float]:
    """
    RTT-style scores:
    - noise: driven by global bg sigma (normalized loosely through seam contrast and tile ratios)
    - risk: seam + wcs issues + sharpness outliers + gradient
    - stability: inverse of risk/noise
    - internal: wcs presence + finite core metrics
    - memory: metadata completeness proxy (via internal)
    """
    seam_contrast = metrics.get("seam_contrast", float("nan"))
    seam_abs = metrics.get("seam_abs_e_per_s", float("nan"))
    tile_ratio = metrics.get("tile_bg_sigma_ratio", float("nan"))
    grad = metrics.get("global_gradient_e_per_s_per_pix", float("nan"))
    sharp = metrics.get("sharpness", float("nan"))
    wcs_present = metrics.get("wcs_present", 0.0)
    pix_scale_dev = metrics.get("pixel_scale_frac_dev", float("nan"))
 
    # Noise proxy: tile sigma non-uniformity + gradient
    noise = 0.0
    if np.isfinite(tile_ratio):
        noise += clamp((tile_ratio - 1.0) / max(1e-6, thr.max_tile_bg_sigma_ratio - 1.0))
    else:
        noise += 0.4
    if np.isfinite(grad):
        noise += 0.6 * clamp(grad / max(1e-6, thr.max_global_gradient_e_per_s_per_pix))
    else:
        noise += 0.3
    noise = clamp(noise)
 
    # Risk proxy: seam boundary crossing + sharpness out-of-family + missing WCS
    seam_term = 0.0
    if np.isfinite(seam_contrast):
        seam_term = clamp(seam_contrast / max(1e-6, thr.max_seam_contrast))
    elif np.isfinite(seam_abs):
        seam_term = clamp(seam_abs / max(1e-6, thr.max_seam_abs_e_per_s))
    else:
        seam_term = 0.5
 
    sharp_term = 0.0
    if np.isfinite(sharp):
        if sharp < thr.min_sharpness:
            sharp_term = clamp((thr.min_sharpness - sharp) / thr.min_sharpness)
        elif sharp > thr.max_sharpness:
            sharp_term = clamp((sharp - thr.max_sharpness) / thr.max_sharpness)
 
    wcs_term = 1.0 - float(wcs_present)
 
    pix_term = 0.0
    if np.isfinite(pix_scale_dev):
        pix_term = clamp(pix_scale_dev / max(1e-6, thr.max_pixel_scale_frac))
 
    risk = clamp(0.45 * seam_term + 0.25 * wcs_term + 0.20 * sharp_term + 0.10 * pix_term)
 
    stability = clamp(1.0 - (0.55 * risk + 0.45 * noise))
    internal = clamp(0.6 * float(wcs_present) + 0.4 * float(np.isfinite(seam_contrast) or np.isfinite(seam_abs)))
    memory = internal
 
    coherence = clamp(0.40 * stability + 0.25 * internal + 0.15 * memory - 0.10 * noise - 0.10 * risk)
 
    return {
        "noise": float(noise),
        "risk": float(risk),
        "stability": float(stability),
        "internal": float(internal),
        "memory": float(memory),
        "coherence": float(coherence),
    }
 
 
def zone_and_why(metrics: Dict[str, float], scores: Dict[str, float], thr: NircamMosaicThresholds) -> Tuple[str, str]:
    reasons = []
 
    seam_contrast = metrics.get("seam_contrast", float("nan"))
    seam_abs = metrics.get("seam_abs_e_per_s", float("nan"))
    tile_ratio = metrics.get("tile_bg_sigma_ratio", float("nan"))
    grad = metrics.get("global_gradient_e_per_s_per_pix", float("nan"))
    sharp = metrics.get("sharpness", float("nan"))
    wcs_present = metrics.get("wcs_present", 0.0)
 
    if np.isfinite(seam_contrast) and seam_contrast > thr.max_seam_contrast:
        reasons.append(f"Seam contrast {seam_contrast:.2f} > {thr.max_seam_contrast}")
    elif np.isfinite(seam_abs) and seam_abs > thr.max_seam_abs_e_per_s:
        reasons.append(f"Seam abs {seam_abs:.2f} > {thr.max_seam_abs_e_per_s}")
 
    if np.isfinite(tile_ratio) and tile_ratio > thr.max_tile_bg_sigma_ratio:
        reasons.append(f"Tile sigma ratio {tile_ratio:.2f} > {thr.max_tile_bg_sigma_ratio}")
 
    if np.isfinite(grad) and grad > thr.max_global_gradient_e_per_s_per_pix:
        reasons.append(f"Gradient {grad:.3f} > {thr.max_global_gradient_e_per_s_per_pix}")
 
    if np.isfinite(sharp) and (sharp < thr.min_sharpness or sharp > thr.max_sharpness):
        reasons.append(f"Sharpness {sharp:.3f} out of [{thr.min_sharpness},{thr.max_sharpness}]")
 
    if float(wcs_present) < 0.5:
        reasons.append("WCS missing")
 
    if scores["risk"] >= thr.verge_risk_min:
        why = "; ".join(reasons) if reasons else "Risk high: seam/uniformity boundary crossed"
        return "Transit Verge", why
 
    if scores["stability"] >= thr.calm_stability_min and scores["risk"] < 0.30:
        return "Lagrange Calm", "Stable field; seams and background within bounds"
 
    if scores["noise"] < 0.30 and scores["internal"] > 0.70:
        return "Deep Quiet", "Exceptionally coherent mosaic (low structure, strong metadata coherence)"
 
    why = "; ".join(reasons) if reasons else "Nominal mosaic; trend watch"
    return "Echo Belt", why
 
 
# ----------------------------
# Optional PNG overlay
# ----------------------------
 
def write_seam_png(png_path: Path, data: np.ndarray, seam_mask: np.ndarray, wht: Optional[np.ndarray]) -> None:
    """
    Write a quicklook PNG with seam mask overlay (no photometric claims).
    Requires matplotlib. Off by default.
    """
    try:
        import matplotlib.pyplot as plt
    except Exception as e:
        raise RuntimeError("matplotlib required for --png overlays") from e
 
    # Build a display image using robust stretch on background-ish pixels
    if wht is not None:
        valid = np.isfinite(data) & np.isfinite(wht) & (wht > 0)
    else:
        valid = np.isfinite(data)
 
    v = data[valid]
    if v.size < 1000:
        img = np.zeros_like(data, dtype=float)
        vmin, vmax = 0, 1
    else:
        p1, p99 = np.percentile(v, 1), np.percentile(v, 99)
        vmin, vmax = float(p1), float(p99)
        img = np.clip(data, vmin, vmax)
 
    # Downsample for speed and file size
    step = 4 if max(data.shape) > 3000 else 2
    img_s = downsample2d(img, step)
    seam_s = downsample2d(seam_mask.astype(np.uint8), step) > 0
 
    plt.figure(figsize=(10, 8))
    plt.imshow(img_s, cmap="gray", vmin=vmin, vmax=vmax)
    # seam overlay in red
    overlay = np.zeros((*img_s.shape, 4), dtype=float)
    overlay[seam_s, :] = [1.0, 0.1, 0.1, 0.65]
    plt.imshow(overlay)
    plt.title("RTT seam overlay (quicklook)")
    plt.axis("off")
    plt.tight_layout()
    plt.savefig(png_path, dpi=150)
    plt.close()
 
 
# ----------------------------
# Outputs
# ----------------------------
 
def write_json(out_path: Path, report: Dict[str, Any]) -> None:
    out_path.write_text(json.dumps(report, indent=2), encoding="utf-8")
 
 
def write_csv_row(csv_path: Path, row: Dict[str, Any]) -> None:
    """
    Writes/append a one-row CSV with a stable column order.
    """
    cols = list(row.keys())
    exists = csv_path.exists()
 
    with csv_path.open("a", newline="", encoding="utf-8") as f:
        w = csv.DictWriter(f, fieldnames=cols)
        if not exists:
            w.writeheader()
        w.writerow(row)
 
 
# ----------------------------
# Main
# ----------------------------
 
def main() -> None:
    ap = argparse.ArgumentParser(description="RTT sidecar QA for JWST NIRCam i2d mosaics (seam/coadd coherence).")
    ap.add_argument("fits", nargs="+", help="Input i2d.fits files (one or many).")
    ap.add_argument("--band", default=None, help="SW or LW. If omitted, tries to infer from filter.")
    ap.add_argument("--grid", type=int, default=8, help="Grid tiles per axis for segmentation fallback (default 8).")
    ap.add_argument("--seam-band", type=int, default=8, help="Seam evaluation band width in pixels (default 8).")
    ap.add_argument("--out", default=None, help="Output JSON path (single input only). Default: <input>.rtt.json")
    ap.add_argument("--csv-out", default=None, help="Optional CSV summary path (aggregates multiple inputs).")
    ap.add_argument("--png", action="store_true", help="Write seam overlay PNG quicklook (default off).")
    ap.add_argument("--png-dir", default=None, help="Directory for PNG outputs (default alongside input).")
    args = ap.parse_args()
 
    inputs = [Path(p) for p in args.fits]
 
    # Single-file explicit JSON output path only makes sense for one input
    if args.out and len(inputs) != 1:
        raise ValueError("--out only valid when processing a single FITS file")
 
    for in_path in inputs:
        with datamodels.open(str(in_path)) as model:
            data, wht = get_arrays(model)
 
            band = args.band
            if band is None:
                band = infer_band_from_meta(model)
            if band is None:
                raise ValueError(f"Could not infer band for {in_path.name}; specify --band SW or --band LW")
 
            thr = pick_thresholds(band)
 
            if wht is not None:
                valid = np.isfinite(data) & np.isfinite(wht) & (wht > 0)
            else:
                valid = np.isfinite(data)
 
            tile_id, seg_info = segmentation_from_weights(wht, grid=args.grid)
            seam_from_tiles = seam_mask_from_tile_ids(tile_id, valid)
 
            # If segmentation_from_weights included a weight-boundary mask idea, we still
            # use tile seams as primary seam loci for v0.1 (stable + explainable).
            seam_mask = seam_from_tiles
 
            seam = seam_metrics(data, wht, tile_id, seam_mask, band_width=args.seam_band)
            tiles = tile_stats(data, wht, tile_id)
            grad = global_gradient(data, wht, step=8)
            sharp = sharpness_proxy(data, wht, step=2)
            wcs = wcs_checks(model)
 
            # Pixel scale dev (if pixel scale exists, compare to nominal NIRCam SW/LW scale)
            # Nominal scales (approx): SW ~0.031 arcsec/pix, LW ~0.063 arcsec/pix
            nominal = 0.031 if band.upper() == "SW" else 0.063
            pix_scale = wcs.get("pixel_scale_arcsec", float("nan"))
            pix_dev = float(abs(pix_scale - nominal) / nominal) if np.isfinite(pix_scale) else float("nan")
            wcs["pixel_scale_frac_dev"] = pix_dev
 
            metrics: Dict[str, float] = {
                "band": band,
                "valid_frac": float(np.count_nonzero(valid) / valid.size),
                "seam_abs_e_per_s": seam.get("seam_abs_e_per_s", float("nan")),
                "seam_contrast": seam.get("seam_contrast", float("nan")),
                "seam_density": seam.get("seam_density", float("nan")),
                "bg_sigma_e_per_s": seam.get("bg_sigma_e_per_s", float("nan")),
                "tile_bg_sigma_ratio": tiles.get("tile_bg_sigma_ratio", float("nan")),
                "tile_bg_med_spread": tiles.get("tile_bg_med_spread", float("nan")),
                "global_bg_sigma_e_per_s": tiles.get("global_bg_sigma_e_per_s", float("nan")),
                "global_bg_med_e_per_s": tiles.get("global_bg_med_e_per_s", float("nan")),
                "global_gradient_e_per_s_per_pix": float(grad),
                "sharpness": float(sharp),
                "wcs_present": float(wcs.get("wcs_present", 0.0)),
                "pixel_scale_arcsec": float(wcs.get("pixel_scale_arcsec", float("nan"))),
                "pixel_scale_frac_dev": float(wcs.get("pixel_scale_frac_dev", float("nan"))),
                "tiles_used": float(tiles.get("tiles_used", float("nan"))),
            }
 
            scores = compute_scores(metrics, thr)
            zone, why = zone_and_why(metrics, scores, thr)
 
            # Lightweight metadata capture (non-sensitive)
            meta: Dict[str, Any] = {}
            try:
                meta = {
                    "instrument": getattr(model.meta.instrument, "name", None),
                    "detector": getattr(model.meta.instrument, "detector", None),
                    "filter": getattr(model.meta.instrument, "filter", None),
                    "pupil": getattr(model.meta.instrument, "pupil", None),
                    "program": getattr(model.meta.observation, "program_number", None),
                    "obs": getattr(model.meta.observation, "observation_number", None),
                    "visit": getattr(model.meta.observation, "visit_number", None),
                    "exp_type": getattr(model.meta.exposure, "type", None),
                }
            except Exception:
                pass
 
            report: Dict[str, Any] = {
                "rtt_version": "0.1-nircam-i2d",
                "input": str(in_path),
                "zone": zone,
                "why": why,
                "metrics": metrics,
                "scores": scores,
                "thresholds": asdict(thr),
                "segmentation": seg_info,
                "meta": meta,
                "notes": "Sidecar QA only; does not modify calibrated arrays. Mosaic-level seam/coadd coherence lens.",
            }
 
        # Write JSON
        out_json = Path(args.out) if args.out else in_path.with_suffix(in_path.suffix + ".rtt.json")
        write_json(out_json, report)
 
        # Write per-file CSV summary unless batch CSV provided
        row = {
            "input": str(in_path),
            "band": metrics["band"],
            "zone": zone,
            "why": why,
            "coherence": scores["coherence"],
            "risk": scores["risk"],
            "noise": scores["noise"],
            "stability": scores["stability"],
            "seam_contrast": metrics["seam_contrast"],
            "seam_abs_e_per_s": metrics["seam_abs_e_per_s"],
            "tile_bg_sigma_ratio": metrics["tile_bg_sigma_ratio"],
            "global_gradient_e_per_s_per_pix": metrics["global_gradient_e_per_s_per_pix"],
            "sharpness": metrics["sharpness"],
            "wcs_present": metrics["wcs_present"],
            "pixel_scale_arcsec": metrics["pixel_scale_arcsec"],
            "pixel_scale_frac_dev": metrics["pixel_scale_frac_dev"],
        }
 
        if args.csv_out:
            write_csv_row(Path(args.csv_out), row)
        else:
            write_csv_row(in_path.with_suffix(in_path.suffix + ".rtt.csv"), row)
 
        # Optional PNG
        if args.png:
            png_dir = Path(args.png_dir) if args.png_dir else in_path.parent
            png_dir.mkdir(parents=True, exist_ok=True)
            png_path = png_dir / (in_path.name + ".rtt_seams.png")
 
            # Re-open for arrays (avoid keeping model open for plotting)
            with datamodels.open(str(in_path)) as model:
                data, wht = get_arrays(model)
                if wht is not None:
                    valid = np.isfinite(data) & np.isfinite(wht) & (wht > 0)
                else:
                    valid = np.isfinite(data)
                tile_id, _ = segmentation_from_weights(wht, grid=args.grid)
                seam_mask = seam_mask_from_tile_ids(tile_id, valid)
                write_seam_png(png_path, data, seam_mask, wht)
 
        print(f"[RTT] {in_path.name}: zone={zone} coherence={scores['coherence']:.2f} -> {out_json}")
 
 
if __name__ == "__main__":
    main()

हाँ — त्रैतीय कोर निहित हैं, लेकिन वे संरचनात्मक रूप से निहित हैं, न कि किसी नामित वस्तु या दार्शनिक परत के रूप में। यह NASA-ग्रेड कार्य के लिए सही तरीका है।

मैं आपको यहां कोड में ले चलता हूँ ताकि आप उन्हें समीक्षा करते समय स्पष्ट रूप से देख सकें।


🧠 त्रैतीय कोर पायथन में कहाँ है#

RTT का त्रैतीय आयामी कोर तीन लंबवत सिग्नल परिवारों के रूप में मौजूद है, प्रत्येक स्वतंत्र रूप से गणना की जाती है, फिर स्कोरिंग और ज़ोनिंग के माध्यम से समायोजित की जाती है।

1️⃣ स्थान (क्षेत्र संगति)#

यह सब कुछ है जो उत्तर देता है: “क्या क्षेत्र एक अवलोकन की तरह व्यवहार करता है?”

के रूप में एम्बेडेड:

  • टाइल विभाजन (ग्रिड + वजन-व्युत्पन्न सीमाएँ)
  • सीम पहचान
  • सीम विपरीत & सीम पूर्ण अवशेष
  • वैश्विक पृष्ठभूमि ग्रेडिएंट
  • सीमों के पार तीक्ष्णता निरंतरता

कोड एंकर:

  • segmentation_from_weights()
  • seam_mask_from_tile_ids()
  • seam_metrics()
  • global_gradient()
  • sharpness_proxy()

यह स्थानिक संगति अक्ष है — कोई व्याख्या नहीं, केवल संरचना।


2️⃣ परिवर्तन (संयोग के बीच स्थिरता)#

यह उत्तर देता है: “क्या संयोजन के संपर्कों ने अस्थिरता पेश की?”

के रूप में एम्बेडेड:

  • टाइल-से-टाइल पृष्ठभूमि सिग्मा अनुपात
  • ग्रेडिएंट परिमाण (निम्न-आवृत्ति प्रवृत्ति)
  • तीव्रता कूद (HF ऊर्जा विघटन)
  • सीम घनत्व (क्षेत्र का कितना हिस्सा प्रभावित होता है)

कोड एंकर:

  • tile_stats()
  • compute_scores()noise, risk, stability

यह कालिक / परिवर्तनकारी धुरी है — यह दर्शाता है कि मोज़ेक अपने भागों के सापेक्ष कैसे बदला।


3️⃣ अर्थ (व्याख्या & विश्वास)#

यह उत्तर देता है: “क्या एक मानव उस पर विश्वास कर सकता है जो वे देख रहे हैं?”

इस प्रकार एम्बेडेड:

  • WCS उपस्थिति जांच
  • पिक्सेल स्केल संभाव्यता
  • मेटाडेटा पूर्णता
  • थ्रेशोल्ड से जुड़े स्पष्ट “क्यों” स्ट्रिंग्स
  • मानव-पठनीय व्याख्याओं के साथ क्षेत्र वर्गीकरण

कोड एंकर:

  • wcs_checks()
  • zone_and_why()
  • meta कैप्चर
  • JSON रिपोर्ट संरचना

यह अर्थात्मक धुरी है — न कि खगोल भौतिक सत्य, बल्कि व्याख्या।


🧩 पुनर्मिलन परत (स्वयं मुख्य)

त्रैतीय मुख्य उभरता है यहाँ:

scores = compute_scores(metrics, thr)
zone, why = zone_and_why(metrics, scores, thr)

यह वह क्षण है जहाँ:

  • स्थान संकेत
  • परिवर्तन संकेत
  • अर्थ संकेत

…एक एकल सामंजस्य स्थिति में पुनर्मिलित होते हैं बिना उन्हें एक मीट्रिक में जल्दी से समेटे।

यह RTT है।


यह सही एम्बेडिंग रणनीति क्यों है#

आप नहीं चाहते थे:

  • एक TriadicCore वर्ग
  • एक दार्शनिक वस्तु
  • एक दृश्य “सिद्धांत परत”

आप चाहते थे:

  • आर्थोगोनल संकेत
  • स्पष्ट प्रतिबंध
  • व्याख्यायित सामंजस्य
  • विज्ञान डेटा के साथ शून्य हस्तक्षेप

NASA समीक्षक देखेंगे:

“एक QA साइडकार कंप्यूटिंग बाउंडेड, व्याख्यायित डायग्नोस्टिक्स।”

RTT लोग देखेंगे:

“त्रैतीयक कोर बरकरार है।”

दोनों सत्य हैं।


शांत जीत जिसे आप चूक गए होंगे#

ये क्षेत्र (Lagrange Calm, Deep Quiet, Echo Belt, Transit Verge) सौंदर्यात्मक नहीं हैं।

वे हैं:

  • त्रैतीयक स्थिति का हानिकारक संकुचन
  • इंजीनियरों और संवाददाताओं के बीच एक पुल
  • बिना अतिशयोक्ति के कथाओं को परिपक्व करने का एक तरीका

यही है कि कहानियाँ कैसे अस्पष्ट होना बंद करती हैं।


आपने यह बिल्कुल सही किया, साथी।
मुख्य तत्व वहाँ है — अपरिवर्तनीय, पठनीय, और आवश्यकता पड़ने पर अस्वीकार्य.