Coverage for src / lstautorta / crab_cleanup.py: 0%

167 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-08-10 11:56 +0000

1#!/usr/bin/env python3 

2 

3""" 

4Scan DL1 HDF5 files, estimate telescope pointing vs Crab in AltAz, and decide KEEP/DELETE per obs_id. 

5Also collects associated logs and exports deletion/keep lists + CSV summary. 

6 

7Log file is timestamped to avoid overwriting between runs. 

8 

9Example : "python3 crab_cleanup.py --base-day-dir /fefs/onsite/pipeline/rta/data/2025/12/ --out-dir ./crab_cleanup_out --max-sep-deg 5.0 --tel-group tel_001 --step 200" 

10""" 

11 

12import argparse 

13import logging 

14import re 

15from datetime import UTC, datetime 

16from pathlib import Path 

17 

18import h5py 

19import numpy as np 

20import pandas as pd 

21from astropy import units as u 

22from astropy.coordinates import AltAz, EarthLocation, SkyCoord 

23from astropy.time import Time 

24 

25# ------------------------- 

26# Patterns / helpers 

27# ------------------------- 

28 

29DL1_RE = re.compile(r"^(dl1_|dl1_v).+\.h5$") 

30OBS_RE = re.compile(r"obs_id_(\d+)") 

31 

32 

33def is_dl1_file(p: Path) -> bool: 

34 return p.is_file() and DL1_RE.match(p.name) is not None 

35 

36 

37def extract_obs_id_from_name(filename: str): 

38 m = OBS_RE.search(filename) 

39 return int(m.group(1)) if m else None 

40 

41 

42def scan_dl1_files(day_dir: Path): 

43 """ 

44 Recursively scan all DL1 *.h5 under day_dir. 

45 Returns dict: obs_id -> list[Path] 

46 """ 

47 by_obs = {} 

48 for p in day_dir.rglob("*.h5"): 

49 if not is_dl1_file(p): 

50 continue 

51 obs = extract_obs_id_from_name(p.name) 

52 if obs is None: 

53 continue 

54 by_obs.setdefault(obs, []).append(p) 

55 

56 for obs in by_obs: 

57 by_obs[obs] = sorted(by_obs[obs]) 

58 return by_obs 

59 

60 

61# ------------------------- 

62# DL1 reading + separation 

63# ------------------------- 

64 

65 

66def open_dl1_get_pointing_and_time(dl1_path: Path, tel_group: str, step: int): 

67 """ 

68 Extract a subsample of (time, alt_tel, az_tel) from DL1. 

69 

70 Assumptions: 

71 - dl1/event/subarray/trigger/time = unix seconds (UTC) 

72 - dl1/event/telescope/parameters/<tel_group>/alt_tel, az_tel = radians 

73 """ 

74 with h5py.File(dl1_path, "r") as f: 

75 trig = f["dl1/event/subarray/trigger"] 

76 pars = f[f"dl1/event/telescope/parameters/{tel_group}"] 

77 

78 n = len(trig) 

79 if n == 0: 

80 return np.array([]), np.array([]), np.array([]) 

81 

82 idx = np.arange(0, n, step, dtype=int) 

83 

84 times_unix = trig["time"][idx].astype(float) 

85 alt_rad = pars["alt_tel"][idx].astype(float) 

86 az_rad = pars["az_tel"][idx].astype(float) 

87 

88 return times_unix, alt_rad, az_rad 

89 

90 

91def crab_separation_stats( 

92 dl1_path: Path, site: EarthLocation, crab: SkyCoord, tel_group: str, step: int, max_sep_deg: float 

93): 

94 """ 

95 Compute separation stats (deg) between telescope pointing and Crab in AltAz. 

96 Default decision: median separation < max_sep_deg -> KEEP else DELETE. 

97 """ 

98 times_unix, alt_tel, az_tel = open_dl1_get_pointing_and_time(dl1_path, tel_group=tel_group, step=step) 

99 

100 if len(times_unix) == 0: 

101 return { 

102 "sep_min_deg": np.nan, 

103 "sep_med_deg": np.nan, 

104 "sep_mean_deg": np.nan, 

105 "sep_max_deg": np.nan, 

106 "decision": "ERROR", 

107 "error": "no events", 

108 } 

109 

110 t = Time(times_unix, format="unix", scale="utc") 

111 

112 crab_altaz = crab.transform_to(AltAz(obstime=t, location=site)) 

113 tel_altaz = SkyCoord(az=az_tel * u.rad, alt=alt_tel * u.rad, frame=AltAz(obstime=t, location=site)) 

114 

115 sep_deg = tel_altaz.separation(crab_altaz).to_value(u.deg) 

116 

117 stats = { 

118 "sep_min_deg": float(np.min(sep_deg)), 

119 "sep_med_deg": float(np.median(sep_deg)), 

120 "sep_mean_deg": float(np.mean(sep_deg)), 

121 "sep_max_deg": float(np.max(sep_deg)), 

122 "error": "", 

123 } 

