Warning, /eic-opticks/docs/inputs-outputs.md is written in an unsupported language. File is not indexed.
0001 # Simulation inputs and outputs
0002
0003 This guide collects the runtime configuration, application entry points, and
0004 GDML requirements that were previously documented in the top-level README. It
0005 also defines the persisted optical-event output protocol for Python readers.
0006 For the optical-surface physics implemented by Simphony, see
0007 [Physics](physics.md).
0008
0009 ## Geometry input
0010
0011 Simphony imports detector geometry in GDML format. Example applications accept
0012 GDML files through `-g`, for example:
0013
0014 ```bash
0015 GPUCerenkov -g tests/geom/opticks_raindrop.gdml
0016 ```
0017
0018 The GDML must define the optical properties needed by the GPU path:
0019
0020 - Efficiency, used to specify detection efficiency and assign sensitive
0021 surfaces
0022 - Material refractive index, used for optical boundary transmission and
0023 reflection
0024 - Material group velocity
0025 - Reflectivity, used for non-sensor specular or diffuse explicit surfaces
0026 - Additional Geant4 material and surface optical properties needed for
0027 CPU-side Geant4 validation, noting that not all of them are used by
0028 Simphony's standard GPU propagation
0029
0030 See [Physics](physics.md) for the current standard-GPU interpretation of GDML
0031 optical-surface properties such as `model`, `finish`, `type`, `EFFICIENCY`,
0032 and `REFLECTIVITY`.
0033
0034 ## Photon source inputs
0035
0036 Simphony examples feed the GPU simulation in three common ways:
0037
0038 - Charged-particle events in Geant4 collect Cerenkov and, where supported,
0039 scintillation gensteps. `GPUCerenkov` and `GPURaytrace` use this path.
0040 - Torch-source applications generate optical photons directly from the JSON
0041 configuration described below.
0042
0043 The [examples guide](../examples/README.md#application-capabilities) compares
0044 the executable-level capabilities, including whether Geant4 optical-photon
0045 tracking is also run for validation.
0046
0047 ## Torch configuration
0048
0049 `simg4ox` reads photon source parameters from a JSON config file, by default `config/dev.json`.
0050
0051 | Field | Description |
0052 |-------|-------------|
0053 | `type` | Source shape: `disc`, `sphere`, `point` |
0054 | `radius` | Size of the source area (mm) |
0055 | `pos` | Center position `[x, y, z]` (mm) |
0056 | `mom` | Emission direction `[x, y, z]` (normalized automatically) |
0057 | `numphoton` | Number of photons to generate |
0058 | `wavelength` | Photon wavelength (nm) |
0059
0060 ## Defining primary particles
0061
0062 For charged-particle examples, the user or developer defines the primary
0063 particle count and Geant4 threading in the macro file, and the primary
0064 particle position, momentum, and related settings in the application's
0065 `GeneratePrimaries` implementation. `src/GPUCerenkov.h` provides a compact
0066 working example.
0067
0068 ```text
0069 /run/numberOfThreads {threads}
0070 /run/verbose 1
0071 /process/optical/cerenkov/setStackPhotons {flag}
0072 /run/initialize
0073 /run/beamOn 500
0074 ```
0075
0076 `setStackPhotons` controls whether Geant4 propagates optical photons. In
0077 production-style GPU runs, Simphony handles optical photon propagation on the
0078 GPU. If more photons are simulated than can fit in GPU memory, move the GPU
0079 simulation and hit retrieval from run-end handling to per-event handling.
0080
0081 ## GDML scintillation properties
0082
0083 For scintillation to work with both Geant4 11.x and Simphony GPU simulation,
0084 the GDML must define properties using the correct syntax.
0085
0086 1. Const properties, such as yield and time constants, must use
0087 `coldim="1"` matrices:
0088
0089 ```xml
0090 <define>
0091 <matrix coldim="1" name="SCINT_YIELD" values="5000.0"/>
0092 <matrix coldim="1" name="FAST_TIME_CONST" values="21.5"/>
0093 </define>
0094 ```
0095
0096 2. Both old and new style property names are required for Simphony
0097 compatibility:
0098
0099 ```xml
0100 <material name="Crystal">
0101 <!-- New Geant4 11.x names -->
0102 <property name="SCINTILLATIONYIELD" ref="SCINT_YIELD"/>
0103 <property name="SCINTILLATIONCOMPONENT1" ref="SCINT_SPECTRUM"/>
0104 <property name="SCINTILLATIONTIMECONSTANT1" ref="FAST_TIME_CONST"/>
0105 <!-- Old-style names for Opticks U4Scint -->
0106 <property name="FASTCOMPONENT" ref="SCINT_SPECTRUM"/>
0107 <property name="SLOWCOMPONENT" ref="SCINT_SPECTRUM"/>
0108 <property name="REEMISSIONPROB" ref="REEMISSION_PROB"/>
0109 </material>
0110 ```
0111
0112 See `tests/geom/8x8SiPM_w_CSI_optial_grease.gdml` for a complete working
0113 example.
0114
0115 ## Output protocol
0116
0117 Simphony persists optical-event arrays as `.npy` files using component names
0118 from `sysrap/SComp.h`. The canonical full step-history filename is
0119 `record.npy`.
0120
0121 The schema below is source-backed by `SEvt::makePhoton`, `SEvt::makeRecord`,
0122 `SEvt::makeSeq`, `SEvt::gatherHit`, `SEvt::gatherDomain`, `sphoton`, `sseq`,
0123 and `OpticksPhoton.h`. Treat the file stems, dtypes, array ranks, `sphoton`
0124 quad layout, `q3` bit packing, and `seq.npy` nibble encoding as the reader
0125 contract. Event folder names and which files appear are runtime configuration.
0126 Auxiliary debug arrays such as `aux.npy`, `prd.npy`, `tag.npy`, `flat.npy`,
0127 and `sup.npy` are lower-level diagnostics.
0128
0129 ### Event directory
0130
0131 For applications that construct `simphony::Config`, the event output base is
0132 `Config::output_dir`, read from the JSON key `event.output_dir` and applied to
0133 the lower-level event system by `Config::Apply`. With the default event
0134 relative directory, GPU event arrays are written below:
0135
0136 ```text
0137 <Config.output_dir>/ALL0_no_opticks_event_name/A000/
0138 ```
0139
0140 The path is assembled as:
0141
0142 | Segment | Source | Notes |
0143 |---------|--------|-------|
0144 | `<Config.output_dir>` | `Config::output_dir`, JSON `event.output_dir` | The app-level base directory for event output folders. The default JSON configs currently use `./`. |
0145 | `ALL0_no_opticks_event_name` | lower-level event relative directory default | Stable enough for readers as a default, but not a data-schema contract. |
0146 | `A000` | event instance and event index | `A` is the GPU `SEvt` instance, `B` is the CPU/G4 comparison instance. The numeric part is zero-padded event index width 3. |
0147
0148 For run-level metadata, the same base and event relative directory are used
0149 without the final `A000` or `B000` event-index subdirectory. The app-level
0150 `Config` interface does not expose a separate save-mode field; prefer
0151 `Config::output_dir` for controlling where event folders are written.
0152
0153 ### File schemas
0154
0155 The table assumes the default full photon representation and unmerged hits:
0156 `Config::mode_lite` is `ModeLite::Unspecified` or `ModeLite::Standard`, and
0157 `Config::mode_merge` is `ModeMerge::Unspecified` or `ModeMerge::Separate`.
0158 Lite and merge modes replace the photon and hit component names with
0159 `photonlite`, `hitlite`, `hitmerged`, or `hitlitemerged` after
0160 `Config::Apply` synchronizes the app-level policy to `SEventConfig`.
0161
0162 | File | Shape | Dtype | Meaning | Written by |
0163 |------|-------|-------|---------|------------|
0164 | `photon.npy` | `(N, 4, 4)` | `float32` | Final state of every photon as `sphoton`. | `EventMode::HitPhoton`, `EventMode::HitPhotonSeq`, `EventMode::DebugLite`, `EventMode::DebugHeavy` |
0165 | `hit.npy` | `(H, 4, 4)` | `float32` | Subset of `photon.npy` whose `flagmask` satisfies the configured hitmask. | `EventMode::Hit`, `EventMode::HitPhoton`, `EventMode::HitPhotonSeq`, `EventMode::HitSeq`, `EventMode::DebugLite`, `EventMode::DebugHeavy` |
0166 | `inphoton.npy` | `(N, 4, 4)` | `float32` | Input photons copied or narrowed from the configured input-photon array. Present only when input photons exist. | `EventMode::DebugLite`, `EventMode::DebugHeavy` |
0167 | `record.npy` | `(N, R, 4, 4)` | `float32` | Full per-step `sphoton` records for each photon. `R` is `MaxRecord`; debug modes set it to `RecordLimit() == sseq::SLOTS == 32`. | `EventMode::DebugLite`, `EventMode::DebugHeavy` |
0168 | `seq.npy` | `(N, 2, 2)` | `uint64` | Compact chronological step history: two `seqhis` words and two `seqbnd` words per photon. | `EventMode::HitPhotonSeq`, `EventMode::HitSeq`, `EventMode::DebugLite`, `EventMode::DebugHeavy` |
0169 | `genstep.npy` | `(G, 6, 4)` | `float32` | Uploaded generation-step records. | `EventMode::Hit`, `EventMode::HitPhoton`, `EventMode::HitPhotonSeq`, `EventMode::HitSeq`, `EventMode::DebugLite`, `EventMode::DebugHeavy` |
0170 | `domain.npy` | `(2, 4, 4)` | `float32` | Domain and config quads from `sevent::get_domain` and `sevent::get_config`. Mostly relevant to compressed records. | `EventMode::DebugLite`, `EventMode::DebugHeavy` |
0171
0172 `N` is the number of photons in the event, `H` is the number of selected hits,
0173 `G` is the number of gensteps, and `R` is the configured record depth. A
0174 configured component with no backing data may be omitted.
0175
0176 ### Event modes
0177
0178 `Config::event_mode`, read from JSON `event.mode`, controls both GPU-to-CPU
0179 gathering and CPU-to-file saving after `Config::Apply` synchronizes it to the
0180 lower-level event system. The app-level default is `EventMode::Minimal`.
0181
0182 | Mode | Files saved with default lite/merge settings | Notes |
0183 |------|----------------------------------------------|-------|
0184 | `EventMode::Minimal` | none | Gathers hits into memory only. Highest-statistics mode. |
0185 | `EventMode::Hit` | `hit.npy`, `genstep.npy` | Hit and genstep output only. |
0186 | `EventMode::HitPhoton` | `hit.npy`, `photon.npy`, `genstep.npy` | Does not save `record.npy` or `seq.npy`. |
0187 | `EventMode::HitPhotonSeq` | `hit.npy`, `photon.npy`, `seq.npy`, `genstep.npy` | Sets `MaxSeq=1`; sequence output without full records. |
0188 | `EventMode::HitSeq` | `hit.npy`, `seq.npy`, `genstep.npy` | Sequence output without full photon output. |
0189 | `EventMode::DebugLite` | `domain.npy`, `genstep.npy`, `inphoton.npy` when present, `photon.npy`, `hit.npy`, `record.npy`, `seq.npy` | Enables full records and sequence history. Low-statistics/debug mode. |
0190 | `EventMode::DebugHeavy` | `DebugLite` files plus lower-level arrays such as `aux.npy`, `prd.npy`, `tag.npy`, `flat.npy`, `sup.npy` | Deep debugging only; substantially higher memory use. |
0191
0192 Use `EventMode::DebugLite` or `EventMode::DebugHeavy` when an external reader
0193 requires `record.npy`. Use `EventMode::HitPhotonSeq`, `EventMode::HitSeq`,
0194 `EventMode::DebugLite`, or `EventMode::DebugHeavy` when it requires
0195 `seq.npy`.
0196
0197 ### Photon and record layout
0198
0199 `photon.npy`, `hit.npy`, `inphoton.npy`, and each `record.npy` step store the
0200 same `sphoton` layout: a `(4, 4)` `float32` matrix. Numeric physics fields are
0201 stored as floats. Packed integer fields occupy `float32` slots and must be
0202 read through a `uint32` view.
0203
0204 | Quad | `.x` | `.y` | `.z` | `.w` |
0205 |------|------|------|------|------|
0206 | `q0` | `pos.x` in mm | `pos.y` in mm | `pos.z` in mm | `time` in ns |
0207 | `q1` | `mom.x` | `mom.y` | `mom.z` | `hitcount_iindex` as `uint32` |
0208 | `q2` | `pol.x` | `pol.y` | `pol.z` | `wavelength` in nm |
0209 | `q3` | `orient_boundary_flag` as `uint32` | `identity` plus index extension as `uint32` | `index` low 32 bits as `uint32` | `flagmask` as `uint32` |
0210
0211 `mom` and `pol` are direction/polarization vectors. `record.npy[i, s]` is the
0212 same layout for photon `i` at step slot `s`. Unused record slots are zero-filled;
0213 used slots are expected to be contiguous from slot 0 until the first all-zero
0214 step or until the configured record depth is reached.
0215
0216 ### Packed fields
0217
0218 Interpret the packed fields with `arr.view(np.uint32)`.
0219
0220 `q1.w` packs hit count and intersected-geometry instance index:
0221
0222 ```text
0223 bits 31-16: hitcount
0224 bits 15-0 : iindex
0225 ```
0226
0227 `q3.x` packs the terminal flag, boundary, and orientation:
0228
0229 ```text
0230 bit 31 : orient sign, where 1 means orient = -1 and 0 means orient = +1
0231 bits 30-16: boundary index, 15 bits
0232 bits 15-0 : terminal flag from OpticksPhoton.h
0233 ```
0234
0235 Use these exact masks and shifts:
0236
0237 ```python
0238 obf = q3[..., 0]
0239 flag = obf & 0x0000FFFF
0240 boundary = (obf & 0x7FFF0000) >> 16
0241 orient = np.where((obf & 0x80000000) != 0, -1.0, 1.0)
0242 ```
0243
0244 `q3.y` stores a 24-bit identity in its low bits and extends the photon index in
0245 its high 8 bits. `q3.z` stores the low 32 bits of the photon index:
0246
0247 ```python
0248 identity = q3[..., 1] & 0x00FFFFFF
0249 index = ((q3[..., 1].astype(np.uint64) >> 24) << 32) | q3[..., 2].astype(np.uint64)
0250 flagmask = q3[..., 3]
0251 ```
0252
0253 `identity` is currently `sensor_identifier + 1`; zero means the photon did not
0254 end on a sensor. `flagmask` is the cumulative OR of every flag set during the
0255 photon lifetime. Each `sphoton::set_flag(f)` updates both the terminal `flag`
0256 and `flagmask |= f`.
0257
0258 ### Flag values
0259
0260 The low 16 `OpticksPhoton.h` flags are the values stored by the terminal
0261 `flag` field and cumulative `flagmask`.
0262
0263 | Flag | Hex | Abbrev | Meaning |
0264 |------|-----|--------|---------|
0265 | `CERENKOV` | `0x0001` | `CK` | Cerenkov generation |
0266 | `SCINTILLATION` | `0x0002` | `SI` | Scintillation generation |
0267 | `TORCH` | `0x0004` | `TO` | Torch source |
0268 | `BULK_ABSORB` | `0x0008` | `AB` | Absorbed in bulk material |
0269 | `BULK_REEMIT` | `0x0010` | `RE` | Re-emitted, including WLS |
0270 | `BULK_SCATTER` | `0x0020` | `SC` | Rayleigh scattered |
0271 | `SURFACE_DETECT` | `0x0040` | `SD` | Detected at a surface |
0272 | `SURFACE_ABSORB` | `0x0080` | `SA` | Absorbed at a surface |
0273 | `SURFACE_DREFLECT` | `0x0100` | `DR` | Diffuse reflection at a surface |
0274 | `SURFACE_SREFLECT` | `0x0200` | `SR` | Specular reflection at a surface |
0275 | `BOUNDARY_REFLECT` | `0x0400` | `BR` | Fresnel reflection at a boundary |
0276 | `BOUNDARY_TRANSMIT` | `0x0800` | `BT` | Transmitted through a boundary |
0277 | `NAN_ABORT` | `0x1000` | `NA` | Aborted due to NaN, usually geometry-related |
0278 | `EFFICIENCY_COLLECT` | `0x2000` | `EC` | Collected by PMT efficiency handling |
0279 | `EFFICIENCY_CULL` | `0x4000` | `EL` | Culled by PMT efficiency handling |
0280 | `MISS` | `0x8000` | `MI` | Missed all geometry |
0281
0282 ### Sequence layout
0283
0284 `seq.npy` stores `sseq` as `(N, 2, 2)` `uint64`:
0285
0286 ```python
0287 seqhis = seq[:, 0, :] # two uint64 words per photon
0288 seqbnd = seq[:, 1, :] # two uint64 words per photon
0289 ```
0290
0291 `sseq::NSEQ == 2`, `sseq::BITS == 4`, `sseq::SLOTMAX == 16`, and
0292 `sseq::SLOTS == 32`. Each photon therefore has 32 chronological history slots.
0293 Slot `0..15` is stored in word 0; slot `16..31` is stored in word 1. Within a
0294 word, each slot is one 4-bit nibble:
0295
0296 ```python
0297 iseq = slot // 16
0298 shift = 4 * (slot - iseq * 16)
0299 nibble = (seqhis[i, iseq] >> shift) & 0xF
0300 flag = 0 if nibble == 0 else 1 << (nibble - 1)
0301 ```
0302
0303 The stored nibble is `FFS(flag) & 0xF`, where `FFS` is find-first-set using
0304 one-based bit positions. For example, `TORCH` (`0x0004`) stores nibble `3`,
0305 and `SURFACE_DETECT` (`0x0040`) stores nibble `7`. A zero nibble marks no
0306 recorded flag and is commonly used as the end of the sequence. Because the
0307 nibble has only four bits and zero is reserved, `MISS` (`0x8000`, FFS 16) is
0308 not representable in `seq.npy` history slots.
0309
0310 `seqbnd` stores `boundary & 0xF` for the same slots. Use `record.npy` or
0311 `photon.npy` when the full 15-bit boundary index is required.
0312
0313 ### Hit semantics
0314
0315 Hits are selected from the final photon array by the configured hitmask:
0316
0317 ```text
0318 (flagmask & hitmask) == hitmask
0319 ```
0320
0321 The current default hitmask is `SD`, meaning `SURFACE_DETECT` (`0x0040`).
0322 Some PMT efficiency workflows use `EC`, meaning `EFFICIENCY_COLLECT`
0323 (`0x2000`). The app-level `Config` interface does not currently expose the
0324 hitmask; the event metadata records the numeric `hitmask`, and readers should
0325 prefer that metadata over hard-coded assumptions.
0326
0327 `hit.npy` is a copy of the selected rows from `photon.npy`, not a different
0328 layout. The original photon index remains available through the packed `q3.y`
0329 and `q3.z` index fields.
0330
0331 Photons that exhaust the bounce limit are not given a distinct terminal flag
0332 by the output format. With default `MaxBounce=31`, debug records have up to 32
0333 slots. A reader can identify likely bounce-limit truncation by checking photons
0334 whose non-zero record-step count equals `record.shape[1]`.
0335
0336 ### Python reader pattern
0337
0338 Use shape and dtype checks before decoding packed fields:
0339
0340 ```python
0341 from pathlib import Path
0342 import numpy as np
0343
0344 event_dir = Path("path/from/Config.output_dir/ALL0_no_opticks_event_name/A000")
0345 record = np.load(event_dir / "record.npy")
0346
0347 assert record.dtype == np.float32
0348 assert record.ndim == 4 and record.shape[2:] == (4, 4)
0349
0350 u32 = record.view(np.uint32)
0351 q3 = u32[..., 3, :]
0352
0353 terminal_flag = q3[..., 0] & 0x0000FFFF
0354 boundary = (q3[..., 0] & 0x7FFF0000) >> 16
0355 orient = np.where((q3[..., 0] & 0x80000000) != 0, -1.0, 1.0)
0356 identity = q3[..., 1] & 0x00FFFFFF
0357 photon_index = ((q3[..., 1].astype(np.uint64) >> 24) << 32) | q3[..., 2].astype(np.uint64)
0358 flagmask = q3[..., 3]
0359
0360 step_used = np.any(record.reshape(record.shape[0], record.shape[1], 16) != 0, axis=2)
0361 step_count = step_used.sum(axis=1)
0362 ```
0363
0364 For `photon.npy`, `hit.npy`, and `inphoton.npy`, use the same decoding with
0365 shape `(N, 4, 4)` and `q3 = arr.view(np.uint32)[:, 3, :]`.