Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
bd4e2f5
First attempt at adding optical photon tracking to ddceler plugin
Feb 16, 2026
cbcddb9
Enable optical photons inDD4hep steering file
Feb 16, 2026
37e320e
Add modified definition of Polystyrene with placeholder optical prope…
Feb 19, 2026
7bb0f45
Add value for Optical Track parameters in steering file
Feb 19, 2026
ff9927a
Revert optical property changes from Preshower example
Feb 19, 2026
8bf1df7
Add simple water scintillator box example to test optical photon offload
Feb 19, 2026
b31e9ba
Merge branch 'celeritas-project:develop' into feature-add-optical-to-…
rahmans1 Feb 25, 2026
81b617b
Merge branch 'celeritas-project:develop' into feature-add-optical-to-…
rahmans1 Mar 2, 2026
cab0240
Remove redundant ddceler classes for optical offload
Mar 2, 2026
dc4c161
Merge branch 'celeritas-project:develop' into feature-add-optical-to-…
rahmans1 Mar 12, 2026
36419af
SurfaceSteppingAction (runs at post) kills photons before the old pos…
Mar 12, 2026
4744661
remove PrimaryId primary from the struct, since the executor never pa…
Mar 12, 2026
1a95646
Extend DetectorHit output to include volume hierarchy
Mar 12, 2026
e0812bd
Propagate volume-hierarchy depth into the optical track state so the …
Mar 12, 2026
1ce58d2
New classes that translate DetectorHitsOutput → G4 touchable → G4VSen…
Mar 12, 2026
ea1339e
Connect the new infrastructure to the existing setup/problem construc…
Mar 12, 2026
c415f83
Construct and destroy the thread-local processor in the two transpor…
Mar 12, 2026
50611cb
User-facing enablement (geant_sd = true default in ddceler)
Mar 12, 2026
f960814
readout changed to CartesianGridXYZ with 5 mm segmentation and partic…
Mar 12, 2026
dbc08b5
Merge branch 'develop' into feature-add-optical-to-ddceler
rahmans1 Mar 20, 2026
bfe4ebd
Remove redundant code to fill detector hit information that's handled…
Mar 20, 2026
e38f969
Guard against null top volume in LevelTouchableUpdater
Mar 20, 2026
b812589
Populate pre-step point in OpticalHitProcessor for DD4hep compatibility
Mar 20, 2026
81d39a4
Fire user tracking actions in TrackingManager for DD4hep compatibility
Mar 20, 2026
2217269
Add direct optical generator mode to ddceler plugin
Mar 21, 2026
97184d5
Fix ScintWater geometry for optical boundary and DD4hep ParticleHandler
Mar 21, 2026
4672791
Add optgun validation steering and revert ScintWater to default EM mode
Mar 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 152 additions & 0 deletions example/ddceler/optical/input/ScintWater.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Copyright Celeritas contributors: see top-level COPYRIGHT file for details -->
<!-- SPDX-License-Identifier: (Apache-2.0 OR MIT) -->
<!--
Minimal optical photon demonstration geometry.

A single 30 cm scintillating water cube. A 1 GeV e- initiates an
EM shower, generating Cherenkov photons from all shower secondaries
and scintillation photons proportional to local energy deposit.
Photons below the critical angle (48.8 deg at the water/air boundary)
escape; the rest undergo total internal reflection and rattle until
absorbed. The late-time tail in the hit contributions is the TIR
signature.
-->
<lccdd>

<info name="ScintWater"
title="Single scintillating water box - optical photon demo"
author="Celeritas example"
status="development"
version="1.0">
<comment>Minimal geometry for optical photon generation and propagation</comment>
</info>

<includes>
<gdmlFile ref="${DD4hepINSTALL}/DDDetectors/compact/elements.xml"/>
<gdmlFile ref="${DD4hepINSTALL}/DDDetectors/compact/materials.xml"/>
</includes>

<!-- ── optical property tables ──────────────────────────────────────── -->
<properties>

<!-- Water refractive index vs photon energy -->
<matrix name="RINDEX_Water" coldim="2" values="
1.77*eV 1.3280
2.34*eV 1.3340
2.76*eV 1.3380
3.10*eV 1.3440
3.54*eV 1.3530
4.14*eV 1.3720
"/>

<!-- Water bulk absorption length (transparent in visible, absorbing in UV) -->
<matrix name="ABSLENGTH_Water" coldim="2" values="
1.77*eV 10.0*m
2.34*eV 10.0*m
2.76*eV 8.0*m
3.10*eV 5.0*m
3.54*eV 1.0*m
4.14*eV 0.1*m
"/>

