#!/usr/bin/env python3 """ generate_rotatable_html.py Generate roman_footprint_rotatable.html — Roman WFI footprints with a user-controlled PA slider/text box. All polygon geometry is computed in the browser from boresight (ra, dec) + SCA IDL vertices, so the user can type any PA and see the footprints update instantly. The JS implements the same attitude_matrix transform as pysiaf: 1. Build a 3x3 rotation matrix M from (V2Ref, V3Ref, ra, dec, pa) 2. For each SCA corner (x_idl, y_idl) in arcsec: (v2, v3) from IDL → tangent-plane → rotate by M → sky (ra_c, dec_c) """ import json, numpy as np, pysiaf, collections from pysiaf.utils.rotations import attitude_matrix from astropy.table import Table from astropy.coordinates import SkyCoord, Galactic import astropy.units as u ECSV = ( "/Users/adam/repos/roman_notebooks/notebooks/" "footprint_visualization/aux_data/roman_gps.sim.ecsv" ) OUT = "/Users/adam/work/roman/roman_footprint_rotatable.html" # ── pysiaf: extract SCA IDL vertices and reference point ───────────────────── print("Loading Roman SIAF …") rsiaf = pysiaf.Siaf("Roman") wfi_cen = rsiaf["WFI_CEN"] V2REF = float(wfi_cen.V2Ref) # arcsec V3REF = float(wfi_cen.V3Ref) # arcsec sensors = [rsiaf[f"WFI{j:02d}_FULL"] for j in range(1, 19)] # Each SCA: 4 V2/V3 corners in telescope frame (arcsec), converted from IDL SCA_VERTS = [] for s in sensors: v2s = []; v3s = [] for k in range(1, 5): xi = float(getattr(s, f"XIdlVert{k}")) yi = float(getattr(s, f"YIdlVert{k}")) v2, v3 = s.idl_to_tel(xi, yi) v2s.append(round(float(v2), 4)) v3s.append(round(float(v3), 4)) SCA_VERTS.append({"v2": v2s, "v3": v3s}) print(f" V2Ref={V2REF:+.1f}\" V3Ref={V3REF:+.1f}\" SCAs: {len(SCA_VERTS)}") # ── ECSV helpers ────────────────────────────────────────────────────────────── _BP_WAVE = {"F062": 0.62, "F087": 0.87, "F106": 1.06, "F129": 1.29, "F146": 1.46, "F158": 1.58, "F184": 1.84, "F213": 2.13, "GRISM": 1.50, "PRISM": 1.50} def unique_pointings(tbl, target): sub = tbl[tbl["TARGET_NAME"] == target] bps = sorted(set(str(b) for b in sub["BANDPASS"]), key=lambda x: _BP_WAVE.get(x, 0.0)) sub = sub[sub["BANDPASS"] == bps[-1]] pa_vals = [round(float(r["PA"]), 1) for r in sub] modal_pa = collections.Counter(pa_vals).most_common(1)[0][0] sub = [r for r, pa in zip(sub, pa_vals) if pa == modal_pa] pts = {} for r in sub: pts[(round(float(r["RA"]), 6), round(float(r["DEC"]), 6))] = True return list(sorted(pts)) def dedup_pointings(pts, sep=7.5): if len(pts) <= 1: return list(pts) ras = np.radians([p[0] for p in pts]) decs = np.radians([p[1] for p in pts]) sd, cd = np.sin(decs), np.cos(decs) thresh = np.radians(sep / 60.0) kept = [0] for i in range(1, len(pts)): cs = sd[i]*sd[kept] + cd[i]*cd[kept]*np.cos(ras[i] - ras[kept]) if np.all(np.arccos(np.clip(cs, -1, 1)) >= thresh): kept.append(i) return [pts[k] for k in kept] def gal_plane_pa(ra, dec): c = SkyCoord(ra=ra*u.deg, dec=dec*u.deg) c2 = SkyCoord(l=c.galactic.l + 0.2*u.deg, b=c.galactic.b, frame=Galactic()).icrs return round(c.position_angle(c2).deg, 3) # ── Build pointing-centre dataset ───────────────────────────────────────────── print("Reading roman_gps.sim.ecsv …") sim_table = Table.read(ECSV) all_targets = sorted(set(str(x) for x in sim_table["TARGET_NAME"])) print(f" {len(sim_table):,} rows · {len(all_targets)} targets") rgps_data = {} for tname in all_targets: all_pts = unique_pointings(sim_table, tname) pts = dedup_pointings(all_pts, sep=7.5) tiles = [] for ra, dec in pts: c = SkyCoord(ra=ra*u.deg, dec=dec*u.deg) gal = c.galactic tiles.append({ "ra": ra, "dec": dec, "l": round(float(gal.l.deg), 4), "b": round(float(gal.b.deg), 4), "pa_gal": gal_plane_pa(ra, dec), }) rgps_data[tname] = tiles print(f" {tname}: {len(pts)} pts") # ── GBTDS hardcoded tiles ───────────────────────────────────────────────────── _GBTDS_TILES = { 'Tile 1': (-0.62, -1.20, 267.2142, -30.0898), 'Tile 2': (-0.21, -1.20, 267.4568, -29.7391), 'Tile 3 (ref)': ( 0.20, -1.20, 267.6974, -29.3884), 'Tile 4': ( 0.60, -1.20, 267.9364, -29.0373), 'Tile 5': ( 1.01, -1.20, 268.1742, -28.6854), 'Tile 6': ( 1.42, -1.20, 268.4104, -28.3331), 'Tile 7 (GC)': ( 0.00, -0.12, 266.5270, -29.0013), } PA_SPRING = 90.6 PA_AUTUMN = 270.6 gbtds_centers = [ {"ra": ra, "dec": dec, "l": l, "b": b, "name": name, "pa_gal": gal_plane_pa(ra, dec)} for name, (l, b, ra, dec) in _GBTDS_TILES.items() ] # ── Layer metadata ───────────────────────────────────────────────────────────── GBTDS_META = { "color": "#4488ff", "label": "GBTDS (7 tiles)", "on": True, } RGPS_TDS_META = { "TDS_Galactic_Center_Lneg": {"color": "#ff7777", "label": "GC L<0", "on": False}, "TDS_Galactic_Center_Lpos": {"color": "#ffbb55", "label": "GC L>0", "on": False}, "TDS_GC_Neg_High+Hourly-Cadence": {"color": "#ff9999", "label": "GC L<0 (h-cad)", "on": True}, "TDS_GC_Pos_High+Hourly-Cadence": {"color": "#ffcc77", "label": "GC L>0 (h-cad)", "on": True}, "TDS_NGC6334_NGC6357": {"color": "#66dd66", "label": "NGC 6334/6357", "on": False}, "TDS_NGC6334_NGC6357_High-Cad": {"color": "#99ff77", "label": "NGC 6334 (h-cad)","on": False}, "TDS_W43": {"color": "#ff66ff", "label": "W43", "on": False}, "TDS_W43_High-Cadence": {"color": "#ffaaff", "label": "W43 (h-cad)", "on": False}, "TDS_Carina_High-Cadence": {"color": "#66aaff", "label": "Carina (h-cad)", "on": False}, "TDS_Carina_Region": {"color": "#99ccff", "label": "Carina Region", "on": False}, } RGPS_META = { "Bulge1_Bpos": {"color": "#ffe070", "label": "Bulge 1 b+", "on": False, "group": "bulge"}, "Bulge2_Bpos": {"color": "#ffd050", "label": "Bulge 2 b+", "on": False, "group": "bulge"}, "Bulge3_Bpos": {"color": "#ffc040", "label": "Bulge 3 b+", "on": False, "group": "bulge"}, "Bulge4_Bneg": {"color": "#ffb030", "label": "Bulge 4 b−", "on": False, "group": "bulge"}, "Bulge5_Bneg": {"color": "#ffa020", "label": "Bulge 5 b−", "on": False, "group": "bulge"}, "Bulge6_Bneg": {"color": "#ff9010", "label": "Bulge 6 b−", "on": False, "group": "bulge"}, "Bulge7_Bpos": {"color": "#ff7800", "label": "Bulge 7 b+", "on": False, "group": "bulge"}, "Bulge8_BNeg": {"color": "#ff6000", "label": "Bulge 8 b−", "on": False, "group": "bulge"}, "Disk1_Carina": {"color": "#80ffee", "label": "Disk 1 Carina","on": False, "group": "disk"}, "Disk2": {"color": "#60eedd", "label": "Disk 2", "on": False, "group": "disk"}, "Disk3": {"color": "#40ddcc", "label": "Disk 3", "on": False, "group": "disk"}, "Disk4": {"color": "#20ccbb", "label": "Disk 4", "on": False, "group": "disk"}, "Disk5": {"color": "#00bbaa", "label": "Disk 5", "on": False, "group": "disk"}, "Disk6": {"color": "#00aa99", "label": "Disk 6", "on": False, "group": "disk"}, "Disk7": {"color": "#009988", "label": "Disk 7", "on": False, "group": "disk"}, "Deep+Spec_ASSC_85": {"color": "#aad4ff", "label": "Deep ASSC 85", "on": False, "group": "deep"}, "Deep+Spec_Acrux": {"color": "#99c4ff", "label": "Deep Acrux", "on": False, "group": "deep"}, "Deep+Spec_G333": {"color": "#88b4ff", "label": "Deep G333", "on": False, "group": "deep"}, "Deep+Spec_M17_Omega": {"color": "#77a4ff", "label": "Deep M17/Omega", "on": False, "group": "deep"}, "Deep+Spec_NGC3324_Carina": {"color": "#6694ff", "label": "Deep NGC 3324", "on": False, "group": "deep"}, "Deep+Spec_NGC5269+NGC5281": {"color": "#5584ff", "label": "Deep NGC 5269/81", "on": False, "group": "deep"}, "Deep+Spec_NGC6357_Lobster": {"color": "#bbdeff", "label": "Deep NGC 6357", "on": False, "group": "deep"}, "Deep+Spec_Teutsch_84": {"color": "#ccdfff", "label": "Deep Teutsch 84", "on": False, "group": "deep"}, "Deep+Spec_Trumpler_35": {"color": "#dde8ff", "label": "Deep Trumpler 35", "on": False, "group": "deep"}, "Deep+Spec_VVV_CL001_UKS_1": {"color": "#ddf4ff", "label": "Deep VVV/UKS 1", "on": False, "group": "deep"}, "Deep+Spec_W40": {"color": "#aaf0ee", "label": "Deep W40", "on": False, "group": "deep"}, "Deep+Spec_W44": {"color": "#99eecc", "label": "Deep W44", "on": False, "group": "deep"}, "Deep+Spec_W51": {"color": "#88eebb", "label": "Deep W51", "on": False, "group": "deep"}, "Deep+Spec_Window_319.5_-0.2": {"color": "#77ddaa", "label": "Deep Win 319.5", "on": False, "group": "deep"}, "Deep+Spec_Window_355_-0.3": {"color": "#aaffdd", "label": "Deep Win 355", "on": False, "group": "deep"}, "Serpens_South": {"color": "#cc88ff", "label": "Serpens South", "on": False, "group": "other"}, } # ── Serialise ───────────────────────────────────────────────────────────────── sca_verts_js = json.dumps(SCA_VERTS, separators=(',', ':')) gbtds_js = json.dumps(gbtds_centers, separators=(',', ':')) rgps_js = json.dumps(rgps_data, separators=(',', ':')) tds_names = [t for t in all_targets if t.startswith("TDS_")] rgps_names = [t for t in all_targets if not t.startswith("TDS_")] def js_meta(meta_dict, keys): items = [] for k in keys: m = meta_dict.get(k, {}) color = json.dumps(m.get("color", "#aaaaaa")) on = "true" if m.get("on", False) else "false" items.append(f' {json.dumps(k)}: {{color:{color},on:{on}}}') return "{\n" + ",\n".join(items) + "\n}" rgps_tds_meta_js = js_meta(RGPS_TDS_META, tds_names) rgps_meta_js = js_meta(RGPS_META, rgps_names) def make_buttons(names, meta_dict, btn_class): html = "" for name in names: m = meta_dict.get(name, {"color": "#aaaaaa", "label": name, "on": False}) active = " active" if m.get("on") else "" html += (f'\n') return html def rgps_buttons_by_group(names, meta_dict): groups = {"bulge": [], "disk": [], "deep": [], "other": []} for name in names: g = meta_dict.get(name, {}).get("group", "other") groups[g].append(name) sections = [] labels = {"bulge": "RGPS Bulge", "disk": "RGPS Disk", "deep": "RGPS Deep+Spec", "other": "RGPS Other"} for gkey in ("bulge", "disk", "deep", "other"): ns = groups[gkey] if not ns: continue btns = make_buttons(ns, meta_dict, f"rgps-btn rgps-{gkey}") sections.append((gkey, labels[gkey], btns)) return sections tds_btn_html = make_buttons(tds_names, RGPS_TDS_META, "rgps-tds-btn") rgps_sections = rgps_buttons_by_group(rgps_names, RGPS_META) rgps_panel_html = "" for gkey, glabel, btns in rgps_sections: rgps_panel_html += f"""