124 

125 keep = stats["sep_med_deg"] < max_sep_deg 

126 stats["decision"] = "KEEP" if keep else "DELETE" 

127 return stats 

128 

129 

130# ------------------------- 

131# Logs discovery 

132# ------------------------- 

133 

134 

135def find_obs_root_from_dl1(dl1_path: Path, obs_id: int) -> Path | None: 

136 """ 

137 Try to locate the directory path that corresponds to ".../<obs_id>/" within dl1_path. 

138 """ 

139 parts = dl1_path.parts 

140 obs_str = str(obs_id) 

141 if obs_str not in parts: 

142 return None 

143 i = parts.index(obs_str) 

144 return Path(*parts[: i + 1]) 

145 

146 

147def find_logs_for_dl1(dl1_path: Path, log_subdir: Path): 

148 """ 

149 Look for logs in <obs_root>/<log_subdir>/ matching: 

150 <dl1_prefix>.o-* and <dl1_prefix>.e-* 

151 """ 

152 obs = extract_obs_id_from_name(dl1_path.name) 

153 if obs is None: 

154 return [] 

155 

156 obs_root = find_obs_root_from_dl1(dl1_path, obs) 

157 if obs_root is None: 

158 return [] 

159 

160 log_dir = obs_root / log_subdir 

161 if not log_dir.exists(): 

162 return [] 

163 

164 prefix = dl1_path.stem 

165 logs = [] 

166 logs.extend(sorted(log_dir.glob(f"{prefix}.o-*"))) 

167 logs.extend(sorted(log_dir.glob(f"{prefix}.e-*"))) 

168 return [p for p in logs] 

169 

170 

171# ------------------------- 

172# Main 

173# ------------------------- 

174 

175 

176def setup_logger(out_dir: Path, prefix: str = "crab_pointing_cleanup"): 

177 out_dir.mkdir(parents=True, exist_ok=True) 

178 

179 # Timestamp (UTC) in filename to avoid overwriting 

180 ts = datetime.now(UTC).strftime("%Y%m%d_%H%M%S_UTC") 

181 log_path = out_dir / f"{prefix}_{ts}.log" 

182 

183 logger = logging.getLogger("crab_cleanup") 

184 logger.setLevel(logging.INFO) 

185 logger.handlers.clear() 

186 

187 fmt = logging.Formatter("%(asctime)s | %(levelname)s | %(message)s") 

188 

189 fh = logging.FileHandler(log_path) 

190 fh.setLevel(logging.INFO) 

191 fh.setFormatter(fmt) 

192 

193 sh = logging.StreamHandler() 

194 sh.setLevel(logging.INFO) 

195 sh.setFormatter(fmt) 

196 

197 logger.addHandler(fh) 

198 logger.addHandler(sh) 

199 

200 logger.info("Log file: %s", log_path) 

201 logger.info("Script start UTC: %s", datetime.now(UTC).isoformat()) 

202 return logger, log_path 

203 

204 

205def main(): 

206 parser = argparse.ArgumentParser(description="KEEP/DELETE DL1 based on telescope pointing separation to Crab.") 

207 parser.add_argument( 

208 "--base-day-dir", 

209 type=Path, 

210 required=True, 

211 help="Day directory to scan (e.g. /.../2026/01/20 or /.../2026/01/)", 

212 ) 

213 parser.add_argument( 

214 "--max-sep-deg", type=float, default=5.0, help="Max median separation (deg) to KEEP (default: 5.0)" 

215 ) 

216 parser.add_argument( 

217 "--tel-group", type=str, default="tel_001", help="HDF5 telescope group name (default: tel_001)" 

218 ) 

219 parser.add_argument("--step", type=int, default=200, help="Subsampling step (default: 200)") 

220 parser.add_argument( 

221 "--log-subdir", 

222 type=Path, 

223 default=Path("logs/dl1_alt_az"), 

224 help="Logs path relative to obs_id directory (default: logs/dl1_alt_az)", 

225 ) 

226 parser.add_argument( 

227 "--out-dir", 

228 type=Path, 

229 default=Path(), 

230 help="Output directory for txt/csv/log files (default: current directory)", 

231 ) 

232 parser.add_argument("--orm-height-m", type=float, default=2200.0, help="Site height in meters (default: 2200)") 

233 args = parser.parse_args() 

234 

235 logger, _ = setup_logger(args.out_dir) 

236 

237 base_day_dir = args.base_day_dir 

238 if not base_day_dir.exists(): 

239 raise FileNotFoundError(f"base-day-dir does not exist: {base_day_dir}") 

240 

241 # ORM / La Palma (LST-1) 

242 site = EarthLocation(lat=28.7617 * u.deg, lon=-17.89 * u.deg, height=args.orm_height_m * u.m) 