<!-- Scintillation emission spectrum (peaked at 400 nm / 3.1 eV) -->
<matrix name="SCINT_Water" coldim="2" values="
2.76*eV 0.00
2.92*eV 0.20
3.02*eV 0.60
3.10*eV 1.00
3.18*eV 0.60
3.28*eV 0.20
3.54*eV 0.00
"/>

<!-- Air refractive index: required by G4OpBoundaryProcess so that photons
reaching the water/air interface are transmitted rather than totally
internally reflected due to missing RINDEX in the surrounding medium. -->
<matrix name="RINDEX_Air" coldim="2" values="
1.77*eV 1.0
4.14*eV 1.0
"/>

</properties>

<!-- ── materials ────────────────────────────────────────────────────── -->
<materials>
<material name="Air">
<property name="RINDEX" ref="RINDEX_Air"/>
</material>
<material name="ScintWater">
<D type="density" unit="g/cm3" value="1.0"/>
<composite n="2" ref="H"/>
<composite n="1" ref="O"/>
<property name="RINDEX" ref="RINDEX_Water"/>
<property name="ABSLENGTH" ref="ABSLENGTH_Water"/>
<property name="SCINTILLATIONCOMPONENT1" ref="SCINT_Water"/>
<!-- Low yield (100/MeV) keeps event size manageable -->
<constant name="SCINTILLATIONYIELD" value="100.0/MeV"/>
<constant name="RESOLUTIONSCALE" value="1.0"/>
<constant name="SCINTILLATIONTIMECONSTANT1" value="2.5*ns"/>
<constant name="SCINTILLATIONYIELD1" value="1.0"/>
</material>
</materials>

<!-- ── world ────────────────────────────────────────────────────────── -->
<define>
<constant name="world_size" value="5*m"/>
<constant name="world_x" value="world_size"/>
<constant name="world_y" value="world_size"/>
<constant name="world_z" value="world_size"/>
<!-- Required by DD4hep ParticleHandler for MC-truth link initialisation.
This geometry has no tracker volume so these are set to zero; the
tracker-region cuts will never actually fire. -->
<constant name="tracker_region_rmax" value="0"/>
<constant name="tracker_region_zmax" value="0"/>
</define>

<!-- ── step limits ──────────────────────────────────────────────────── -->
<limits>
<limitset name="WaterLimits">
<limit name="step_length_max" particles="*" value="5.0" unit="mm"/>
</limitset>
</limits>

<regions>
<region name="WaterRegion" eunit="MeV" lunit="mm" cut="0.001" threshold="0.001">
<limitsetref name="WaterLimits"/>
</region>
</regions>

<!-- ── detector ─────────────────────────────────────────────────────── -->
<detectors>
<detector id="1" name="WaterBox"
type="DD4hep_BoxSegment"
readout="WaterHits"
sensitive="true"
region="WaterRegion"
limits="WaterLimits">
<material name="ScintWater"/>
<sensitive type="calorimeter"/>
<box x="30*cm" y="30*cm" z="30*cm"/>
<position x="0" y="0" z="0"/>
<rotation x="0" y="0" z="0"/>
</detector>
</detectors>

<!-- ── readout ──────────────────────────────────────────────────────── -->
<readouts>
<readout name="WaterHits">
<segmentation type="CartesianGridXYZ" grid_size_x="5*mm" grid_size_y="5*mm" grid_size_z="5*mm"/>
<id>system:8,x:8:-8,y:-8,z:-8</id>
</readout>
</readouts>

<!-- ── field ────────────────────────────────────────────────────────── -->
<!-- Negligible field required by the current CelerPhysics implementation -->
<fields>
<field name="TinyBz" type="ConstantField" field="magnetic">
<strength x="0" y="0" z="1e-4*tesla"/>
</field>
</fields>

</lccdd>
66 changes: 66 additions & 0 deletions example/ddceler/optical/input/steeringFile-optgun.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Copyright Celeritas contributors: see top-level COPYRIGHT file for details
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
"""DDG4 steering file for optical photon propagation validation.

Fires monochromatic optical photons directly into the scintillating water
box so that Celeritas (direct-offload mode) and pure Geant4 can be compared
on identical input — isolating propagation from generation differences.
"""

from DDSim.DD4hepSimulation import DD4hepSimulation

