geant4-python-application 0.1.0__cp38-cp38-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.
- geant4_python_application/.dylibs/libG4FR.dylib +0 -0
- geant4_python_application/.dylibs/libG4GMocren.dylib +0 -0
- geant4_python_application/.dylibs/libG4OpenGL.dylib +0 -0
- geant4_python_application/.dylibs/libG4RayTracer.dylib +0 -0
- geant4_python_application/.dylibs/libG4ToolsSG.dylib +0 -0
- geant4_python_application/.dylibs/libG4Tree.dylib +0 -0
- geant4_python_application/.dylibs/libG4VRML.dylib +0 -0
- geant4_python_application/.dylibs/libG4analysis.dylib +0 -0
- geant4_python_application/.dylibs/libG4clhep.dylib +0 -0
- geant4_python_application/.dylibs/libG4digits_hits.dylib +0 -0
- geant4_python_application/.dylibs/libG4error_propagation.dylib +0 -0
- geant4_python_application/.dylibs/libG4event.dylib +0 -0
- geant4_python_application/.dylibs/libG4expat.dylib +0 -0
- geant4_python_application/.dylibs/libG4gdml.dylib +0 -0
- geant4_python_application/.dylibs/libG4geometry.dylib +0 -0
- geant4_python_application/.dylibs/libG4geomtext.dylib +0 -0
- geant4_python_application/.dylibs/libG4global.dylib +0 -0
- geant4_python_application/.dylibs/libG4graphics_reps.dylib +0 -0
- geant4_python_application/.dylibs/libG4intercoms.dylib +0 -0
- geant4_python_application/.dylibs/libG4interfaces.dylib +0 -0
- geant4_python_application/.dylibs/libG4materials.dylib +0 -0
- geant4_python_application/.dylibs/libG4mctruth.dylib +0 -0
- geant4_python_application/.dylibs/libG4modeling.dylib +0 -0
- geant4_python_application/.dylibs/libG4parmodels.dylib +0 -0
- geant4_python_application/.dylibs/libG4particles.dylib +0 -0
- geant4_python_application/.dylibs/libG4physicslists.dylib +0 -0
- geant4_python_application/.dylibs/libG4processes.dylib +0 -0
- geant4_python_application/.dylibs/libG4ptl.2.3.3.dylib +0 -0
- geant4_python_application/.dylibs/libG4readout.dylib +0 -0
- geant4_python_application/.dylibs/libG4run.dylib +0 -0
- geant4_python_application/.dylibs/libG4track.dylib +0 -0
- geant4_python_application/.dylibs/libG4tracking.dylib +0 -0
- geant4_python_application/.dylibs/libG4visHepRep.dylib +0 -0
- geant4_python_application/.dylibs/libG4vis_management.dylib +0 -0
- geant4_python_application/__init__.py +49 -0
- geant4_python_application/_geant4_application.cpython-38-darwin.so +0 -0
- geant4_python_application/_geant4_vis.cpython-38-darwin.so +0 -0
- geant4_python_application/_qt_from_pyqt5_qt5 +1 -0
- geant4_python_application/_version.py +24 -0
- geant4_python_application/application.py +442 -0
- geant4_python_application/cad.py +124 -0
- geant4_python_application/detector.py +97 -0
- geant4_python_application/distributed.py +45 -0
- geant4_python_application/events.py +87 -0
- geant4_python_application/files/__init__.py +1 -0
- geant4_python_application/files/datasets.py +297 -0
- geant4_python_application/files/directories.py +40 -0
- geant4_python_application/gdml.py +39 -0
- geant4_python_application/generator.py +63 -0
- geant4_python_application/geometry.py +52 -0
- geant4_python_application/io.py +154 -0
- geant4_python_application/optical.py +199 -0
- geant4_python_application/scoring.py +118 -0
- geant4_python_application-0.1.0.dist-info/METADATA +220 -0
- geant4_python_application-0.1.0.dist-info/RECORD +66 -0
- geant4_python_application-0.1.0.dist-info/WHEEL +5 -0
- geant4_python_application-0.1.0.dist-info/licenses/LICENSE +28 -0
- geant4_python_application-0.1.0.dist-info/licenses/NOTICE +21 -0
- geant4_python_application-0.1.0.dist-info/licenses/licenses/CLHEP-COPYING +674 -0
- geant4_python_application-0.1.0.dist-info/licenses/licenses/CLHEP-COPYING.LESSER +165 -0
- geant4_python_application-0.1.0.dist-info/licenses/licenses/Expat-COPYING +22 -0
- geant4_python_application-0.1.0.dist-info/licenses/licenses/Geant4-LICENSE +73 -0
- geant4_python_application-0.1.0.dist-info/licenses/licenses/PTL-LICENSE +21 -0
- geant4_python_application-0.1.0.dist-info/licenses/licenses/Xerces-C-LICENSE +202 -0
- geant4_python_application-0.1.0.dist-info/licenses/licenses/Xerces-C-NOTICE +10 -0
- 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
|