Skip to content

Commit 13ded29

Browse files
plexoosCopilot
andauthored
fix: align post-boundary optical timing with Geant4 11.4+ (#260)
Geant4 11.4 substantially refactored G4OpBoundaryProcess, including the dielectric-dielectric boundary handling path. In our simg4ox and GPURaytrace workflows, Geant4 11.4+ is the first release family where timing-sensitive optical comparisons diverge from the Geant4 11.3 baselines. That makes post-boundary timing correctness explicit in a way our previous implementation did not handle. This branch fixes two gaps in our handling of post-boundary optical timing: 1. The Opticks GPU path advanced the next segment using the pre-boundary medium group velocity even after transmission into material2. 2. The lightweight Geant4-side tracking/recording path used by simg4ox did not explicitly enable UseGivenVelocity(true) for optical photons, so the track was not forced to follow the boundary-selected velocity during post-boundary recording. Fix the GPU side by carrying the active-medium timing state through qsim: - store both material1 and material2 group velocities in sstate - track current_material_index/current_group_velocity in sctx - use current_group_velocity in propagate_to_boundary - switch the carried material/velocity in propagate_at_boundary after reflection/transmission - refresh the carried velocity after each boundary lookup when the boundary orientation still matches the active medium - initialize the new fields in the QSim test/debug kernels Fix the Geant4-side CPU recording path by enabling UseGivenVelocity(true) for optical photons in g4app tracking action, so the recorded post-boundary timing follows the velocity selected by Geant4. Because hit merging in this codebase is time-bucket based, correcting post-boundary timing changes timing-sensitive observables and can change merged hit counts. The follow-up test updates on this branch therefore make expected residual diffs and hit-count baselines depend on the Geant4 release family instead of treating the new values as regressions. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 3ac12ac commit 13ded29

9 files changed

Lines changed: 124 additions & 30 deletions

File tree

qudarap/QSim.cu

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,8 @@ __global__ void _QSim_propagate_to_boundary( qsim* sim, sphoton* photon, unsigne
324324
ctx.prd = &dbg->prd ; // no need for local copy when readonly
325325
ctx.s = dbg->s ;
326326
ctx.p = dbg->p ; // need local copy of photon otherwise will have write interference between threads
327+
ctx.current_group_velocity = ctx.s.material1_group_velocity();
328+
ctx.current_material_index = ctx.s.index.x;
327329

328330
unsigned flag = 0u ;
329331
//sim->propagate_to_boundary( flag, p, prd, s, rng, idx, tagr );
@@ -357,6 +359,8 @@ __global__ void _QSim_propagate_at_boundary_generate( qsim* sim, sphoton* photon
357359
ctx.prd = &dbg->prd ; // no need for local copy when readonly
358360
ctx.s = dbg->s ;
359361
ctx.p = dbg->p ; // need local copy of photon otherwise will have write interference between threads
362+
ctx.current_group_velocity = ctx.s.material1_group_velocity();
363+
ctx.current_material_index = ctx.s.index.x;
360364

361365
quad4& q = (quad4&)ctx.p ;
362366
q.q0.f = q.q1.f ; // non-standard record initial mom and pol into q0, q3
@@ -402,6 +406,8 @@ __global__ void _QSim_propagate_at_boundary_mutate( qsim* sim, sphoton* photon,
402406
ctx.p = photon[idx] ;
403407
ctx.s = dbg->s ;
404408
ctx.prd = &dbg->prd ;
409+
ctx.current_group_velocity = ctx.s.material1_group_velocity();
410+
ctx.current_material_index = ctx.s.index.x;
405411

406412
quad4& q = (quad4&)ctx.p ;
407413
q.q0.f = q.q1.f ; // non-standard record initial mom and pol into q0, q3
@@ -441,6 +447,8 @@ __global__ void _QSim_propagate_at_multifilm_mutate( qsim* sim, sphoton* photon,
441447
ctx.p = photon[idx] ;
442448
ctx.s = dbg->s ;
443449
ctx.prd = &dbg->prd ;
450+
ctx.current_group_velocity = ctx.s.material1_group_velocity();
451+
ctx.current_material_index = ctx.s.index.x;
444452

445453
quad4& q = (quad4&)ctx.p ;
446454

@@ -820,9 +828,3 @@ extern void QSim_multifilm_lookup_all(dim3 numBlocks, dim3 threadsPerBlock, qsim
820828
printf("//QSim_multifilm_lookup width %d height %d \n", width, height );
821829
_QSim_multifilm_lookup_all<<<numBlocks,threadsPerBlock>>>( sim, sample,result , width, height );
822830
}
823-
824-
825-
826-
827-
828-

qudarap/QState.hh

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ inline sstate QState::Make()
5151
float m1_scattering_length = 1000.f ;
5252
float m1_reemission_prob = 0.f ;
5353
float m1_group_velocity = 300.f ;
54+
float m2_group_velocity = 300.f;
5455

5556
float m2_refractive_index = qenvfloat("M2_REFRACTIVE_INDEX", "1.5" ) ;
5657
float m2_absorption_length = 1000.f ;
@@ -65,7 +66,7 @@ inline sstate QState::Make()
6566
sstate s ;
6667
s.material1 = make_float4( m1_refractive_index, m1_absorption_length, m1_scattering_length, m1_reemission_prob );
6768
s.material2 = make_float4( m2_refractive_index, m2_absorption_length, m2_scattering_length, m2_reemission_prob );
68-
s.m1group2 = make_float4( m1_group_velocity, 0.f, 0.f, 0.f );
69+
s.m1group2 = make_float4(m1_group_velocity, m2_group_velocity, 0.f, 0.f);
6970
s.surface = make_float4( su_detect, su_absorb, su_reflect_specular, su_reflect_diffuse );
7071
s.optical = make_uint4( 0u, 0u, 0u, 0u ); // x/y/z/w index/type/finish/value
7172
s.index = make_uint4( 0u, 0u, 0u, 0u ); // indices of m1/m2/surf/sensor
@@ -144,5 +145,3 @@ inline std::string QState::Desc( const sstate& s )
144145
std::string repr = ss.str();
145146
return repr ;
146147
}
147-
148-

qudarap/qbnd.h

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,9 @@ Notice that s.optical.x and s.index.z are the same thing.
166166
So half of s.index is extraneous and the m1 index and m2 index
167167
is not much used.
168168
169-
Also only one elemnt of m1group2 is actually used
169+
Also only one element of m1group2 was formerly used. The y slot now carries
170+
material2 group velocity so transmitted segments can carry the correct active
171+
medium timing across boundary crossings.
170172
171173
172174
s.optical.x
@@ -191,9 +193,10 @@ inline QBND_METHOD void qbnd::fill_state(sstate& s, unsigned boundary, float wav
191193

192194

193195
s.material1 = boundary_lookup( wavelength, m1_line, 0); // refractive_index, absorption_length, scattering_length, reemission_prob
194-
s.m1group2 = boundary_lookup( wavelength, m1_line, 1); // group_velocity , (unused , unused , unused)
196+
s.m1group2 = boundary_lookup( wavelength, m1_line, 1); // x: material1 group_velocity, y: material2 group_velocity, z/w unused
195197
s.material2 = boundary_lookup( wavelength, m2_line, 0); // refractive_index, (absorption_length, scattering_length, reemission_prob) only m2:refractive index actually used
196198
s.surface = boundary_lookup( wavelength, su_line, 0); // detect, , absorb , (reflect_specular), reflect_diffuse [they add to 1. so one not used]
199+
s.set_material2_group_velocity(boundary_lookup(wavelength, m2_line, 1).x);
197200

198201
s.optical = optical[su_line].u ; // 1-based-surface-index-0-meaning-boundary/type/finish/value (type,finish,value not used currently)
199202

qudarap/qsim.h

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -723,7 +723,7 @@ inline QSIM_METHOD int qsim::propagate_to_boundary(unsigned& flag, RNG& rng, sct
723723
const float& absorption_length = s.material1.y ;
724724
const float& scattering_length = s.material1.z ;
725725
const float& reemission_prob = s.material1.w ;
726-
const float& group_velocity = s.m1group2.x ;
726+
const float& group_velocity = ctx.current_group_velocity;
727727
const float& distance_to_boundary = ctx.prd->q0.f.w ;
728728

729729

@@ -1184,6 +1184,8 @@ inline QSIM_METHOD int qsim::propagate_at_boundary(unsigned& flag, RNG& rng, sct
11841184

11851185
flag = reflect ? BOUNDARY_REFLECT : BOUNDARY_TRANSMIT ;
11861186

1187+
ctx.current_material_index = reflect ? s.index.x : s.index.y;
1188+
ctx.current_group_velocity = reflect ? s.material1_group_velocity() : s.material2_group_velocity();
11871189

11881190
#if !defined(PRODUCTION) && defined(DEBUG_TAG)
11891191
if( flag == BOUNDARY_REFLECT )
@@ -2249,6 +2251,18 @@ inline QSIM_METHOD int qsim::propagate(const int bounce, RNG& rng, sctx& ctx )
22492251

22502252
bnd->fill_state(ctx.s, boundary, ctx.p.wavelength, cosTheta, ctx.pidx, base->pidx );
22512253

2254+
if (ctx.current_material_index == 0u)
2255+
{
2256+
ctx.current_material_index = ctx.s.index.x;
2257+
ctx.current_group_velocity = ctx.s.material1_group_velocity();
2258+
}
2259+
else if (ctx.s.index.x == ctx.current_material_index)
2260+
{
2261+
// When the boundary orientation agrees with the carried medium, refresh
2262+
// the active velocity from the wavelength-dependent material lookup.
2263+
ctx.current_group_velocity = ctx.s.material1_group_velocity();
2264+
}
2265+
22522266
unsigned flag = 0 ;
22532267

22542268
int command = propagate_to_boundary( flag, rng, ctx );
@@ -2540,4 +2554,3 @@ inline QSIM_METHOD void qsim::generate_photon(sphoton& p, RNG& rng, const quad6&
25402554
p.set_index(photon_id) ;
25412555
}
25422556
#endif
2543-

src/g4app.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,14 @@ struct TrackingAction : G4UserTrackingAction
376376

377377
void PreUserTrackingAction(const G4Track *track) override
378378
{
379+
if (track->GetDefinition() == G4OpticalPhoton::OpticalPhotonDefinition())
380+
{
381+
// Geant4 boundary updates optical velocity via ProposeVelocity, but the
382+
// track must honor the given velocity for post-boundary timing to match.
383+
G4Track *mutable_track = const_cast<G4Track *>(track);
384+
mutable_track->UseGivenVelocity(true);
385+
}
386+
379387
spho *label = STrackInfo::GetRef(track);
380388

381389
if (label == nullptr)

sysrap/sctx.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ struct sctx
7777

7878
sphoton p ;
7979
sstate s ;
80+
float current_group_velocity = 0.f;
81+
unsigned current_material_index = 0u;
8082

8183
#ifndef PRODUCTION
8284
srec rec ;
@@ -184,4 +186,3 @@ SCTX_METHOD void sctx::end()
184186
}
185187

186188
#endif
187-

sysrap/sstate.h

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ BUT seems no point doing that, can just directly use them from PRD.
2525
struct sstate
2626
{
2727
float4 material1 ; // refractive_index/absorption_length/scattering_length/reemission_prob
28-
float4 m1group2 ; // group_velocity/spare1/spare2/spare3
28+
float4 m1group2 ; // material1_group_velocity/material2_group_velocity/spare2/spare3
2929
float4 material2 ;
3030
float4 surface ; // detect/absorb/reflect_specular/reflect_diffuse
3131

@@ -42,6 +42,9 @@ struct sstate
4242
void save(const char* dir) const ;
4343
#endif
4444

45+
SSTATE_METHOD float material1_group_velocity() const { return m1group2.x; }
46+
SSTATE_METHOD float material2_group_velocity() const { return m1group2.y; }
47+
SSTATE_METHOD void set_material2_group_velocity(float gv) { m1group2.y = gv; }
4548

4649
};
4750

@@ -71,7 +74,7 @@ inline std::ostream& operator<<(std::ostream& os, const sstate& s )
7174
<< " (refractive_index/absorption_length/scattering_length/reemission_prob) "
7275
<< std::endl
7376
<< " m1group2 " << s.m1group2
74-
<< " (group_velocity/spare1/spare2/spare3) "
77+
<< " (material1_group_velocity/material2_group_velocity/spare2/spare3) "
7578
<< std::endl
7679
<< " material2 " << s.material2
7780
<< " (refractive_index/absorption_length/scattering_length/reemission_prob) "
@@ -134,6 +137,3 @@ But if decide to pursure streamlined qstate with mixed up fields
134137
the named field access would be helpful to isolate user code from changes to the struct.
135138
136139
**/
137-
138-
139-

tests/compare_ab.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,37 @@
11
import numpy as np
22

3+
from optiphy.geant4_version import detect_geant4_version, geant4_series
4+
5+
6+
EXPECTED_DIFF = {
7+
"11.3": [14, 22, 32, 34, 40, 81, 85],
8+
"11.4+": [0, 30, 32, 34, 42, 69, 78, 85, 86],
9+
}
10+
11+
12+
def expected_diff_for_version(version):
13+
return EXPECTED_DIFF[geant4_series(version)]
14+
15+
16+
geant4_version = detect_geant4_version()
17+
expected_diff = expected_diff_for_version(geant4_version)
18+
319
a = np.load("/tmp/fakeuser/opticks/GEOM/fakegeom/simg4ox/ALL0_no_opticks_event_name/A000/record.npy")
420
b = np.load("/tmp/fakeuser/opticks/GEOM/fakegeom/simg4ox/ALL0_no_opticks_event_name/B000/f000/record.npy")
521

622
print(a.shape)
723
print(b.shape)
24+
print(f"GEANT4_VERSION={geant4_version}")
25+
print(f"EXPECTED_DIFF={expected_diff}")
826

927
assert a.shape == b.shape
1028

11-
diff = [i for i, (a, b) in enumerate(zip(a[:, 1:], b[:, 0:-1])) if not np.allclose(a, b, rtol=0, atol=1e-5)]
29+
# Geant4 and Opticks record one-step-shifted sequences for this test geometry,
30+
# so compare the aligned slices directly, including time.
31+
a_cmp = a[:, 1:]
32+
b_cmp = b[:, 0:-1]
33+
34+
diff = [i for i, (ac, bc) in enumerate(zip(a_cmp, b_cmp)) if not np.allclose(ac, bc, rtol=0, atol=1e-5)]
1235
print(diff)
1336

14-
assert diff == [14, 22, 32, 34, 40, 81, 85]
37+
assert diff == expected_diff

tests/test_GPURaytrace.sh

Lines changed: 54 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,50 @@
22

33
set -e
44

5-
65
SEED=42
76
TOLERANCE=113
87
PASS=true
98

9+
GEANT4_VERSION=$(geant4-version version)
10+
GEANT4_SERIES=$(geant4-version series)
11+
12+
declare -Ar SUPPORTED_GEANT4_SERIES=(
13+
["11.3"]=1
14+
["11.4+"]=1
15+
)
16+
17+
declare -Ar EXPECTED_G4_HITS=(
18+
["11.3:cerenkov"]=12672
19+
["11.3:cerenkov_scintillation"]=17473
20+
["11.4+:cerenkov"]=11842
21+
["11.4+:cerenkov_scintillation"]=21411
22+
)
23+
24+
declare -Ar EXPECTED_OPTICKS_HITS=(
25+
["11.3:cerenkov"]=12664
26+
["11.3:cerenkov_scintillation"]=17443
27+
["11.4+:cerenkov"]=11827
28+
["11.4+:cerenkov_scintillation"]=21390
29+
)
30+
31+
set_expected_hits() {
32+
local test_name=$1
33+
local key="${GEANT4_SERIES}:${test_name}"
34+
35+
if [[ -z ${SUPPORTED_GEANT4_SERIES[$GEANT4_SERIES]+x} ]]; then
36+
echo "FAILED: unsupported Geant4 version $GEANT4_VERSION. Add hit-count references for this release."
37+
exit 1
38+
fi
39+
40+
if [[ -z ${EXPECTED_G4_HITS[$key]+x} || -z ${EXPECTED_OPTICKS_HITS[$key]+x} ]]; then
41+
echo "FAILED: unknown test name: $test_name"
42+
exit 1
43+
fi
44+
45+
EXPECTED_G4=${EXPECTED_G4_HITS[$key]}
46+
EXPECTED_OPTICKS=${EXPECTED_OPTICKS_HITS[$key]}
47+
}
48+
1049
check_hits() {
1150
local label=$1
1251
local actual=$2
@@ -21,8 +60,12 @@ check_hits() {
2160
fi
2261
}
2362

63+
echo "Using Geant4 version $GEANT4_VERSION"
64+
2465
# ---- Test 1: Cerenkov only (opticks_raindrop.gdml) ----
2566
echo "=== Test 1: Cerenkov only (opticks_raindrop.gdml) ==="
67+
set_expected_hits cerenkov
68+
2669
echo "Running GPURaytrace with seed $SEED ..."
2770
OUTPUT=$(USER=fakeuser GEOM=fakegeom GPURaytrace \
2871
-g "$OPTICKS_HOME/tests/geom/opticks_raindrop.gdml" \
@@ -32,15 +75,17 @@ OUTPUT=$(USER=fakeuser GEOM=fakegeom GPURaytrace \
3275
G4_HITS=$(echo "$OUTPUT" | grep "Geant4: NumHits:" | awk '{print $NF}')
3376
OPTICKS_HITS=$(echo "$OUTPUT" | grep "Opticks: NumHits:" | awk '{print $NF}')
3477

35-
echo "Geant4: NumHits: $G4_HITS (expected 12672 +/- $TOLERANCE)"
36-
echo "Opticks: NumHits: $OPTICKS_HITS (expected 12664 +/- $TOLERANCE)"
78+
echo "Geant4: NumHits: $G4_HITS (expected $EXPECTED_G4 +/- $TOLERANCE)"
79+
echo "Opticks: NumHits: $OPTICKS_HITS (expected $EXPECTED_OPTICKS +/- $TOLERANCE)"
3780

38-
check_hits "Geant4 NumHits" "$G4_HITS" 12672
39-
check_hits "Opticks NumHits" "$OPTICKS_HITS" 12664
81+
check_hits "Geant4 NumHits" "$G4_HITS" "$EXPECTED_G4"
82+
check_hits "Opticks NumHits" "$OPTICKS_HITS" "$EXPECTED_OPTICKS"
4083

4184
# ---- Test 2: Cerenkov + Scintillation (opticks_raindrop_with_scintillation.gdml) ----
4285
echo ""
4386
echo "=== Test 2: Cerenkov + Scintillation (opticks_raindrop_with_scintillation.gdml) ==="
87+
set_expected_hits cerenkov_scintillation
88+
4489
echo "Running GPURaytrace with seed $SEED ..."
4590
OUTPUT=$(USER=fakeuser GEOM=fakegeom GPURaytrace \
4691
-g "$OPTICKS_HOME/tests/geom/opticks_raindrop_with_scintillation.gdml" \
@@ -50,11 +95,11 @@ OUTPUT=$(USER=fakeuser GEOM=fakegeom GPURaytrace \
5095
G4_HITS=$(echo "$OUTPUT" | grep "Geant4: NumHits:" | awk '{print $NF}')
5196
OPTICKS_HITS=$(echo "$OUTPUT" | grep "Opticks: NumHits:" | awk '{print $NF}')
5297

53-
echo "Geant4: NumHits: $G4_HITS (expected 17473 +/- $TOLERANCE)"
54-
echo "Opticks: NumHits: $OPTICKS_HITS (expected 17443 +/- $TOLERANCE)"
98+
echo "Geant4: NumHits: $G4_HITS (expected $EXPECTED_G4 +/- $TOLERANCE)"
99+
echo "Opticks: NumHits: $OPTICKS_HITS (expected $EXPECTED_OPTICKS +/- $TOLERANCE)"
55100

56-
check_hits "Geant4 NumHits" "$G4_HITS" 17473
57-
check_hits "Opticks NumHits" "$OPTICKS_HITS" 17443
101+
check_hits "Geant4 NumHits" "$G4_HITS" "$EXPECTED_G4"
102+
check_hits "Opticks NumHits" "$OPTICKS_HITS" "$EXPECTED_OPTICKS"
58103

59104
# ---- Summary ----
60105
echo ""

0 commit comments

Comments
 (0)