geant4-python-application 0.1.0__cp39-cp39-macosx_11_0_arm64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. geant4_python_application/.dylibs/libG4FR.dylib +0 -0
  2. geant4_python_application/.dylibs/libG4GMocren.dylib +0 -0
  3. geant4_python_application/.dylibs/libG4OpenGL.dylib +0 -0
  4. geant4_python_application/.dylibs/libG4RayTracer.dylib +0 -0
  5. geant4_python_application/.dylibs/libG4ToolsSG.dylib +0 -0
  6. geant4_python_application/.dylibs/libG4Tree.dylib +0 -0
  7. geant4_python_application/.dylibs/libG4VRML.dylib +0 -0
  8. geant4_python_application/.dylibs/libG4analysis.dylib +0 -0
  9. geant4_python_application/.dylibs/libG4clhep.dylib +0 -0
  10. geant4_python_application/.dylibs/libG4digits_hits.dylib +0 -0
  11. geant4_python_application/.dylibs/libG4error_propagation.dylib +0 -0
  12. geant4_python_application/.dylibs/libG4event.dylib +0 -0
  13. geant4_python_application/.dylibs/libG4expat.dylib +0 -0
  14. geant4_python_application/.dylibs/libG4gdml.dylib +0 -0
  15. geant4_python_application/.dylibs/libG4geometry.dylib +0 -0
  16. geant4_python_application/.dylibs/libG4geomtext.dylib +0 -0
  17. geant4_python_application/.dylibs/libG4global.dylib +0 -0
  18. geant4_python_application/.dylibs/libG4graphics_reps.dylib +0 -0
  19. geant4_python_application/.dylibs/libG4intercoms.dylib +0 -0
  20. geant4_python_application/.dylibs/libG4interfaces.dylib +0 -0
  21. geant4_python_application/.dylibs/libG4materials.dylib +0 -0
  22. geant4_python_application/.dylibs/libG4mctruth.dylib +0 -0
  23. geant4_python_application/.dylibs/libG4modeling.dylib +0 -0
  24. geant4_python_application/.dylibs/libG4parmodels.dylib +0 -0
  25. geant4_python_application/.dylibs/libG4particles.dylib +0 -0
  26. geant4_python_application/.dylibs/libG4physicslists.dylib +0 -0
  27. geant4_python_application/.dylibs/libG4processes.dylib +0 -0
  28. geant4_python_application/.dylibs/libG4ptl.2.3.3.dylib +0 -0
  29. geant4_python_application/.dylibs/libG4readout.dylib +0 -0
  30. geant4_python_application/.dylibs/libG4run.dylib +0 -0
  31. geant4_python_application/.dylibs/libG4track.dylib +0 -0
  32. geant4_python_application/.dylibs/libG4tracking.dylib +0 -0
  33. geant4_python_application/.dylibs/libG4visHepRep.dylib +0 -0
  34. geant4_python_application/.dylibs/libG4vis_management.dylib +0 -0
  35. geant4_python_application/__init__.py +49 -0
  36. geant4_python_application/_geant4_application.cpython-39-darwin.so +0 -0
  37. geant4_python_application/_geant4_vis.cpython-39-darwin.so +0 -0
  38. geant4_python_application/_qt_from_pyqt5_qt5 +1 -0
  39. geant4_python_application/_version.py +24 -0
  40. geant4_python_application/application.py +442 -0
  41. geant4_python_application/cad.py +124 -0
  42. geant4_python_application/detector.py +97 -0
  43. geant4_python_application/distributed.py +45 -0
  44. geant4_python_application/events.py +87 -0
  45. geant4_python_application/files/__init__.py +1 -0
  46. geant4_python_application/files/datasets.py +297 -0
  47. geant4_python_application/files/directories.py +40 -0
  48. geant4_python_application/gdml.py +39 -0
  49. geant4_python_application/generator.py +63 -0
  50. geant4_python_application/geometry.py +52 -0
  51. geant4_python_application/io.py +154 -0
  52. geant4_python_application/optical.py +199 -0
  53. geant4_python_application/scoring.py +118 -0
  54. geant4_python_application-0.1.0.dist-info/METADATA +220 -0
  55. geant4_python_application-0.1.0.dist-info/RECORD +66 -0
  56. geant4_python_application-0.1.0.dist-info/WHEEL +5 -0
  57. geant4_python_application-0.1.0.dist-info/licenses/LICENSE +28 -0
  58. geant4_python_application-0.1.0.dist-info/licenses/NOTICE +21 -0
  59. geant4_python_application-0.1.0.dist-info/licenses/licenses/CLHEP-COPYING +674 -0
  60. geant4_python_application-0.1.0.dist-info/licenses/licenses/CLHEP-COPYING.LESSER +165 -0
  61. geant4_python_application-0.1.0.dist-info/licenses/licenses/Expat-COPYING +22 -0
  62. geant4_python_application-0.1.0.dist-info/licenses/licenses/Geant4-LICENSE +73 -0
  63. geant4_python_application-0.1.0.dist-info/licenses/licenses/PTL-LICENSE +21 -0
  64. geant4_python_application-0.1.0.dist-info/licenses/licenses/Xerces-C-LICENSE +202 -0
  65. geant4_python_application-0.1.0.dist-info/licenses/licenses/Xerces-C-NOTICE +10 -0
  66. geant4_python_application-0.1.0.dist-info/licenses/licenses/zlib-LICENSE +22 -0