runner = DD4hepSimulation()

runner.action.run = "CelerRun"
runner.action.tracker = "Geant4TrackerAction"
runner.action.trackerSDTypes = ["tracker"]
runner.action.calo = "Geant4CalorimeterAction"
runner.action.calorimeterSDTypes = ["calorimeter"]

runner.outputConfig.forceDD4HEP = True
runner.numberOfEvents = 100

# Field tracking configuration
runner.field.delta_chord = 0.025 # mm
runner.field.delta_intersection = 1e-2 # mm
runner.field.delta_one_step = 0.001 # mm
runner.field.eps_min = 5e-5 # mm
runner.field.eps_max = 0.001 # mm
runner.field.min_chord_step = 1e-6 # mm

# 3.1 eV (400 nm) optical photon gun, 1 mm inside the water box front face
runner.enableGun = True
runner.gun.particle = "opticalphoton"
runner.gun.energy = 3.1e-3 # MeV (= 3.1 eV)
runner.gun.direction = (0, 0, 1)
runner.gun.position = (0, 0, -149) # mm
runner.gun.multiplicity = 1000


def setup_physics(kernel):
"""Configure optical physics list with Celeritas integration."""
from DDG4 import Geant4, PhysicsList

phys = Geant4(kernel).setupPhysics("QGSP_BERT")

# Optical photon processes: boundary, absorption, Rayleigh
ph = PhysicsList(kernel, "Geant4OpticalPhotonPhysics/OpticalGammaPhys")
ph.VerboseLevel = 1
ph.addParticleConstructor("G4OpticalPhoton")
ph.enableUI()
phys.adopt(ph)

# No Cherenkov or scintillation — photons are injected directly

# Celeritas offload with optical tracking (direct mode)
celer_phys = PhysicsList(kernel, str("CelerPhysics"))
celer_phys.MaxNumTracks = 2048
celer_phys.InitCapacity = 245760
celer_phys.OpticalTracks = 2048
celer_phys.OpticalGenerator = "direct"
phys.adopt(celer_phys)
phys.dump()
return None


runner.physics.setupUserPhysics(setup_physics)
89 changes: 89 additions & 0 deletions example/ddceler/optical/input/steeringFile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Copyright Celeritas contributors: see top-level COPYRIGHT file for details
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
"""DDG4 steering file for the optical photon demonstration.

A 1 GeV e- enters a 30 cm scintillating water cube, initiating an EM
shower (X0 ~ 36 cm in water). All shower secondaries are above the
Cherenkov threshold (~0.26 MeV kinetic energy in water), producing a
dense cone of optical photons. Scintillation photons are generated
along the shower axis proportional to the local energy deposit.
All optical photons are offloaded to Celeritas for GPU tracking.
"""

from DDSim.DD4hepSimulation import DD4hepSimulation

runner = DD4hepSimulation()

runner.action.run = "CelerRun"
runner.action.tracker = "Geant4TrackerAction"
runner.action.trackerSDTypes = ["tracker"]
runner.action.calo = "Geant4CalorimeterAction"
runner.action.calorimeterSDTypes = ["calorimeter"]

runner.outputConfig.forceDD4HEP = True
runner.numberOfEvents = 100

# Field tracking configuration
runner.field.delta_chord = 0.025 # mm
runner.field.delta_intersection = 1e-2 # mm
runner.field.delta_one_step = 0.001 # mm
runner.field.eps_min = 5e-5 # mm
runner.field.eps_max = 0.001 # mm
runner.field.min_chord_step = 1e-6 # mm

# 1 GeV e-: EM shower mostly contained within the 30 cm water box
runner.enableGun = True
runner.gun.particle = "e-"
runner.gun.energy = "1*GeV"
runner.gun.distribution = "uniform"
runner.gun.etaMin = 5.0
runner.gun.etaMax = 5.0
runner.gun.position = "0 0 -155*mm"


def setup_physics(kernel):
"""Configure optical physics list with Celeritas integration."""
from DDG4 import Geant4, PhysicsList

phys = Geant4(kernel).setupPhysics("QGSP_BERT")

# Optical photon processes: boundary, absorption, Rayleigh
ph = PhysicsList(kernel, "Geant4OpticalPhotonPhysics/OpticalGammaPhys")
ph.VerboseLevel = 1
ph.addParticleConstructor("G4OpticalPhoton")
ph.enableUI()
phys.adopt(ph)

