Files
giant/giant/materials.py
T
lars 68fb99bed8 Condition on material/particle physical properties instead of learned embeddings
Adds model.conditioning = "physical" | "embedding": physical mode routes
particle mass/charge and material Z_eff/A_eff/density/X0/lambda_int through
small MLPs to replace the learned PDG/material embedding tables, so the
surrogate generalizes to PDG codes/materials outside the training vocab
instead of memorizing it. "embedding" stays available as the comparison
baseline (old checkpoints without the key default to it).

Stage 2 now regresses a secondary's mass/charge directly against a fixed
physics-derived target instead of a learned/snapped embedding, and uses no
snapping at inference — the model's raw predicted (mass, charge) is the
secondary's physical identity, including for its own further rollout steps.
A separate reporting-only nearest-known-PDG lookup (never fed back into the
model) populates output pdg columns / the embedding-mode rollout fallback.

giant/materials.py's table is populated with Geant4's own built-in NIST
constants (Z_eff, A_eff, density, X0, lambda_int), extracted directly from
the Geant4 11.4.1 build vendored in minicalosim via G4NistManager rather
than hand-typed literature values. G4_LYSO is left unfilled: confirmed (both
by runtime lookup and by searching minicalosim's history) that it's never
actually a constructed Geant4 material there, only documentation/UI color-map
text.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-17 15:12:54 +02:00

146 lines
5.3 KiB
Python

"""Material physical-property table for the "physical" conditioning mode.
Values are Geant4's own built-in NIST material constants, not hand-typed
literature numbers -- extracted directly from a Geant4 11.4.1 build (the one
vendored in /home/lars/Programming/minicalosim/lib/geant4, built at
minicalosim/build/geant4-install) via a small standalone C++ program linked
against that build (G4NistManager::FindOrBuildMaterial + G4Material::
GetDensity/GetRadlen/GetNuclearInterLength + G4IonisParamMat::GetZeffective).
`a_eff` isn't directly exposed by Geant4, so it's computed with the same
atomic-number-density-weighted-average formula Geant4 itself uses for Zeff
(see G4IonisParamMat::BuildFluctModel in
lib/geant4/source/materials/src/G4IonisParamMat.cc), just applied to A
instead of Z -- for a single-element material this is exact; for a compound
it matches Geant4's own effective-Z convention rather than a different
weighting scheme.
Never silently substitute a default for a material missing from this table
(see UnknownMaterialError/MaterialPropertiesNotFilledError below) -- a wrong
material property would corrupt a whole conditioning axis without any
visible symptom until deep into training.
"""
from __future__ import annotations
from typing import NamedTuple
import numpy as np
class MaterialProperties(NamedTuple):
z_eff: float | None # effective atomic number
a_eff: float | None # effective atomic mass [g/mol]
density: float | None # [g/cm^3]
x0: float | None # radiation length [cm]
lambda_int: float | None # nuclear interaction length [cm]
class UnknownMaterialError(KeyError):
pass
class MaterialPropertiesNotFilledError(NotImplementedError):
pass
# Keys: every NIST material name seen in
# physics/detector-design/minicalosim-geometry.md, plus G4_AIR/G4_lAr which
# appear in dataset parquet files but not that doc. Values from Geant4's
# built-in NIST database (see module docstring) -- all present except
# G4_LYSO, which is not actually a stock Geant4 NIST material (confirmed:
# G4NistManager::FindOrBuildMaterial("G4_LYSO") fails to build in the
# vendored Geant4 11.4.1; it only appears as a plotting-color key in
# minicalosim/bind/G4Calo.py, never constructed in DetectorConstruction.cc)
# -- left unfilled until it's either built as a custom material (e.g.
# Lu1.8Y0.2SiO5:Ce) or dropped from the geometry menu.
MATERIAL_PROPERTIES: dict[str, MaterialProperties] = {
"G4_PbWO4": MaterialProperties(
z_eff=31.333333,
a_eff=75.843426,
density=8.28,
x0=0.892453,
lambda_int=20.739740,
),
"G4_CESIUM_IODIDE": MaterialProperties(
z_eff=54.0, a_eff=129.904539, density=4.51, x0=1.860288, lambda_int=39.305990
),
"G4_Pb": MaterialProperties(
z_eff=82.0, a_eff=207.216962, density=11.35, x0=0.561253, lambda_int=18.247950
),
"G4_W": MaterialProperties(
z_eff=74.0, a_eff=183.841648, density=19.30, x0=0.350418, lambda_int=10.311580
),
"G4_Cu": MaterialProperties(
z_eff=29.0, a_eff=63.545648, density=8.96, x0=1.435578, lambda_int=15.587940
),
"G4_Fe": MaterialProperties(
z_eff=26.0, a_eff=55.845113, density=7.874, x0=1.757493, lambda_int=16.990300
),
"G4_BRASS": MaterialProperties(
z_eff=30.939130,
a_eff=68.500857,
density=8.52,
x0=1.367465,
lambda_int=16.947420,
),
"G4_POLYSTYRENE": MaterialProperties(
z_eff=3.5, a_eff=6.509339, density=1.06, x0=41.312510, lambda_int=68.749880
),
"G4_PLASTIC_SC_VINYLTOLUENE": MaterialProperties(
z_eff=3.368421,
a_eff=6.219791,
density=1.032,
x0=42.544200,
lambda_int=69.969390,
),
"G4_BGO": MaterialProperties(
z_eff=27.578947,
a_eff=65.565839,
density=7.13,
x0=1.118030,
lambda_int=22.710130,
),
"G4_LYSO": MaterialProperties(None, None, None, None, None),
"G4_AIR": MaterialProperties(
z_eff=7.261982,
a_eff=14.547593,
density=1.204790e-3,
x0=30392.070000,
lambda_int=71009.500000,
),
"G4_lAr": MaterialProperties(
z_eff=18.0, a_eff=39.947692, density=1.396, x0=14.003440, lambda_int=85.706400
),
}
def get_material_properties(
name: str, table: dict[str, MaterialProperties] | None = None
) -> MaterialProperties:
t = MATERIAL_PROPERTIES if table is None else table
if name not in t:
raise UnknownMaterialError(
f"material {name!r} is not in giant.materials.MATERIAL_PROPERTIES "
f"-- add it (known: {sorted(t)})"
)
props = t[name]
if any(v is None for v in props):
raise MaterialPropertiesNotFilledError(
f"material {name!r} has un-filled physical properties in "
"giant/materials.py -- a physicist must populate real "
"z_eff/a_eff/density/x0/lambda_int values before "
"conditioning='physical' can be used with this material"
)
return props
def material_properties_array(
names: np.ndarray, table: dict[str, MaterialProperties] | None = None
) -> np.ndarray:
"""(N,) str material names -> (N, 5) float32 [z_eff, a_eff, density, x0, lambda_int]."""
out = np.array(
[get_material_properties(str(m), table) for m in np.asarray(names)],
dtype=np.float32,
)
return out.reshape(-1, 5)