Files
giant/tests/test_materials.py
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

106 lines
3.6 KiB
Python

import numpy as np
import pytest
from giant.materials import (
MaterialProperties,
MaterialPropertiesNotFilledError,
UnknownMaterialError,
get_material_properties,
material_properties_array,
)
def test_get_material_properties_unknown_name_raises():
with pytest.raises(UnknownMaterialError):
get_material_properties("G4_Unobtainium")
def test_get_material_properties_unfilled_entry_raises():
"""G4_LYSO is not a stock Geant4 NIST material (confirmed against the
vendored Geant4 11.4.1 build) and is the one entry still shipped unfilled."""
with pytest.raises(MaterialPropertiesNotFilledError):
get_material_properties("G4_LYSO")
def test_get_material_properties_returns_filled_entry_from_injected_table():
table = {
"G4_Pb": MaterialProperties(
z_eff=82.0, a_eff=207.2, density=11.35, x0=0.5612, lambda_int=17.59
)
}
props = get_material_properties("G4_Pb", table)
assert props.z_eff == 82.0
assert props.a_eff == 207.2
assert props.density == 11.35
assert props.x0 == 0.5612
assert props.lambda_int == 17.59
def test_material_properties_array_shape_and_values():
table = {
"G4_Pb": MaterialProperties(82.0, 207.2, 11.35, 0.5612, 17.59),
"G4_W": MaterialProperties(74.0, 183.84, 19.3, 0.3504, 9.95),
}
names = np.array(["G4_Pb", "G4_W", "G4_Pb"], dtype=object)
arr = material_properties_array(names, table)
assert arr.shape == (3, 5)
assert arr.dtype == np.float32
np.testing.assert_allclose(arr[0], [82.0, 207.2, 11.35, 0.5612, 17.59], rtol=1e-5)
np.testing.assert_allclose(arr[1], [74.0, 183.84, 19.3, 0.3504, 9.95], rtol=1e-5)
def test_all_known_materials_present_in_stub_table():
"""Every material referenced elsewhere in the repo must at least have a
stub entry (even if unfilled) -- an unknown name should never be the
failure mode a physicist hits when populating the table."""
from giant.materials import MATERIAL_PROPERTIES
expected = {
"G4_PbWO4",
"G4_CESIUM_IODIDE",
"G4_Pb",
"G4_W",
"G4_Cu",
"G4_Fe",
"G4_BRASS",
"G4_POLYSTYRENE",
"G4_PLASTIC_SC_VINYLTOLUENE",
"G4_BGO",
"G4_LYSO",
"G4_AIR",
"G4_lAr",
}
assert expected <= set(MATERIAL_PROPERTIES.keys())
def test_all_materials_filled_except_lyso():
"""G4_LYSO is the sole intentionally-unfilled entry (not a stock Geant4
NIST material); every other known material has real Geant4-derived
values -- see the module docstring for provenance."""
from giant.materials import MATERIAL_PROPERTIES
for name, props in MATERIAL_PROPERTIES.items():
if name == "G4_LYSO":
assert all(v is None for v in props)
else:
assert all(v is not None for v in props), f"{name} unexpectedly unfilled"
def test_elemental_material_z_eff_matches_atomic_number():
"""Single-element materials' z_eff must equal the element's real Z."""
pb = get_material_properties("G4_Pb")
assert pb.z_eff == pytest.approx(82.0)
w = get_material_properties("G4_W")
assert w.z_eff == pytest.approx(74.0)
fe = get_material_properties("G4_Fe")
assert fe.z_eff == pytest.approx(26.0)
def test_pbwo4_values_match_known_cms_ecal_reference():
"""PbWO4 (CMS ECAL crystal) has well-known reference values: X0~0.89cm,
density 8.28 g/cm^3 -- sanity check the Geant4-derived numbers land there."""
pbwo4 = get_material_properties("G4_PbWO4")
assert pbwo4.density == pytest.approx(8.28)
assert pbwo4.x0 == pytest.approx(0.89, abs=0.01)
assert pbwo4.z_eff == pytest.approx(31.33, abs=0.01)