File indexing completed on 2026-04-10 07:49:36
0001
0002 """
0003 photon_history_summary.py : Debug analysis of opticks GPU simulation output
0004 =================================================================
0005
0006 Reads photon.npy, hit.npy, inphoton.npy, record.npy, seq.npy from
0007 an opticks event folder and prints summary tables for debugging.
0008
0009 Requires OPTICKS_EVENT_MODE=HitPhoton (or DebugLite/DebugHeavy) so
0010 that photon and record arrays are actually saved to disk.
0011
0012 Usage::
0013
0014 python optiphy/ana/photon_history_summary.py /tmp/MISSING_USER/opticks/GEOM/GEOM/GPUPhotonSourceMinimal/ALL0_no_opticks_event_name/A000
0015
0016 # or just the parent (auto-selects A000):
0017 python optiphy/ana/photon_history_summary.py /tmp/MISSING_USER/opticks/GEOM/GEOM/GPUPhotonSourceMinimal/ALL0_no_opticks_event_name
0018
0019 # show per-photon detail for first 20 photons:
0020 python optiphy/ana/photon_history_summary.py <path> --detail 20
0021
0022 # show step-by-step record for specific photons:
0023 python optiphy/ana/photon_history_summary.py <path> --trace 0,227,235
0024
0025 # filter by terminal flag:
0026 python optiphy/ana/photon_history_summary.py <path> --flag BOUNDARY_REFLECT
0027
0028 # show all non-hit (absorbed/lost) photons:
0029 python optiphy/ana/photon_history_summary.py <path> --lost
0030 """
0031 import sys
0032 import os
0033 import argparse
0034 import numpy as np
0035
0036
0037 FLAG_ENUM = {
0038 0x00001: "CERENKOV",
0039 0x00002: "SCINTILLATION",
0040 0x00004: "TORCH",
0041 0x00008: "BULK_ABSORB",
0042 0x00010: "BULK_REEMIT",
0043 0x00020: "BULK_SCATTER",
0044 0x00040: "SURFACE_DETECT",
0045 0x00080: "SURFACE_ABSORB",
0046 0x00100: "SURFACE_DREFLECT",
0047 0x00200: "SURFACE_SREFLECT",
0048 0x00400: "BOUNDARY_REFLECT",
0049 0x00800: "BOUNDARY_TRANSMIT",
0050 0x01000: "NAN_ABORT",
0051 0x02000: "EFFICIENCY_COLLECT",
0052 0x04000: "EFFICIENCY_CULL",
0053 0x08000: "MISS",
0054 0x10000: "__NATURAL",
0055 0x20000: "__MACHINERY",
0056 0x40000: "__EMITSOURCE",
0057 0x80000: "PRIMARYSOURCE",
0058 0x100000: "GENSTEPSOURCE",
0059 0x200000: "DEFER_FSTRACKINFO",
0060 }
0061
0062 FLAG_ABBREV = {
0063 0x00001: "CK",
0064 0x00002: "SI",
0065 0x00004: "TO",
0066 0x00008: "AB",
0067 0x00010: "RE",
0068 0x00020: "SC",
0069 0x00040: "SD",
0070 0x00080: "SA",
0071 0x00100: "DR",
0072 0x00200: "SR",
0073 0x00400: "BR",
0074 0x00800: "BT",
0075 0x01000: "NA",
0076 0x02000: "EC",
0077 0x04000: "EL",
0078 0x08000: "MI",
0079 0x10000: "Nat",
0080 0x20000: "Mac",
0081 0x40000: "Emi",
0082 0x80000: "PS",
0083 0x100000: "GS",
0084 0x200000: "DF",
0085 }
0086
0087
0088
0089
0090 HIT_FLAGS = {0x0040, 0x2000}
0091
0092
0093 FFS_TO_FLAG = {i + 1: 1 << i for i in range(16)}
0094
0095
0096 def flag_name(f):
0097 return FLAG_ENUM.get(f, f"0x{f:04x}")
0098
0099
0100 def flag_abbrev(f):
0101 return FLAG_ABBREV.get(f, f"?{f:x}")
0102
0103
0104 def flagmask_str(fm):
0105 """Decode cumulative flagmask into abbreviated flag names."""
0106 parts = []
0107 for bit in range(22):
0108 f = 1 << bit
0109 if fm & f:
0110 parts.append(flag_abbrev(f))
0111 return "|".join(parts)
0112
0113
0114 def decode_seq_history(seqhis):
0115 """Decode seq nibbles into list of (flag_value, abbrev) tuples."""
0116 steps = []
0117 for slot in range(32):
0118 iseq = slot // 16
0119 shift = 4 * (slot - iseq * 16)
0120 if iseq == 0:
0121 nibble = (seqhis[0] >> shift) & 0xF
0122 else:
0123 nibble = (seqhis[1] >> shift) & 0xF
0124 if nibble == 0:
0125 break
0126 f = FFS_TO_FLAG.get(nibble, 0)
0127 steps.append((f, flag_abbrev(f)))
0128 return steps
0129
0130
0131 def extract_photon_fields(photon):
0132 """Extract decoded fields from photon array (N, 4, 4) float32."""
0133 q3 = photon[:, 3, :].view(np.uint32)
0134 obf = q3[:, 0]
0135 flag = obf & 0xFFFF
0136 boundary = (obf >> 16) & 0x7FFF
0137 orient = (obf >> 31).astype(np.int8)
0138 orient = np.where(orient, -1, 1)
0139 flagmask = q3[:, 3]
0140 identity = q3[:, 1]
0141 index = q3[:, 2] & 0x7FFFFFFF
0142
0143 pos = photon[:, 0, :3]
0144 time = photon[:, 0, 3]
0145 mom = photon[:, 1, :3]
0146 pol = photon[:, 2, :3]
0147 wavelength = photon[:, 2, 3]
0148
0149 return dict(
0150 pos=pos, time=time, mom=mom, pol=pol, wavelength=wavelength,
0151 flag=flag, boundary=boundary, orient=orient,
0152 flagmask=flagmask, identity=identity, index=index,
0153 )
0154
0155
0156 def count_record_steps(record):
0157 """Count non-zero steps per photon in record array (N, maxstep, 4, 4)."""
0158 n, maxstep = record.shape[:2]
0159
0160 step_nonzero = np.any(record.reshape(n, maxstep, -1) != 0, axis=2)
0161
0162
0163 nsteps = step_nonzero.sum(axis=1).astype(int)
0164 return nsteps
0165
0166
0167 def load_event(path):
0168 """Load arrays from an opticks event folder. Returns dict of arrays."""
0169 arrays = {}
0170 names = ["photon", "hit", "inphoton", "record", "seq", "genstep", "domain"]
0171 for name in names:
0172 fpath = os.path.join(path, f"{name}.npy")
0173 if os.path.exists(fpath):
0174 arrays[name] = np.load(fpath)
0175 return arrays
0176
0177
0178 def read_hitmask(path):
0179 """Read hitmask from event metadata. Returns int or None."""
0180 meta_path = os.path.join(path, "NPFold_meta.txt")
0181 if not os.path.exists(meta_path):
0182 return None
0183 with open(meta_path) as f:
0184 for line in f:
0185 if line.startswith("hitmask:"):
0186 parts = line.split(":", 1)
0187 if len(parts) < 2:
0188 return None
0189 value = parts[1].strip()
0190 if not value:
0191 return None
0192 try:
0193
0194 return int(value, 0)
0195 except ValueError:
0196 return None
0197 return None
0198
0199
0200 def resolve_event_path(path):
0201 """Resolve event folder path, auto-selecting A000 if needed."""
0202 if os.path.exists(os.path.join(path, "photon.npy")):
0203 return path
0204 a000 = os.path.join(path, "A000")
0205 if os.path.exists(os.path.join(a000, "photon.npy")):
0206 return a000
0207
0208 if os.path.isdir(path):
0209 for d in sorted(os.listdir(path)):
0210 dp = os.path.join(path, d)
0211 if os.path.isdir(dp) and os.path.exists(os.path.join(dp, "photon.npy")):
0212 return dp
0213 return path
0214
0215
0216 def print_overview(arrays):
0217 """Print overview table of loaded arrays."""
0218 print("=" * 70)
0219 print("LOADED ARRAYS")
0220 print("=" * 70)
0221 for name, arr in sorted(arrays.items()):
0222 print(f" {name:12s} shape={str(arr.shape):24s} dtype={arr.dtype}")
0223 print()
0224
0225
0226 def print_outcome_table(pf, n_photons, n_hits, hitmask, arrays):
0227 """Print table of photon outcomes by terminal flag."""
0228 print("=" * 70)
0229 print("PHOTON OUTCOMES (by terminal flag)")
0230 print("=" * 70)
0231
0232 hm_str = flagmask_str(hitmask) if hitmask else "unknown"
0233 print(f" Hitmask: {hm_str} (0x{hitmask:x})" if hitmask else " Hitmask: not found in metadata")
0234
0235 flag_vals, flag_counts = np.unique(pf["flag"], return_counts=True)
0236 order = np.argsort(-flag_counts)
0237
0238 print(f"\n {'Flag':<22s} {'Count':>7s} {'%':>7s} {'Boundary indices'}")
0239 print(f" {'-'*22} {'-'*7} {'-'*7} {'-'*30}")
0240 for idx in order:
0241 f = flag_vals[idx]
0242 c = flag_counts[idx]
0243 mask = pf["flag"] == f
0244 bnd_vals = np.unique(pf["boundary"][mask])
0245 bnd_str = ", ".join(str(b) for b in bnd_vals)
0246 pct = 100.0 * c / n_photons
0247 print(f" {flag_name(f):<22s} {c:7d} {pct:6.1f}% bnd=[{bnd_str}]")
0248
0249 hit_rate = 100.0 * n_hits / n_photons if n_photons > 0 else 0.0
0250 print(f"\n Total photons: {n_photons} Hits: {n_hits} "
0251 f"Lost: {n_photons - n_hits} Hit rate: {hit_rate:.1f}%")
0252
0253
0254 if "record" in arrays:
0255 record = arrays["record"]
0256 maxstep = record.shape[1]
0257 nsteps = count_record_steps(record)
0258 n_truncated = int(np.sum(nsteps == maxstep))
0259 if n_truncated > 0:
0260 print(f" Truncated at MaxBounce ({maxstep} steps): {n_truncated}")
0261 print()
0262
0263
0264 def print_flagmask_table(pf, n_photons):
0265 """Print table of unique cumulative flagmask histories."""
0266 print("=" * 70)
0267 print("PHOTON HISTORIES (by cumulative flagmask)")
0268 print("=" * 70)
0269
0270 fm_vals, fm_counts = np.unique(pf["flagmask"], return_counts=True)
0271 order = np.argsort(-fm_counts)
0272
0273 print(f" {'Flagmask':<40s} {'Count':>7s} {'%':>7s}")
0274 print(f" {'-'*40} {'-'*7} {'-'*7}")
0275 for idx in order:
0276 fm = fm_vals[idx]
0277 c = fm_counts[idx]
0278 pct = 100.0 * c / n_photons
0279 desc = flagmask_str(fm)
0280 print(f" {desc:<40s} {c:7d} {pct:6.1f}%")
0281 print()
0282
0283
0284 def print_wavelength_table(pf, arrays):
0285 """Print wavelength shift analysis."""
0286 print("=" * 70)
0287 print("WAVELENGTH ANALYSIS")
0288 print("=" * 70)
0289
0290 wl = pf["wavelength"]
0291 print(f" Final photons: min={wl.min():.1f} max={wl.max():.1f} "
0292 f"mean={wl.mean():.1f} std={wl.std():.1f} nm")
0293
0294 if "inphoton" in arrays:
0295 inp_wl = arrays["inphoton"][:, 2, 3]
0296 print(f" Input photons: min={inp_wl.min():.1f} max={inp_wl.max():.1f} "
0297 f"mean={inp_wl.mean():.1f} std={inp_wl.std():.1f} nm")
0298
0299 shifted = np.sum(np.abs(wl - inp_wl) > 0.1)
0300 print(f" Wavelength shifted: {shifted}/{len(wl)}")
0301
0302 if "hit" in arrays and len(arrays["hit"]) > 0:
0303 hit_wl = arrays["hit"][:, 2, 3]
0304 print(f" Hit wavelength: min={hit_wl.min():.1f} max={hit_wl.max():.1f} "
0305 f"mean={hit_wl.mean():.1f} std={hit_wl.std():.1f} nm")
0306 elif "hit" in arrays:
0307 print(f" Hit wavelength: no hits")
0308
0309
0310 hc = 1239.84193
0311 if "inphoton" in arrays:
0312 inp_wl = arrays["inphoton"][:, 2, 3]
0313 inp_E = hc / inp_wl
0314 out_E = hc / wl
0315 violations = np.sum(out_E > inp_E + 0.001)
0316 print(f" Energy conservation violations: {violations}")
0317 print()
0318
0319
0320 def print_position_table(pf):
0321 """Print position statistics."""
0322 print("=" * 70)
0323 print("POSITION / TIME")
0324 print("=" * 70)
0325
0326 pos = pf["pos"]
0327 r = np.sqrt(np.sum(pos ** 2, axis=1))
0328 t = pf["time"]
0329
0330 print(f" Radius: min={r.min():.2f} max={r.max():.2f} mean={r.mean():.2f} mm")
0331 print(f" Time: min={t.min():.3f} max={t.max():.3f} mean={t.mean():.3f} ns")
0332 print()
0333
0334
0335 def print_seq_table(arrays, n_show=15):
0336 """Print sequence history table from seq.npy."""
0337 if "seq" not in arrays:
0338 return
0339
0340 seq = arrays["seq"]
0341 n = seq.shape[0]
0342 print("=" * 70)
0343 print(f"STEP SEQUENCE HISTORIES (top {n_show})")
0344 print("=" * 70)
0345
0346
0347 seqhis = seq[:, 0, :]
0348
0349
0350 unique_seqhis, counts = np.unique(seqhis, axis=0, return_counts=True)
0351 order = np.argsort(-counts)
0352
0353
0354 print(f" {'#':>4s} {'Count':>7s} {'%':>7s} {'Sequence'}")
0355 print(f" {'-'*4} {'-'*7} {'-'*7} {'-'*40}")
0356 for rank, idx in enumerate(order[:n_show]):
0357 c = counts[idx]
0358 pct = 100.0 * c / n
0359 steps = decode_seq_history(unique_seqhis[idx])
0360 label = " ".join(abbr for _, abbr in steps)
0361 print(f" {rank:4d} {c:7d} {pct:6.1f}% {label}")
0362
0363 if len(order) > n_show:
0364 print(f" ... ({len(order)} unique sequences total)")
0365 print()
0366
0367 def print_record_steps_table(arrays):
0368 """Print step count distribution from record.npy."""
0369 if "record" not in arrays:
0370 return
0371
0372 record = arrays["record"]
0373 nsteps = count_record_steps(record)
0374
0375 print("=" * 70)
0376 print("RECORD STEP COUNTS")
0377 print("=" * 70)
0378
0379 step_vals, step_counts = np.unique(nsteps, return_counts=True)
0380 maxstep = record.shape[1]
0381
0382 print(f" {'Steps':>6s} {'Count':>7s} {'%':>7s} {'Note'}")
0383 print(f" {'-'*6} {'-'*7} {'-'*7} {'-'*20}")
0384 for sv, sc in zip(step_vals, step_counts):
0385 pct = 100.0 * sc / len(nsteps)
0386 note = " <-- max (truncated)" if sv == maxstep else ""
0387 print(f" {sv:6d} {sc:7d} {pct:6.1f}%{note}")
0388
0389 print(f"\n Mean steps: {nsteps.mean():.1f} Max allowed: {maxstep}")
0390 print()
0391
0392
0393 def print_photon_detail(arrays, pf, indices):
0394 """Print detailed per-photon info for given indices."""
0395 print("=" * 70)
0396 print("PHOTON DETAIL")
0397 print("=" * 70)
0398
0399 record = arrays.get("record")
0400 seq = arrays.get("seq")
0401 n_photons = len(pf["flag"])
0402
0403 for i in indices:
0404 if i >= n_photons:
0405 print(f"\n photon[{i}]: index out of range (max {n_photons - 1})")
0406 continue
0407
0408 pos = pf["pos"][i]
0409 wl = pf["wavelength"][i]
0410 t = pf["time"][i]
0411 f = pf["flag"][i]
0412 bnd = pf["boundary"][i]
0413 fm = pf["flagmask"][i]
0414
0415 r = np.sqrt(np.sum(pos ** 2))
0416
0417 print(f"\n photon[{i}]")
0418 print(f" Final: pos=({pos[0]:.2f}, {pos[1]:.2f}, {pos[2]:.2f}) r={r:.2f}mm"
0419 f" t={t:.3f}ns wl={wl:.1f}nm")
0420 print(f" Flag: {flag_name(f)} bnd={bnd} flagmask={flagmask_str(fm)}")
0421
0422 if seq is not None:
0423 steps = decode_seq_history(seq[i, 0, :])
0424 seq_str = " ".join(abbr for _, abbr in steps)
0425 print(f" Seq: {seq_str}")
0426
0427 if record is not None:
0428 rec = record[i]
0429 maxstep = rec.shape[0]
0430 print(f" {'Step':>4s} {'Flag':<18s} {'Bnd':>3s} {'Position (x,y,z)':^30s}"
0431 f" {'r':>8s} {'Time':>8s} {'WL':>7s}")
0432 print(f" {'-'*4} {'-'*18} {'-'*3} {'-'*30} {'-'*8} {'-'*8} {'-'*7}")
0433
0434 for s in range(maxstep):
0435 if np.all(rec[s] == 0):
0436 break
0437 sq3 = rec[s, 3, :].view(np.uint32)
0438 sf = sq3[0] & 0xFFFF
0439 sb = (sq3[0] >> 16) & 0x7FFF
0440 spos = rec[s, 0, :3]
0441 st = rec[s, 0, 3]
0442 swl = rec[s, 2, 3]
0443 sr = np.sqrt(np.sum(spos ** 2))
0444 print(f" {s:4d} {flag_name(sf):<18s} {sb:3d}"
0445 f" ({spos[0]:9.2f},{spos[1]:9.2f},{spos[2]:9.2f})"
0446 f" {sr:8.2f} {st:8.3f} {swl:7.1f}")
0447 print()
0448
0449
0450 def print_lost_photons(arrays, pf, hitmask):
0451 """Print detail for all non-detected photons.
0452
0453 A photon is "lost" if its terminal flag is not in the hit set.
0454 The hitmask is read from event metadata (default: SURFACE_DETECT).
0455 With CustomART PMTs the hitmask may be EFFICIENCY_COLLECT instead.
0456 """
0457
0458
0459
0460 if hitmask is not None:
0461 lost_mask = (pf["flagmask"] & hitmask) != hitmask
0462 else:
0463
0464 lost_mask = np.ones(len(pf["flag"]), dtype=bool)
0465 for hf in HIT_FLAGS:
0466 lost_mask &= pf["flag"] != hf
0467
0468 lost_idx = np.where(lost_mask)[0]
0469
0470 if len(lost_idx) == 0:
0471 print("No lost photons — all photons were detected.\n")
0472 return
0473
0474 print("=" * 70)
0475 hm_str = flagmask_str(hitmask) if hitmask else "SD|EC"
0476 print(f"LOST PHOTONS ({len(lost_idx)} non-detected, hitmask={hm_str})")
0477 print("=" * 70)
0478
0479
0480 lost_flags = pf["flag"][lost_mask]
0481 fvals, fcounts = np.unique(lost_flags, return_counts=True)
0482 for fv, fc in zip(fvals, fcounts):
0483 print(f" {flag_name(fv):<22s}: {fc}")
0484
0485
0486 if "record" in arrays:
0487 record = arrays["record"]
0488 maxstep = record.shape[1]
0489 nsteps = count_record_steps(record)
0490 lost_at_max = np.sum(nsteps[lost_idx] == maxstep)
0491 if lost_at_max > 0:
0492 print(f"\n Note: {lost_at_max} lost photon(s) hit the {maxstep}-step"
0493 f" bounce limit (trapped by total internal reflection?)")
0494 print()
0495
0496 print_photon_detail(arrays, pf, lost_idx)
0497
0498
0499 def expected_output_path(executable="GPUPhotonSourceMinimal"):
0500 """Compute the expected opticks output path from environment variables.
0501
0502 Opticks saves output to:
0503 $TMP/GEOM/$GEOM/$ExecutableName/ALL0_no_opticks_event_name/A000/
0504
0505 Where:
0506 $TMP defaults to /tmp/$USER/opticks
0507 $GEOM defaults to GEOM
0508 """
0509 tmp = os.environ.get("TMP")
0510 if tmp is None:
0511 user = os.environ.get("USER", "MISSING_USER")
0512 tmp = f"/tmp/{user}/opticks"
0513 geom = os.environ.get("GEOM", "GEOM")
0514 return os.path.join(tmp, "GEOM", geom, executable,
0515 "ALL0_no_opticks_event_name", "A000")
0516
0517
0518 def print_output_path_help():
0519 """Print where opticks writes output files."""
0520 print("OUTPUT FILE LOCATION")
0521 print("=" * 70)
0522 print()
0523 print("Opticks saves .npy arrays to:")
0524 print()
0525 print(" $TMP/GEOM/$GEOM/<ExecutableName>/ALL0_no_opticks_event_name/A000/")
0526 print()
0527 tmp = os.environ.get("TMP")
0528 if tmp is None:
0529 user = os.environ.get("USER", "MISSING_USER")
0530 tmp = f"/tmp/{user}/opticks"
0531 print(f" $TMP is not set, defaults to: {tmp}")
0532 else:
0533 print(f" $TMP = {tmp}")
0534 geom = os.environ.get("GEOM")
0535 if geom is None:
0536 print(f" $GEOM is not set, defaults to: GEOM")
0537 geom = "GEOM"
0538 else:
0539 print(f" $GEOM = {geom}")
0540 print()
0541 print("For common executables, the expected paths are:")
0542 print()
0543 for exe in ("GPUPhotonSourceMinimal", "GPUPhotonSource", "GPUPhotonFileSource"):
0544 p = expected_output_path(exe)
0545 print(f" {exe}:")
0546 print(f" {p}")
0547 print()
0548
0549
0550 def check_event_mode():
0551 """Check that OPTICKS_EVENT_MODE is set to a mode that saves output arrays."""
0552 valid_modes = ("HitPhoton", "DebugLite", "DebugHeavy")
0553 mode = os.environ.get("OPTICKS_EVENT_MODE")
0554 if mode is None:
0555 print("ERROR: OPTICKS_EVENT_MODE environment variable is not set.")
0556 print()
0557 print("This script requires photon/record arrays that are only saved")
0558 print("when OPTICKS_EVENT_MODE is set to one of:")
0559 print()
0560 for m in valid_modes:
0561 print(f" export OPTICKS_EVENT_MODE={m}")
0562 print()
0563 print("The default mode (Minimal) gathers hits into memory but does")
0564 print("NOT save photon.npy, record.npy, or other arrays to disk.")
0565 print()
0566 print("Set the variable before running the simulation, e.g.:")
0567 print()
0568 print(" OPTICKS_EVENT_MODE=HitPhoton GPUPhotonSourceMinimal -g geo.gdml -c cfg -m run.mac")
0569 print()
0570 print_output_path_help()
0571 sys.exit(1)
0572 if mode not in valid_modes:
0573 print(f"ERROR: OPTICKS_EVENT_MODE={mode} does not save full output arrays.")
0574 print()
0575 print("This script requires OPTICKS_EVENT_MODE set to one of:")
0576 print()
0577 for m in valid_modes:
0578 print(f" export OPTICKS_EVENT_MODE={m}")
0579 print()
0580 print(f"Current value '{mode}' may not save photon.npy or record.npy.")
0581 print()
0582 print_output_path_help()
0583 sys.exit(1)
0584
0585
0586 def main():
0587 parser = argparse.ArgumentParser(
0588 description="Opticks GPU simulation output analysis",
0589 formatter_class=argparse.RawDescriptionHelpFormatter,
0590 epilog=__doc__,
0591 )
0592 parser.add_argument("path", help="Path to opticks event folder (containing photon.npy)")
0593 parser.add_argument("--detail", type=int, metavar="N", default=0,
0594 help="Show per-photon detail for first N photons")
0595 parser.add_argument("--trace", type=str, metavar="I,J,...", default=None,
0596 help="Show step-by-step record for specific photon indices")
0597 parser.add_argument("--flag", type=str, metavar="FLAG", default=None,
0598 help="Filter: only show photons with this terminal flag (e.g. BULK_ABSORB)")
0599 parser.add_argument("--lost", action="store_true",
0600 help="Show all non-detected (lost) photons with step traces")
0601 parser.add_argument("--seq-top", type=int, metavar="N", default=15,
0602 help="Number of top sequence histories to show (default: 15)")
0603
0604 args = parser.parse_args()
0605
0606 path = resolve_event_path(args.path)
0607 if not os.path.exists(os.path.join(path, "photon.npy")):
0608 print(f"Error: photon.npy not found in {path}")
0609 print()
0610 print("Make sure the simulation was run with OPTICKS_EVENT_MODE=HitPhoton")
0611 print("(or DebugLite/DebugHeavy) so that output arrays are saved to disk.")
0612 print()
0613 print_output_path_help()
0614 sys.exit(1)
0615
0616 arrays = load_event(path)
0617 hitmask = read_hitmask(path)
0618 print(f"\nEvent folder: {path}\n")
0619 print_overview(arrays)
0620
0621 photon = arrays["photon"]
0622 pf = extract_photon_fields(photon)
0623 n_photons = len(pf["flag"])
0624 n_hits = len(arrays["hit"]) if "hit" in arrays else 0
0625
0626
0627 if args.flag:
0628
0629 flag_val = None
0630 for v, name in FLAG_ENUM.items():
0631 if name == args.flag.upper():
0632 flag_val = v
0633 break
0634 if flag_val is None:
0635 print(f"Error: unknown flag '{args.flag}'. Available flags:")
0636 for v, name in sorted(FLAG_ENUM.items()):
0637 print(f" {name}")
0638 sys.exit(1)
0639
0640 mask = pf["flag"] == flag_val
0641 indices = np.where(mask)[0]
0642 print(f"Filter: terminal flag = {args.flag.upper()} ({len(indices)} photons)\n")
0643 print_photon_detail(arrays, pf, indices)
0644 return
0645
0646
0647 print_outcome_table(pf, n_photons, n_hits, hitmask, arrays)
0648 print_flagmask_table(pf, n_photons)
0649 print_wavelength_table(pf, arrays)
0650 print_position_table(pf)
0651 print_seq_table(arrays, n_show=args.seq_top)
0652 print_record_steps_table(arrays)
0653
0654
0655 if args.detail > 0:
0656 indices = list(range(min(args.detail, n_photons)))
0657 print_photon_detail(arrays, pf, indices)
0658
0659 if args.trace:
0660 indices = [int(x.strip()) for x in args.trace.split(",")]
0661 print_photon_detail(arrays, pf, indices)
0662
0663 if args.lost:
0664 print_lost_photons(arrays, pf, hitmask)
0665
0666
0667 if __name__ == "__main__":
0668 main()