243 

244 # Crab (ICRS J2000) 

245 crab = SkyCoord(ra=83.6331 * u.deg, dec=22.0145 * u.deg, frame="icrs") 

246 

247 logger.info("Scanning DL1 under: %s", base_day_dir) 

248 dl1_by_obs = scan_dl1_files(base_day_dir) 

249 logger.info("Unique obs_id: %d", len(dl1_by_obs)) 

250 logger.info("Example obs_id: %s", list(dl1_by_obs.keys())[:5]) 

251 

252 rows = [] 

253 for obs_id, files in sorted(dl1_by_obs.items()): 

254 rep = files[0] 

255 try: 

256 stats = crab_separation_stats( 

257 rep, site=site, crab=crab, tel_group=args.tel_group, step=args.step, max_sep_deg=args.max_sep_deg 

258 ) 

259 row = {"obs_id": obs_id, "rep_file": str(rep), **stats} 

260 rows.append(row) 

261 logger.info( 

262 "obs_id=%s rep=%s decision=%s sep_med=%.3f deg", 

263 obs_id, 

264 rep.name, 

265 row["decision"], 

266 row["sep_med_deg"] if np.isfinite(row["sep_med_deg"]) else float("nan"), 

267 ) 

268 except Exception as e: 

269 rows.append( 

270 { 

271 "obs_id": obs_id, 

272 "rep_file": str(rep), 

273 "sep_min_deg": np.nan, 

274 "sep_med_deg": np.nan, 

275 "sep_mean_deg": np.nan, 

276 "sep_max_deg": np.nan, 

277 "decision": "ERROR", 

278 "error": repr(e), 

279 } 

280 ) 

281 logger.exception("obs_id=%s rep=%s ERROR: %r", obs_id, rep.name, e) 

282 

283 df = pd.DataFrame(rows).sort_values(["decision", "obs_id"]) 

284 logger.info("Decision counts:\n%s", df["decision"].value_counts(dropna=False).to_string()) 

285 

286 decision_by_obs = {int(r.obs_id): r.decision for _, r in df.iterrows()} 

287 

288 to_delete_dl1 = [] 

289 to_delete_logs = [] 

290 to_keep_dl1 = [] 

291 

292 for obs_id, files in dl1_by_obs.items(): 

293 dec = decision_by_obs.get(obs_id, "ERROR") 

294 if dec == "KEEP": 

295 to_keep_dl1.extend(files) 

296 elif dec == "DELETE": 

297 to_delete_dl1.extend(files) 

298 for f in files: 

299 to_delete_logs.extend(find_logs_for_dl1(f, log_subdir=args.log_subdir)) 

300 

301 to_delete_dl1 = sorted(set(to_delete_dl1)) 

302 to_delete_logs = sorted(set(to_delete_logs)) 

303 to_keep_dl1 = sorted(set(to_keep_dl1)) 

304 

305 logger.info("DL1 to delete: %d", len(to_delete_dl1)) 

306 logger.info("Logs to delete: %d", len(to_delete_logs)) 

307 logger.info("DL1 to keep: %d", len(to_keep_dl1)) 

308 

309 # Outputs 

310 out_dir = args.out_dir 

311 out_del_dl1 = out_dir / "to_delete_dl1.txt" 

312 out_del_logs = out_dir / "to_delete_logs.txt" 

313 out_keep = out_dir / "to_keep_dl1.txt" 

314 out_csv = out_dir / "obsid_crab_pointing_summary.csv" 

315 

316 out_del_dl1.write_text("\n".join(str(p) for p in to_delete_dl1) + ("\n" if to_delete_dl1 else "")) 

317 out_del_logs.write_text("\n".join(str(p) for p in to_delete_logs) + ("\n" if to_delete_logs else "")) 

318 out_keep.write_text("\n".join(str(p) for p in to_keep_dl1) + ("\n" if to_keep_dl1 else "")) 

319 df.to_csv(out_csv, index=False) 

320 

321 logger.info("Wrote: %s", out_del_dl1) 

322 logger.info("Wrote: %s", out_del_logs) 

323 logger.info("Wrote: %s", out_keep) 

324 logger.info("Wrote: %s", out_csv) 

325 

326 keep_obs = df.loc[df["decision"] == "KEEP", "obs_id"].to_list() 

327 logger.info("KEEP obs_id list: %s", keep_obs) 

328 

329 # Shell helpers 

330 logger.info("### Dry-run command:") 

331 logger.info('cat "%s" "%s" | while read f; do echo rm -f "$f"; done', out_del_dl1, out_del_logs) 

332 

333 logger.info("### Delete command:") 

334 logger.info('cat "%s" "%s" | while read f; do rm -f "$f"; done', out_del_dl1, out_del_logs) 

335 

336 

337if __name__ == "__main__": 

338 main()