# Cherenkov radiation from shower tracks.
# MaxNumPhotonsPerStep is kept low to avoid exhausting the optical
# generator buffer given the high shower track multiplicity.
ph = PhysicsList(kernel, "Geant4CerenkovPhysics/CerenkovPhys")
ph.MaxNumPhotonsPerStep = 50
ph.MaxBetaChangePerStep = 10.0
ph.TrackSecondariesFirst = False
ph.VerboseLevel = 1
ph.enableUI()
phys.adopt(ph)

# Scintillation along the shower axis (yield = 100/MeV in geometry XML)
ph = PhysicsList(kernel, "Geant4ScintillationPhysics/ScintillatorPhys")
ph.ScintillationYieldFactor = 1.0
ph.ScintillationExcitationRatio = 1.0
ph.TrackSecondariesFirst = False
ph.VerboseLevel = 1
ph.enableUI()
phys.adopt(ph)

# Celeritas offload with optical tracking
celer_phys = PhysicsList(kernel, str("CelerPhysics"))
celer_phys.MaxNumTracks = 2048
celer_phys.InitCapacity = 245760
celer_phys.IgnoreProcesses = ["CoulombScat"]
# OpticalGenerators defaults to OpticalTracks * 8 = 16384
celer_phys.OpticalTracks = 2048
phys.adopt(celer_phys)
phys.dump()
return None


runner.physics.setupUserPhysics(setup_physics)
70 changes: 70 additions & 0 deletions example/ddceler/optical/run-optgun.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#!/bin/sh -e
#-------------------------------- -*- sh -*- ---------------------------------#
# Copyright Celeritas contributors: see top-level COPYRIGHT file for details
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
#-----------------------------------------------------------------------------#
# Run the optical photon gun validation study.
#
# Usage:
# ./run-optgun.sh [celeritas|geant4] [additional ddsim options]
#
# Examples:
# ./run-optgun.sh celeritas
# ./run-optgun.sh geant4
#-----------------------------------------------------------------------------#

log() { printf "%s\n" "$1" >&2; }

resolve_symlinks() {
_path="$1"
while [ -L "$_path" ]; do
_path=$(readlink -f "$_path")
done
printf "%s\n" "$_path"
}

MODE=$1
if [ "$MODE" != "celeritas" ] && [ "$MODE" != "geant4" ]; then
log "Usage: $0 [celeritas|geant4] [additional ddsim options]"
exit 1
fi
shift

[ "$MODE" = "geant4" ] && export CELER_DISABLE=1

EXAMPLE_DIR=$(cd "$(dirname $0)" && pwd)

if [ -z "${Celeritas_ROOT}" ]; then
Celeritas_ROOT=$(cd "$EXAMPLE_DIR"/../../.. && pwd)/install
log "warning: Celeritas_ROOT is undefined: using ${Celeritas_ROOT}"
fi

DDSIM=$(command -v ddsim 2>/dev/null || printf "")
[ -z "$DDSIM" ] && { log "error: ddsim not found"; exit 1; }
DDSIM=$(resolve_symlinks "$DDSIM")

[ -z "$DD4hepINSTALL" ] && { log "error: DD4hepINSTALL not set"; exit 1; }

CELER_LIB_DIR=$(ls -1 -d "$Celeritas_ROOT"/lib 2>/dev/null | head -1)
[ -z "$CELER_LIB_DIR" ] && { log "error: celeritas not found in $Celeritas_ROOT"; exit 1; }

if [ "$(uname -s)" = "Darwin" ]; then
export DYLD_LIBRARY_PATH=${CELER_LIB_DIR}:${DYLD_LIBRARY_PATH}
else
export LD_LIBRARY_PATH=${CELER_LIB_DIR}:${LD_LIBRARY_PATH}
fi

PYTHON=$(command -v python3 2>/dev/null || command -v python 2>/dev/null || printf "")
[ -z "$PYTHON" ] && { log "error: python not found"; exit 1; }
PYTHON=$(resolve_symlinks "$PYTHON")

log "Running optical gun validation with ${MODE} physics"
mkdir -p "${EXAMPLE_DIR}/output/${MODE}-optgun"
cd "${EXAMPLE_DIR}/output/${MODE}-optgun"

set -x
exec "$PYTHON" "$DDSIM" \
--compactFile="${EXAMPLE_DIR}/input/ScintWater.xml" \
--steering="${EXAMPLE_DIR}/input/steeringFile-optgun.py" \
--outputFile="optgun_demo.root" \
"$@"
Loading
Loading