@@ -0,0 +1,39 @@
1
+ from __future__ import annotations
2
+
3
+ basic_gdml = R"""<?xml version="1.0" encoding="utf-8" standalone="no" ?>
4
+ <gdml xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
5
+ xsi:noNamespaceSchemaLocation="http://service-spi.web.cern.ch/service-spi/app/releases/GDML/schema/gdml.xsd">
6
+
7
+ <define>
8
+ <constant name="worldSize" value="1000"/>
9
+ <constant name="boxSize" value="500"/>
10
+ </define>
11
+
12
+ <solids>
13
+ <box name="WorldSolid" x="worldSize" y="worldSize" z="worldSize" lunit="mm"/>
14
+ <box name="boxSolid" x="boxSize" y="boxSize" z="boxSize" lunit="mm"/>
15
+ </solids>
16
+
17
+ <structure>
18
+ <volume name="boxVolume">
19
+ <materialref ref="G4_WATER"/>
20
+ <solidref ref="boxSolid"/>
21
+ </volume>
22
+
23
+ <volume name="World">
24
+ <materialref ref="G4_AIR"/>
25
+ <solidref ref="WorldSolid"/>
26
+
27
+ <physvol name="box">
28
+ <volumeref ref="boxVolume"/>
29
+ <position name="boxPosition" unit="mm" x="0" y="0" z="0"/>
30
+ </physvol>
31
+
32
+ </volume>
33
+ </structure>
34
+
35
+ <setup name="Default" version="1.0">
36
+ <world ref="World"/>
37
+ </setup>
38
+ </gdml>
39
+ """
@@ -0,0 +1,63 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from geant4_python_application.application import Message
6
+
7
+ if TYPE_CHECKING:
8
+ from geant4_python_application.application import Application
9
+
10
+
11
+ class Generator:
12
+ """Configure Geant4's particle gun or General Particle Source (GPS)."""
13
+
14
+ def __init__(self, application: Application):
15
+ self._application = application
16
+
17
+ @property
18
+ def type(self) -> str:
19
+ return self._application._send_and_recv(
20
+ Message("generator", "get_type", (), {})
21
+ )
22
+
23
+ @type.setter
24
+ def type(self, generator_type: str):
25
+ self._application._send_and_recv(
26
+ Message("generator", "set_type", (generator_type,), {})
27
+ )
28
+
29
+ def use_gun(self) -> Generator:
30
+ self.type = "gun"
31
+ return self
32
+
33
+ def use_gps(self) -> Generator:
34
+ self.type = "gps"
35
+ return self
36
+
37
+ def particle(self, name: str) -> Generator:
38
+ command = "/gps/particle" if self.type == "gps" else "/gun/particle"
39
+ self._application.command(f"{command} {name}")
40
+ return self
41
+
42
+ def energy(self, value: float, unit: str = "MeV") -> Generator:
43
+ command = "/gps/ene/mono" if self.type == "gps" else "/gun/energy"
44
+ self._application.command(f"{command} {value} {unit}")
45
+ return self
46
+
47
+ def position(self, x: float, y: float, z: float, unit: str = "cm") -> Generator:
48
+ if self.type == "gps":
49
+ self._application.command("/gps/pos/type Point")
50
+ self._application.command(f"/gps/pos/centre {x} {y} {z} {unit}")
51
+ else:
52
+ self._application.command(f"/gun/position {x} {y} {z} {unit}")
53
+ return self
54
+
55
+ def direction(self, x: float, y: float, z: float) -> Generator:
56
+ command = "/gps/direction" if self.type == "gps" else "/gun/direction"
57
+ self._application.command(f"{command} {x} {y} {z}")
58
+ return self
59
+
60
+ def commands(self, commands: list[str]) -> Generator:
61
+ """Apply advanced source commands such as GPS distributions."""
62
+ self._application.commands(commands)
63
+ return self
@@ -0,0 +1,52 @@
1
+ from __future__ import annotations
2
+
3
+ """Pure-Python GDML geometry generators.
4
+
5
+ These helpers expand regular layouts into ordinary GDML placements. They are
6
+ useful where an official example uses a live ``G4VPVParameterisation`` callback:
7
+ the geometry is equivalent at initialization time, but there is no callback
8
+ during tracking.
9
+ """
10
+
11
+
12
+ def linear_array_gdml(
13
+ count: int,
14
+ *,
15
+ spacing_mm: float = 20.0,
16
+ box_size_mm: float = 10.0,
17
+ material: str = "G4_Si",
18
+ world_material: str = "G4_AIR",
19
+ ) -> str:
20
+ """Return GDML containing ``count`` equally spaced boxes along x."""
21
+ if count < 1:
22
+ raise ValueError("count must be at least one")
23
+ if spacing_mm <= 0 or box_size_mm <= 0:
24
+ raise ValueError("spacing_mm and box_size_mm must be positive")
25
+ extent = (count - 1) * spacing_mm + box_size_mm
26
+ world_x = max(100.0, extent + 4.0 * spacing_mm)
27
+ placements = []
28
+ x0 = -0.5 * (count - 1) * spacing_mm
29
+ for index in range(count):
30
+ x = x0 + index * spacing_mm
31
+ placements.append(
32
+ f''' <physvol name="cell_{index}">
33
+ <volumeref ref="CellVolume"/>
34
+ <position name="cell_position_{index}" unit="mm" x="{x:.12g}" y="0" z="0"/>
35
+ </physvol>'''
36
+ )
37
+ return f'''<?xml version="1.0" encoding="UTF-8" ?>
38
+ <gdml xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://service-spi.web.cern.ch/service-spi/app/releases/GDML/schema/gdml.xsd">
39
+ <solids>
40
+ <box name="WorldSolid" lunit="mm" x="{world_x:.12g}" y="100" z="100"/>
41
+ <box name="CellSolid" lunit="mm" x="{box_size_mm:.12g}" y="40" z="40"/>
42
+ </solids>
43
+ <structure>
44
+ <volume name="CellVolume"><materialref ref="{material}"/><solidref ref="CellSolid"/></volume>
45
+ <volume name="World">
46
+ <materialref ref="{world_material}"/><solidref ref="WorldSolid"/>
47
+ {chr(10).join(placements)}
48
+ </volume>
49
+ </structure>
50
+ <setup name="Default" version="1.0"><world ref="World"/></setup>
51
+ </gdml>
52
+ '''
@@ -0,0 +1,154 @@
1
+ from __future__ import annotations
2
+
3
+ """VTK / ROOT / external-generator helpers (pure Python, optional deps).
4
+
5
+ - mesh_to_vtk: write Cartesian energy mesh to legacy VTK STRUCTURED_POINTS.
6
+ - events_to_parquet / events_from_parquet: always available (pyarrow).
7
+ - events_to_root / events_from_root: via uproot if installed.
8
+ - hepmc_to_primaries: minimal HepMC2-ASCII -> awkward primaries.
9
+ """
10
+
11
+ from pathlib import Path
12
+
13
+ import awkward as ak
14
+ import numpy as np
15
+
16
+
17
+ def mesh_to_vtk(mesh: np.ndarray, edges: tuple, filename: str | Path) -> Path:
18
+ """Write (nx,ny,nz) energy mesh to legacy VTK STRUCTURED_POINTS file."""
19
+ mesh = np.asarray(mesh, dtype=float)
20
+ if mesh.ndim != 3:
21
+ msg = f"mesh must be 3D, got shape {mesh.shape}"
22
+ raise ValueError(msg)
23
+ nx, ny, nz = mesh.shape
24
+ if len(edges) != 3:
25
+ msg = "edges must be (x_edges, y_edges, z_edges)"
26
+ raise ValueError(msg)
27
+ origins, spacings = [], []
28
+ dims = []
29
+ for n, e in zip((nx, ny, nz), edges):
30
+ e = np.asarray(e, dtype=float)
31
+ if len(e) != n + 1:
32
+ msg = f"edges length {len(e)} inconsistent with mesh dim {n}"
33
+ raise ValueError(msg)
34
+ origins.append(float(e[0]))
35
+ spacings.append(float(e[1] - e[0]) if n > 0 else 1.0)
36
+ dims.append(n + 1 if False else n) # points == cells for cell data
37
+ out = Path(filename)
38
+ # VTK Fortran ordering: x varies fastest
39
+ flat = mesh.ravel(order="F")
40
+ with out.open("w") as f:
41
+ f.write("# vtk DataFile Version 3.0\n")
42
+ f.write("geant4 energy mesh\n")
43
+ f.write("ASCII\n")
44
+ f.write("DATASET STRUCTURED_POINTS\n")
45
+ f.write(f"DIMENSIONS {nx} {ny} {nz}\n")
46
+ f.write(f"ORIGIN {origins[0]} {origins[1]} {origins[2]}\n")
47
+ f.write(f"SPACING {spacings[0]} {spacings[1]} {spacings[2]}\n")
48
+ f.write(f"POINT_DATA {nx * ny * nz}\n")
49
+ f.write("SCALARS energy_keV float 1\n")
50
+ f.write("LOOKUP_TABLE default\n")
51
+ for v in flat:
52
+ f.write(f"{v}\n")
53
+ return out
54
+
55
+
56
+ def events_to_parquet(events: ak.Array, filename: str | Path) -> Path:
57
+ out = Path(filename)
58
+ ak.to_parquet(events, out)
59
+ return out
60
+
61
+
62
+ def events_from_parquet(filename: str | Path) -> ak.Array:
63
+ return ak.from_parquet(Path(filename))
64
+
65
+
66
+ def events_to_root(
67
+ events: ak.Array, filename: str | Path, treename: str = "events"
68
+ ) -> Path:
69
+ """Write flat per-event summary + hits to ROOT via uproot (requires uproot)."""
70
+ try:
71
+ import uproot
72
+ except ImportError as e:
73
+ msg = "uproot is required for ROOT I/O: pip install uproot"
74
+ raise ImportError(msg) from e
75
+ out = Path(filename)
76
+ # Flatten what ROOT can store: per-event id + deposited energy + nhits.
77
+ # Full nested step records stay in parquet; ROOT holds analysis summary.
78
+ import geant4_python_application as g4
79
+
80
+ edep = np.asarray(g4.Scoring.energy_deposit(events), dtype=np.float64)
81
+ ids = (
82
+ np.asarray(events.id, dtype=np.int64)
83
+ if "id" in events.fields
84
+ else np.arange(len(events))
85
+ )
86
+ with uproot.recreate(out) as f:
87
+ f[treename] = {"id": ids, "edep_keV": edep}
88
+ return out
89
+
90
+
91
+ def events_from_root(filename: str | Path, treename: str = "events") -> dict:
92
+ try:
93
+ import uproot
94
+ except ImportError as e:
95
+ msg = "uproot is required for ROOT I/O: pip install uproot"
96
+ raise ImportError(msg) from e
97
+ with uproot.open(f"{filename}:{treename}") as t:
98
+ return t.arrays(library="np")
99
+
100
+
101
+ _PDG_TO_G4 = {
102
+ 11: "e-",
103
+ -11: "e+",
104
+ 22: "gamma",
105
+ 13: "mu-",
106
+ -13: "mu+",
107
+ 2112: "neutron",
108
+ 2212: "proton",
109
+ 211: "pi+",
110
+ -211: "pi-",
111
+ 111: "pi0",
112
+ 321: "kaon+",
113
+ -321: "kaon-",
114
+ 12: "nu_e",
115
+ -12: "anti_nu_e",
116
+ 14: "nu_mu",
117
+ -14: "anti_nu_mu",
118
+ }
119
+
120
+
121
+ def hepmc_to_primaries(hepmc_text: str) -> ak.Array:
122
+ """Parse minimal HepMC2 ASCII (E/P lines) into awkward primaries.
123
+
124
+ Expects lines like: P <id> <pdg> <px_GeV> <py> <pz> <E_GeV> ...
125
+ Position defaults to origin; direction = p/|p|; energy = kinetic approx.
126
+ Unknown PDG codes fall back to geantino.
127
+ """
128
+ records = []
129
+ for line in hepmc_text.splitlines():
130
+ s = line.strip().split()
131
+ if not s or s[0] != "P":
132
+ continue
133
+ if len(s) < 7:
134
+ continue
135
+ pdg = int(s[2])
136
+ px, py, pz, e = (float(s[3]), float(s[4]), float(s[5]), float(s[6]))
137
+ p = float(np.sqrt(px * px + py * py + pz * pz))
138
+ if p == 0:
139
+ continue
140
+ particle = _PDG_TO_G4.get(pdg, "geantino")
141
+ # awkward primary energy convention in this package: keV numbers?
142
+ # Application primaries use raw doubles passed to gun in keV? Keep MeV->keV.
143
+ records.append(
144
+ {
145
+ "particle": particle,
146
+ "energy": e * 1e6, # GeV -> keV (gun awkward path uses keV)
147
+ "direction": {"x": px / p, "y": py / p, "z": pz / p},
148
+ "position": {"x": 0.0, "y": 0.0, "z": 0.0},
149
+ }
150
+ )
151
+ if not records:
152
+ msg = "no P lines parsed from HepMC text"
153
+ raise ValueError(msg)
154
+ return ak.Array(records)
@@ -0,0 +1,199 @@
1
+ from __future__ import annotations
2
+
3
+ """Minimal optical-materials/surfaces helpers (GDML-based).
4
+
5
+ Geant4 optical properties (RINDEX, scintillation, WLS, ...) and optical
6
+ surfaces (skin/border) are defined in GDML via <matrix>, <property> and
7
+ <opticalsurface>/<skinsurface>/<bordersurface> tags. This module provides a
8
+ compact, fast-simulating water-scintillator GDML example plus UI-command
9
+ helpers for tuning optical processes at run time.
10
+ """
11
+
12
+ optical_water_gdml = R"""<?xml version="1.0" encoding="UTF-8" ?>
13
+ <gdml xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://service-spi.web.cern.ch/service-spi/app/releases/GDML/schema/gdml.xsd">
14
+ <define>
15
+ <matrix coldim="2" name="WATERRINDEX" values="2.034*eV 1.3435 3.026*eV 1.3545 4.136*eV 1.3608"/>
16
+ <matrix coldim="2" name="WATERABSLENGTH" values="2.034*eV 20000 4.136*eV 14500"/>
17
+ <matrix coldim="2" name="SCINTCOMP" values="2.034*eV 1 3.026*eV 1 4.136*eV 1"/>
18
+ <matrix coldim="1" name="SCINTYIELD" values="50"/>
19
+ <matrix coldim="1" name="RESOLUTIONSCALE" values="1"/>
20
+ <matrix coldim="1" name="TIMECONSTANT1" values="1"/>
21
+ <position name="center" unit="mm" x="0" y="0" z="0"/>
22
+ </define>
23
+ <materials>
24
+ <element Z="1" formula="H" name="Hydrogen"><atom value="1.01"/></element>
25
+ <element Z="8" formula="O" name="Oxygen"><atom value="16.0"/></element>
26
+ <material name="WaterScint" state="liquid">
27
+ <property name="RINDEX" ref="WATERRINDEX"/>
28
+ <property name="ABSLENGTH" ref="WATERABSLENGTH"/>
29
+ <property name="SCINTILLATIONCOMPONENT1" ref="SCINTCOMP"/>
30
+ <property name="SCINTILLATIONYIELD" ref="SCINTYIELD"/>
31
+ <property name="RESOLUTIONSCALE" ref="RESOLUTIONSCALE"/>
32
+ <property name="SCINTILLATIONTIMECONSTANT1" ref="TIMECONSTANT1"/>
33
+ <D unit="g/cm3" value="1"/>
34
+ <fraction n="0.1119" ref="Hydrogen"/>
35
+ <fraction n="0.8881" ref="Oxygen"/>
36
+ </material>
37
+ <material name="AirOpt" state="gas">
38
+ <D unit="g/cm3" value="0.00129"/>
39
+ <fraction n="0.7" ref="Hydrogen"/>
40
+ <fraction n="0.3" ref="Oxygen"/>
41
+ </material>
42
+ </materials>
43
+ <solids>
44
+ <box lunit="mm" name="WorldSolid" x="1000" y="1000" z="1000"/>
45
+ <box lunit="mm" name="ScintSolid" x="500" y="500" z="500"/>
46
+ <opticalsurface finish="polished" model="glisur" name="ScintSurface" type="dielectric_dielectric" value="1.0"/>
47
+ </solids>
48
+ <structure>
49
+ <volume name="ScintVolume">
50
+ <materialref ref="WaterScint"/>
51
+ <solidref ref="ScintSolid"/>
52
+ </volume>
53
+ <volume name="World">
54
+ <materialref ref="AirOpt"/>
55
+ <solidref ref="WorldSolid"/>
56
+ <physvol name="scint">
57
+ <volumeref ref="ScintVolume"/>
58
+ <positionref ref="center"/>
59
+ </physvol>
60
+ </volume>
61
+ <skinsurface name="ScintSkin" surfaceproperty="ScintSurface">
62
+ <volumeref ref="ScintVolume"/>
63
+ </skinsurface>
64
+ </structure>
65
+ <setup name="Default" version="1.0">
66
+ <world ref="World"/>
67
+ </setup>
68
+ </gdml>
69
+ """
70
+
71
+
72
+ # Compact, self-contained optical geometries used by the Python adaptations of
73
+ # Geant4's LXe and WLS examples. They intentionally model the transport
74
+ # concepts rather than the examples' custom PMT/sensitive-detector classes.
75
+ lxe_scintillator_gdml = R"""<?xml version="1.0" encoding="UTF-8" ?>
76
+ <gdml xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://service-spi.web.cern.ch/service-spi/app/releases/GDML/schema/gdml.xsd">
77
+ <define>
78
+ <matrix coldim="2" name="LXeRINDEX" values="6.2*eV 1.59 7.0*eV 1.61 7.5*eV 1.64"/>
79
+ <matrix coldim="2" name="LXeABSLENGTH" values="6.2*eV 1000 7.5*eV 1000"/>
80
+ <matrix coldim="2" name="LXeSCINT" values="6.2*eV 0.1 7.0*eV 1.0 7.5*eV 0.1"/>
81
+ <matrix coldim="1" name="LXeYIELD" values="120"/>
82
+ <matrix coldim="1" name="LXeRESOLUTION" values="1"/>
83
+ <matrix coldim="1" name="LXeTIME" values="2.2"/>
84
+ <matrix coldim="2" name="GasRINDEX" values="1.0*eV 1.0 8.0*eV 1.0"/>
85
+ <position name="center" unit="mm" x="0" y="0" z="0"/>
86
+ </define>
87
+ <materials>
88
+ <element Z="54" formula="Xe" name="Xenon"><atom value="131.293"/></element>
89
+ <element Z="7" formula="N" name="Nitrogen"><atom value="14.007"/></element>
90
+ <material name="LiquidXenon" state="liquid">
91
+ <property name="RINDEX" ref="LXeRINDEX"/>
92
+ <property name="ABSLENGTH" ref="LXeABSLENGTH"/>
93
+ <property name="SCINTILLATIONCOMPONENT1" ref="LXeSCINT"/>
94
+ <property name="SCINTILLATIONYIELD" ref="LXeYIELD"/>
95
+ <property name="RESOLUTIONSCALE" ref="LXeRESOLUTION"/>
96
+ <property name="SCINTILLATIONTIMECONSTANT1" ref="LXeTIME"/>
97
+ <D unit="g/cm3" value="2.95"/>
98
+ <fraction n="1" ref="Xenon"/>
99
+ </material>
100
+ <material name="NitrogenGas" state="gas">
101
+ <property name="RINDEX" ref="GasRINDEX"/>
102
+ <D unit="g/cm3" value="0.001165"/>
103
+ <fraction n="1" ref="Nitrogen"/>
104
+ </material>
105
+ </materials>
106
+ <solids>
107
+ <box lunit="mm" name="WorldSolid" x="1000" y="1000" z="1000"/>
108
+ <box lunit="mm" name="LXeSolid" x="300" y="300" z="300"/>
109
+ <opticalsurface finish="polished" model="unified" name="LXeSurface" type="dielectric_dielectric" value="1.0"/>
110
+ </solids>
111
+ <structure>
112
+ <volume name="LXeVolume">
113
+ <materialref ref="LiquidXenon"/>
114
+ <solidref ref="LXeSolid"/>
115
+ </volume>
116
+ <volume name="World">
117
+ <materialref ref="NitrogenGas"/>
118
+ <solidref ref="WorldSolid"/>
119
+ <physvol name="lxe"><volumeref ref="LXeVolume"/><positionref ref="center"/></physvol>
120
+ </volume>
121
+ <skinsurface name="LXeSkin" surfaceproperty="LXeSurface"><volumeref ref="LXeVolume"/></skinsurface>
122
+ </structure>
123
+ <setup name="Default" version="1.0"><world ref="World"/></setup>
124
+ </gdml>
125
+ """
126
+
127
+
128
+ wls_fiber_gdml = R"""<?xml version="1.0" encoding="UTF-8" ?>
129
+ <gdml xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://service-spi.web.cern.ch/service-spi/app/releases/GDML/schema/gdml.xsd">
130
+ <define>
131
+ <matrix coldim="2" name="CoreRINDEX" values="2.0*eV 1.60 4.0*eV 1.60"/>
132
+ <matrix coldim="2" name="CoreABSLENGTH" values="2.0*eV 5000 4.0*eV 5000"/>
133
+ <matrix coldim="2" name="WLSABSLENGTH" values="2.0*eV 5000 2.8*eV 5 4.0*eV 1"/>
134
+ <matrix coldim="2" name="WLSEMISSION" values="2.0*eV 0.1 2.4*eV 1.0 2.8*eV 0.1 4.0*eV 0.0"/>
135
+ <matrix coldim="1" name="WLSTIME" values="0.5"/>
136
+ <matrix coldim="2" name="CladRINDEX" values="2.0*eV 1.49 4.0*eV 1.49"/>
137
+ <matrix coldim="2" name="AirRINDEX" values="2.0*eV 1.0 4.0*eV 1.0"/>
138
+ <position name="center" unit="mm" x="0" y="0" z="0"/>
139
+ </define>
140
+ <materials>
141
+ <element Z="1" formula="H" name="HydrogenWLS"><atom value="1.008"/></element>
142
+ <element Z="6" formula="C" name="CarbonWLS"><atom value="12.011"/></element>
143
+ <material name="WLSCore" state="solid">
144
+ <property name="RINDEX" ref="CoreRINDEX"/>
145
+ <property name="ABSLENGTH" ref="CoreABSLENGTH"/>
146
+ <property name="WLSABSLENGTH" ref="WLSABSLENGTH"/>
147
+ <property name="WLSCOMPONENT" ref="WLSEMISSION"/>
148
+ <property name="WLSTIMECONSTANT" ref="WLSTIME"/>
149
+ <D unit="g/cm3" value="1.05"/>
150
+ <fraction n="0.0774" ref="HydrogenWLS"/><fraction n="0.9226" ref="CarbonWLS"/>
151
+ </material>
152
+ <material name="WLSCladding" state="solid">
153
+ <property name="RINDEX" ref="CladRINDEX"/>
154
+ <D unit="g/cm3" value="1.19"/>
155
+ <fraction n="0.0774" ref="HydrogenWLS"/><fraction n="0.9226" ref="CarbonWLS"/>
156
+ </material>
157
+ <material name="WLSAir" state="gas">
158
+ <property name="RINDEX" ref="AirRINDEX"/>
159
+ <D unit="g/cm3" value="0.0012"/>
160
+ <fraction n="0.0774" ref="HydrogenWLS"/><fraction n="0.9226" ref="CarbonWLS"/>
161
+ </material>
162
+ </materials>
163
+ <solids>
164
+ <box lunit="mm" name="WorldSolid" x="100" y="100" z="500"/>
165
+ <tube aunit="deg" deltaphi="360" lunit="mm" name="CladdingSolid" rmax="1.2" rmin="0" startphi="0" z="400"/>
166
+ <tube aunit="deg" deltaphi="360" lunit="mm" name="CoreSolid" rmax="1.0" rmin="0" startphi="0" z="400"/>
167
+ </solids>
168
+ <structure>
169
+ <volume name="CoreVolume"><materialref ref="WLSCore"/><solidref ref="CoreSolid"/></volume>
170
+ <volume name="CladdingVolume">
171
+ <materialref ref="WLSCladding"/><solidref ref="CladdingSolid"/>
172
+ <physvol name="fiber_core"><volumeref ref="CoreVolume"/><positionref ref="center"/></physvol>
173
+ </volume>
174
+ <volume name="World">
175
+ <materialref ref="WLSAir"/><solidref ref="WorldSolid"/>
176
+ <physvol name="fiber"><volumeref ref="CladdingVolume"/><positionref ref="center"/></physvol>
177
+ </volume>
178
+ </structure>
179
+ <setup name="Default" version="1.0"><world ref="World"/></setup>
180
+ </gdml>
181
+ """
182
+
183
+ #: Common optical-process UI commands for Geant4 11.2 (apply via app.commands(...)).
184
+ #: Verified against /process/optical/scintillation and /process/optical/cerenkov.
185
+ optical_tuning_commands = [
186
+ "/process/optical/scintillation/setStackPhotons true",
187
+ "/process/optical/cerenkov/setMaxPhotons 100",
188
+ "/process/optical/verbose 1",
189
+ ]
190
+
191
+
192
+ def scintillation_commands(
193
+ stack_photons: bool = True, max_cerenkov: int = 100
194
+ ) -> list[str]:
195
+ """Return UI commands tuning scintillation/Cerenkov at run time (Geant4 11.2)."""
196
+ return [
197
+ f"/process/optical/scintillation/setStackPhotons {'true' if stack_photons else 'false'}",
198
+ f"/process/optical/cerenkov/setMaxPhotons {max_cerenkov}",
199
+ ]
@@ -0,0 +1,118 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import TYPE_CHECKING
5
+
6
+ import awkward as ak
7
+ import numpy as np
8
+
9
+ if TYPE_CHECKING:
10
+ from geant4_python_application.application import Application
11
+
12
+
13
+ class NativeScoringMesh:
14
+ """Configure a Geant4 command-based Cartesian scoring mesh.
15
+
16
+ This uses Geant4's native ``G4ScoringManager`` commands. It is distinct
17
+ from :class:`Scoring`, which bins recorded steps in Python after a run.
18
+ """
19
+
20
+ def __init__(
21
+ self,
22
+ application: Application,
23
+ name: str = "mesh",
24
+ quantity: str = "eDep",
25
+ ):
26
+ self.application = application
27
+ self.name = name
28
+ self.quantity = quantity
29
+ self._closed = False
30
+
31
+ def box(
32
+ self,
33
+ half_size: tuple[float, float, float],
34
+ bins: tuple[int, int, int],
35
+ *,
36
+ unit: str = "mm",
37
+ center: tuple[float, float, float] = (0.0, 0.0, 0.0),
38
+ ) -> NativeScoringMesh:
39
+ """Create a box mesh; ``half_size`` follows Geant4 mesh semantics."""
40
+ if self._closed:
41
+ raise RuntimeError("scoring mesh is already closed")
42
+ sx, sy, sz = half_size
43
+ nx, ny, nz = bins
44
+ cx, cy, cz = center
45
+ self.application.commands(
46
+ [
47
+ f"/score/create/boxMesh {self.name}",
48
+ f"/score/mesh/boxSize {sx} {sy} {sz} {unit}",
49
+ f"/score/mesh/nBin {nx} {ny} {nz}",
50
+ f"/score/mesh/translate/xyz {cx} {cy} {cz} {unit}",
51
+ f"/score/quantity/energyDeposit {self.quantity}",
52
+ "/score/close",
53
+ ]
54
+ )
55
+ self._closed = True
56
+ return self
57
+
58
+ def dump(self, path: str | Path, *, option: str = "") -> Path:
59
+ """Dump the native mesh quantity after a run and return its path."""
60
+ if not self._closed:
61
+ raise RuntimeError("configure and close the scoring mesh before dumping")
62
+ output = Path(path).expanduser().resolve()
63
+ output.parent.mkdir(parents=True, exist_ok=True)
64
+ if " " in str(output):
65
+ raise ValueError("Geant4 scoring output paths cannot contain spaces")
66
+ suffix = f" {option}" if option else ""
67
+ self.application.command(
68
+ f"/score/dumpQuantityToFile {self.name} {self.quantity} {output}{suffix}"
69
+ )
70
+ return output
71
+
72
+
73
+ class Scoring:
74
+ """Scoring operations over Geant4 event, track, and step records."""
75
+
76
+ @staticmethod
77
+ def energy_deposit(events: ak.Array, volume: str | None = None) -> ak.Array:
78
+ """Return deposited energy in keV for each event."""
79
+ steps = events.track.step
80
+ energy = steps.energy
81
+ if volume is not None:
82
+ energy = energy[steps.volume == volume]
83
+ return ak.sum(ak.flatten(energy, axis=-1), axis=1)
84
+
85
+ @staticmethod
86
+ def hits(events: ak.Array, volume: str | None = None) -> ak.Array:
87
+ """Return positive-energy steps, optionally restricted to a volume."""
88
+ return events.hits(volume)
89
+
90
+ @staticmethod
91
+ def energy_mesh(
92
+ events: ak.Array,
93
+ bins: int | tuple[int, int, int] = 10,
94
+ mesh_range: tuple[tuple[float, float], tuple[float, float], tuple[float, float]]
95
+ | None = None,
96
+ volume: str | None = None,
97
+ ) -> tuple[np.ndarray, tuple[np.ndarray, np.ndarray, np.ndarray]]:
98
+ """Bin step energy into a Cartesian 3D mesh.
99
+
100
+ Positions are in millimetres and mesh values are deposited energy in keV.
101
+ """
102
+ hits = Scoring.hits(events, volume)
103
+ weights = ak.to_numpy(ak.flatten(hits.energy, axis=None))
104
+ if len(weights) == 0:
105
+ shape = (bins, bins, bins) if isinstance(bins, int) else bins
106
+ return np.zeros(shape), (np.array([]), np.array([]), np.array([]))
107
+
108
+ points = np.column_stack(
109
+ [
110
+ ak.to_numpy(ak.flatten(hits.position.x, axis=None)),
111
+ ak.to_numpy(ak.flatten(hits.position.y, axis=None)),
112
+ ak.to_numpy(ak.flatten(hits.position.z, axis=None)),
113
+ ]
114
+ )
115
+ mesh, edges = np.histogramdd(
116
+ points, bins=bins, range=mesh_range, weights=weights
117
+ )
118
+ return mesh, edges