Skip to content

Tubs volume transmits ~25% fewer photons than an area-matched Hexagon volume under normal-incidence parallel-beam illumination #1107

Description

@gopalsurya2015

Title

Tubs volume transmits ~25% fewer photons than an area-matched Hexagon volume under normal-incidence parallel-beam illumination

Environment

  • OpenGATE version: 10.1.0
  • OpenGATE install path: /home/gopalsurya2015/gate_env/lib/python3.10/site-packages/opengate/__init__.py
  • Python version: 3.10.12
  • OS: Linux gopalgo-v1 6.8.0-1063-gcp #69~22.04.1-Ubuntu SMP Tue Jun 30 15:12:15 UTC 2026 x86_64 GNU/Linux
  • Geant4 version: not determined (see note below)

Summary

A Tubs volume (cylindrical hole) and a Hexagon volume (hexagonal
hole) with exactly matched cross-sectional area (0.5261 mm² each)
transmit meaningfully different fractions of a normal-incidence
parallel photon beam fired straight through them. For photons
travelling parallel to a hole's axis, transmission fraction should
depend only on open cross-sectional area, not hole shape — basic
solid geometry, independently confirmed by a from-scratch geometric
ray tracer built with no Geant4/OpenGATE dependency at all (see below).
OpenGATE's simulated result diverges from this by ~25%, consistently,
across 9 independent variations of the test.

Minimal reproduction

Attached: opengate_tubs_hexagon_bug_repro.py — self-contained,
depends only on opengate, scipy, uproot. Builds a single hole
(no repeated/parameterised lattice) drilled through a small tungsten
(G4_W) block, fires 2,000,000 monoenergetic 140 keV photons in a
perfectly parallel beam straight through it, and counts raw hits on a
downstream CdTe detector.

Latest run output:

Hex hole area:      0.5261 mm^2
Cylinder hole area: 0.5261 mm^2 (ratio: 1.0000)

   hexagon: 213863 raw hits / 2,000,000 primaries = 10.693% transmission
  cylinder: 160347 raw hits / 2,000,000 primaries = 8.017% transmission

Cylinder/Hexagon transmission ratio: 0.750

Expected ratio: ~1.0 (matched area, normal incidence, no physics
reason for the two shapes to differ). Observed: consistently ~0.73-0.75
across every test variation described below.

Investigation already performed

This ratio was reproduced across 9 independent, purpose-built
isolation tests, each targeting one specific candidate explanation:

  1. Full 56,834-hole lattice, full physics (real tungsten density,
    normal Compton/photoelectric physics):
    36.79% (hex) vs 26.97%
    (cylinder), ratio 0.733.
  2. Same lattice, deterministic-blocking material (density scaled
    ~100x normal, to make septa transmission ~e^-1600 ≈ 0 regardless
    of angle) — isolates geometry from photon physics:
    38.02% (hex)
    vs 27.88% (cylinder), ratio 0.733. Gap unchanged with photon
    physics effectively removed — rules out Compton scatter, septal
    penetration, and photoelectric absorption as the cause.
  3. Hole area measured directly via pixel-mask connected-component
    analysis on the transmission image: exact match, 0.5261 mm² for
    both shapes (ratio 1.000) — rules out a hole-sizing bug.
  4. Hole count and nearest-neighbor spacing measured directly (not
    assumed) from the same images: 22 holes each, spacing within
    0.15% of each other and within 0.8% of the pitch construction's
    theoretical value (1.27mm) — rules out a placement/pitch bug.
  5. Independent geometric ray tracer (pure Python/numpy, zero
    Geant4/OpenGATE dependency) replicating the exact same lattice
    (pitch, offset_nb=2 interleaving, hole areas) via Monte Carlo point
    sampling, testing 5 different hexagon rotational orientations:
    all converge to ~36%, matching EACH OTHER and matching the
    analytic open-area prediction — the gap does not appear at all
    in an independent model of the same geometry
    , ruling out
    orientation-dependent hexagon packing efficiency as an explanation.
  6. Exact constructor parameters read back from the live Geant4
    objects
    (not just what was requested) immediately before solid
    construction: hole.radius/hole.height (hex) and
    hole.rmax/hole.rmin/hole.dz/hole.sphi/hole.dphi
    (cylinder) all matched intended values exactly, including the
    confirmed-correct half-length convention for Tubs.dz.
  7. Single hole, no RepeatParametrisedVolume (this reproduction
    case): ratio 0.749 — matches the full-lattice ratio (0.733)
    closely, ruling out the repeat/parameterisation mechanism as the
    cause.
  8. Rotation transform chain eliminated entirely, by redefining
    the beam axis so neither the mother block nor the hole needs any
    rotation applied (both left at Geant4's default identity): ratio
    0.751 — unchanged from the rotated case (0.749). Also separately
    verified via scipy.spatial.transform.Rotation that the composed
    rotation in the rotated version was mathematically exact (hole
    axis lands at world [0,1,0] to floating-point precision).
  9. Boundary-grazing at exact axial incidence tested via a tiny
    0.01° beam tilt off exact [0,0,1] (negligible ~0.002mm lateral
    drift over the 14mm hole depth): ratio 0.750 — unchanged.
    Simulation time did increase substantially with the tilt (46.7s →
    170.8s for the cylinder case), but this timing change was NOT
    accompanied by any change in the transmission ratio, suggesting
    the timing and transmission effects are decoupled, not causally
    linked.

We also read G4Tubs::Inside(), both DistanceToIn() overloads, and
both DistanceToOut() overloads directly from the Geant4 source, and
confirmed all are purely analytic closed-form quadratic solutions —
no facetization, faceting parameter, or voxel-mesh approximation
anywhere in the navigation-relevant code paths (the only
mesh/vertices-related code, CreateRotatedVertices(), is used solely
for bounding-box/visualization purposes and is never called by the
navigation methods above).

What we're asking

We've exhausted what's diagnosable by reading source and building
isolation tests from the application side. Given the above, the
remaining discrepancy appears to be something in the interaction
between OpenGATE's Tubs wrapper and Geant4's G4Tubs/navigation
machinery that we haven't been able to identify without step-through
debugging access. Any guidance on:

  • whether this is a known issue,
  • whether there's a debug/verbose mode that would show per-step
    navigation decisions for a Tubs daughter volume, or
  • anything else in the OpenGATE→Geant4 volume-placement path worth
    checking

would be very much appreciated.

opengate_tubs_hexagon_bug_repro.py

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions