File indexing completed on 2026-10-16 08:49:32
0001 """Versioned material files and the explicit legacy compatibility bindings."""
0002
0003 import json
0004 import shutil
0005 import subprocess
0006 from pathlib import Path
0007
0008 import pytest
0009 import acts
0010
0011 acts_json = pytest.importorskip("acts.json")
0012 TrackingGeometryMaterialJsonConverter = acts_json.TrackingGeometryMaterialJsonConverter
0013 MaterialMapDecorator = acts_json.MaterialMapDecorator
0014
0015 FIXTURES = Path(__file__).resolve().parents[3] / "docs/examples"
0016
0017
0018 def test_material_document(tmp_path):
0019 converter = TrackingGeometryMaterialJsonConverter()
0020 material = converter.fromFile(FIXTURES / "material-map-v1/minimal.json")
0021 material.description = "Python round trip"
0022 options = TrackingGeometryMaterialJsonConverter.Options()
0023 options.materialFractionBits = 16
0024 for suffix in (".json", ".cbor"):
0025 path = tmp_path / ("material" + suffix)
0026 converter.toFile(material, path, options)
0027 assert converter.fromFile(path).description == "Python round trip"
0028 assert isinstance(acts.IMaterialDecorator.fromFile(path), MaterialMapDecorator)
0029 encoded = json.loads((tmp_path / "material.json").read_text())
0030 assert encoded["header"]["version"] == 1
0031 assert encoded["header"]["description"] == "Python round trip"
0032
0033
0034 def test_legacy_material_error():
0035 path = FIXTURES / "material_map_example.json"
0036 with pytest.raises(
0037 ValueError, match="Legacy material format.*ActsMaterialMapMigrate"
0038 ):
0039 TrackingGeometryMaterialJsonConverter().fromFile(path)
0040
0041
0042 @pytest.fixture
0043 def material_migrator():
0044 executable = shutil.which("ActsMaterialMapMigrate")
0045 if executable is None:
0046 candidate = (
0047 Path(acts.__file__).absolute().parents[2] / "bin/ActsMaterialMapMigrate"
0048 )
0049 if not candidate.is_file():
0050 pytest.skip("ActsMaterialMapMigrate is not built")
0051 executable = str(candidate)
0052
0053 return executable
0054
0055
0056 def test_material_map_migration(tmp_path, material_migrator):
0057 executable = material_migrator
0058
0059 def run(*args):
0060 return subprocess.run(
0061 [executable, *map(str, args)], capture_output=True, text=True
0062 )
0063
0064 original = FIXTURES / "material_map_example.json"
0065 output = tmp_path / "new.json"
0066 rejected = run(original, output)
0067 assert rejected.returncode != 0
0068 assert "surface material only" in rejected.stderr
0069 assert not output.exists()
0070 legacy = json.loads(original.read_text())
0071 legacy["Volumes"]["entries"] = []
0072 legacy["KeyedSurfaces"] = [
0073 {
0074 "key": "migration-test",
0075 "geometry_id": 0,
0076 "material": legacy["Surfaces"]["entries"][0]["value"]["material"],
0077 }
0078 ]
0079 source = tmp_path / "legacy.json"
0080 source.write_text(json.dumps(legacy))
0081 migrated = run(source, output)
0082 assert migrated.returncode == 0, migrated.stderr
0083 material = TrackingGeometryMaterialJsonConverter().fromFile(output)
0084 assert "migration-test" in material.keyedSurfaces
0085 assert len(material.surfaceMaterials) == len(legacy["Surfaces"]["entries"])
0086 quantized = tmp_path / "quantized.json"
0087 result = run(
0088 source,
0089 quantized,
0090 "--material-fraction-bits",
0091 16,
0092 "--indentation",
0093 0,
0094 "--compression-level",
0095 19,
0096 )
0097 assert result.returncode == 0, result.stderr
0098 TrackingGeometryMaterialJsonConverter().fromFile(quantized)
0099 for args in [
0100 (output, tmp_path / "invalid.json"),
0101 (source, source),
0102 (source, output, "--material-fraction-bits", "24"),
0103 (source, output, "--indentation"),
0104 ]:
0105 assert run(*args).returncode != 0
0106
0107
0108 def test_root_material_map_migration(tmp_path, material_migrator):
0109 import numpy as np
0110 import uproot
0111
0112 source = tmp_path / "material.root"
0113 output = tmp_path / "material.json"
0114 geometry_id = (1 << 56) | (256 << 8)
0115 data = {
0116 "hGeoId": np.array([geometry_id], dtype=np.int64),
0117 **{
0118 name: np.array([value], dtype=np.float32)
0119 for name, value in {
0120 "t": 0.25,
0121 "x0": 93.7,
0122 "l0": 465.2,
0123 "A": 28.0855,
0124 "Z": 14.0,
0125 "rho": 0.00233,
0126 }.items()
0127 },
0128 }
0129 with uproot.recreate(source) as root:
0130 tree = root.mktree(
0131 "HomogeneousMaterial", {name: values.dtype for name, values in data.items()}
0132 )
0133 tree.extend(data)
0134
0135 def run(path, target):
0136 return subprocess.run(
0137 [material_migrator, str(path), str(target)], capture_output=True, text=True
0138 )
0139
0140 result = run(source, output)
0141 if "ROOT input is unavailable" in result.stderr:
0142 assert result.returncode != 0
0143 assert "ACTS_BUILD_PLUGIN_ROOT=ON" in result.stderr
0144 assert not output.exists()
0145 return
0146 assert result.returncode == 0, result.stderr
0147 converter = TrackingGeometryMaterialJsonConverter()
0148 assert len(converter.fromFile(output).surfaceMaterials) == 1
0149 document = json.loads(output.read_text())
0150 entry = document["surfaces"][0]
0151 assert entry["target"]["geometry_id"] == {"volume": 1, "sensitive": 256}
0152 assert entry["material"]["slab"]["thickness"] == pytest.approx(0.25)
0153 assert entry["material"]["slab"]["material"]["radiation_length"] == pytest.approx(
0154 93.7
0155 )
0156 cbor = tmp_path / "material.cbor"
0157 assert run(source, cbor).returncode == 0
0158 assert len(converter.fromFile(cbor).surfaceMaterials) == 1
0159
0160 with uproot.update(source) as root:
0161 root.mkdir("VolumeMaterial_vol1")
0162 rejected = run(source, tmp_path / "volume.json")
0163 assert rejected.returncode != 0
0164 assert "surface material only" in rejected.stderr
0165 assert not (tmp_path / "volume.json").exists()
0166
0167 empty = tmp_path / "empty.root"
0168 with uproot.recreate(empty):
0169 pass
0170 rejected = run(empty, tmp_path / "empty.json")
0171 assert rejected.returncode != 0
0172 assert "No material maps" in rejected.stderr