68fb99bed8
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>
106 lines
3.6 KiB
Python
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)
|