Skip to content

Commit d293dc5

Browse files
authored
feat(optiphy): add Geant4 version detection utility (#283)
Add a small utility module in [optiphy/geant4_version.py](optiphy/geant4_version.py) to detect the installed Geant4 version and normalize it to the release series expected by `optiphy`. ## Changes - read the version from `GEANT4_VERSION` when available, with fallback to `geant4-config --version` - parse `major.minor.patch` version strings - map supported releases to `11.3` and `11.4+` - expose a simple CLI that prints either the raw version or normalized series - raise clear errors when the version cannot be detected or is not yet supported
1 parent 4813ef1 commit d293dc5

3 files changed

Lines changed: 122 additions & 0 deletions

File tree

optiphy/geant4_version.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import argparse
2+
import os
3+
import re
4+
import subprocess
5+
6+
7+
def parse_geant4_version(version):
8+
match = re.fullmatch(r"\s*(\d+)\.(\d+)(?:\.(?:(\d+)|p(\d+))(?:\.beta)?)?\s*", version)
9+
if match is None:
10+
raise RuntimeError(f"Unable to parse Geant4 version: {version!r}")
11+
12+
major, minor, patch, patchlevel = match.groups(default="0")
13+
return int(major), int(minor), int(patch if patch != "0" else patchlevel)
14+
15+
16+
def detect_geant4_version():
17+
version = os.environ.get("GEANT4_VERSION")
18+
if version:
19+
return version.strip()
20+
21+
try:
22+
return subprocess.check_output(["geant4-config", "--version"], text=True).strip()
23+
except (OSError, subprocess.CalledProcessError) as err:
24+
raise RuntimeError(
25+
"Unable to determine Geant4 version. Set GEANT4_VERSION or ensure geant4-config is available."
26+
) from err
27+
28+
29+
def geant4_series(version=None):
30+
resolved = detect_geant4_version() if version is None else version
31+
major, minor, _patch = parse_geant4_version(resolved)
32+
33+
if (major, minor) == (11, 3):
34+
return "11.3"
35+
if (major, minor) >= (11, 4):
36+
return "11.4+"
37+
38+
raise RuntimeError(
39+
f"Unsupported Geant4 version {resolved!r}. Add support for this release family."
40+
)
41+
42+
43+
def main():
44+
parser = argparse.ArgumentParser()
45+
parser.add_argument("field", nargs="?", choices=("version", "series"), default="version")
46+
args = parser.parse_args()
47+
48+
version = detect_geant4_version()
49+
50+
if args.field == "version":
51+
print(version)
52+
else:
53+
print(geant4_series(version))
54+
55+
56+
if __name__ == "__main__":
57+
main()
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import re
2+
3+
import pytest
4+
5+
from optiphy.geant4_version import geant4_series, parse_geant4_version
6+
7+
8+
@pytest.mark.parametrize(
9+
("version", "expected"),
10+
[
11+
("11.3.2", "11.3"),
12+
("11.3.0.beta", "11.3"),
13+
("11.4.0", "11.4+"),
14+
("11.4.p01", "11.4+"),
15+
("11.4.0.beta", "11.4+"),
16+
("11.5.1", "11.4+"),
17+
("12.0.0", "11.4+"),
18+
("12.0.p02", "11.4+"),
19+
("12.0.0.beta", "11.4+"),
20+
("12.1.3", "11.4+"),
21+
("13.0.0", "11.4+"),
22+
],
23+
)
24+
def test_geant4_series_supported_versions(version, expected):
25+
assert geant4_series(version) == expected
26+
27+
28+
@pytest.mark.parametrize("version", ["9.6.1", "9.6.p03", "10.7.4"])
29+
def test_geant4_series_rejects_older_major_versions(version):
30+
with pytest.raises(RuntimeError, match=re.escape(f"Unsupported Geant4 version {version!r}")):
31+
geant4_series(version)
32+
33+
34+
@pytest.mark.parametrize("version", ["11.2.0", "11.2.0.beta"])
35+
def test_geant4_series_rejects_unsupported_11_minor(version):
36+
with pytest.raises(RuntimeError, match=re.escape(f"Unsupported Geant4 version {version!r}")):
37+
geant4_series(version)
38+
39+
40+
def test_geant4_series_rejects_unparseable_version():
41+
version = "not-a-version"
42+
with pytest.raises(RuntimeError, match=re.escape(f"Unable to parse Geant4 version: {version!r}")):
43+
geant4_series(version)
44+
45+
46+
@pytest.mark.parametrize(
47+
("version", "expected"),
48+
[
49+
("11.4", (11, 4, 0)),
50+
("11.4.0", (11, 4, 0)),
51+
("11.4.0.beta", (11, 4, 0)),
52+
("11.4.p01", (11, 4, 1)),
53+
("11.2.0.beta", (11, 2, 0)),
54+
("12.0.p02", (12, 0, 2)),
55+
],
56+
)
57+
def test_parse_geant4_version_accepts_supported_patch_formats(version, expected):
58+
assert parse_geant4_version(version) == expected
59+
60+
61+
@pytest.mark.parametrize("version", ["11.4.p", "11.4-dev", "11.4foo", "11.4.p01foo", "11.4.0.betafoo"])
62+
def test_parse_geant4_version_rejects_trailing_suffixes(version):
63+
with pytest.raises(RuntimeError, match=re.escape(f"Unable to parse Geant4 version: {version!r}")):
64+
parse_geant4_version(version)

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ dependencies = [
1111
]
1212

1313
[project.scripts]
14+
geant4-version = "optiphy.geant4_version:main"
1415
generate-input-photons = "optiphy.tools.generate_input_photons:main"
1516
plot-csg = "optiphy.tools.plot_csg:main"
1617
serve-path = "optiphy.tools.serve_path:main"

0 commit comments

Comments
 (0)