diff --git a/celerpy/conf/settings.py b/celerpy/conf/settings.py index 4b1804c..b3ac935 100644 --- a/celerpy/conf/settings.py +++ b/celerpy/conf/settings.py @@ -1,12 +1,23 @@ # Copyright 2024 UT-Battelle, LLC, and other Celeritas developers. # See the top-level LICENSE file for details. # SPDX-License-Identifier: Apache-2.0 +from enum import StrEnum from typing import Optional from pydantic import DirectoryPath from pydantic_settings import BaseSettings, SettingsConfigDict +class LogLevel(StrEnum): + """Minimum verbosity level for logging.""" + + debug = "debug" + info = "info" + warning = "warning" + error = "error" + critical = "critical" + + class Settings(BaseSettings): """Global settings for Celeritas front end. @@ -25,7 +36,23 @@ class Settings(BaseSettings): color: bool = True "Enable colorized terminal output" - # TODO: log, log_local, disable_device + disable_device: bool = False + "Disable GPU execution even if available" + + g4org_export: Optional[str] = None + "Filename base to export converted Geant4 geometry" + + g4org_verbose: bool = False + "Filename base to export converted Geant4 geometry" + + log: LogLevel = LogLevel.info + "World log level" + + log_local: LogLevel = LogLevel.warning + "Self log level" prefix_path: Optional[DirectoryPath] = None "Path to the Celeritas build/install directory" + + profiling: bool = False + "Enable NVTX/ROCTX/Perfetto profiling" diff --git a/celerpy/model.py b/celerpy/model.py index daece34..1363638 100644 --- a/celerpy/model.py +++ b/celerpy/model.py @@ -62,6 +62,9 @@ class ModelSetup(_Model): geometry_file: FilePath "Path to the GDML input file" + perfetto_file: Optional[FilePath] = None + "Path to write Perfetto profiling output" + # celer-geo/GeoInput.hh class TraceSetup(_Model): diff --git a/celerpy/process.py b/celerpy/process.py index 0cf8bea..f542eb0 100644 --- a/celerpy/process.py +++ b/celerpy/process.py @@ -17,19 +17,35 @@ M = TypeVar("M", bound=BaseModel) P = TypeVar("P", bound=Popen) +_settings_env = { + "profiling": "CELER_ENABLE_PROFILING", +} + +for _attr, _ in settings: + if _attr.startswith("g4org"): + _settings_env[_attr] = _attr.upper() + + +def settings_to_env() -> dict[str, str]: + """Convert settings to environment variables.""" + env = {} + for attr, value in settings: + if value is None: + continue + try: + key = _settings_env[attr] + except KeyError: + key = "CELER_" + attr.upper() + env[key] = str(value) + return env + def launch(executable: str, *, env=None, **kwargs) -> Popen: """Set up and launch a Celeritas process with stdin/stdout pipes.""" # Set up environment variables if env is None: env = os.environ.copy() - for attr in ["color"]: - key = "CELER_" + attr.upper() - value = getattr(settings, attr) - if isinstance(value, bool): - # Set to 1 or blank - value = "1" if value else "" - env[key] = value + env.update(settings_to_env()) # Create child process, which implicitly keeps a copy of the file # descriptors diff --git a/celerpy/settings.py b/celerpy/settings.py index 7f2b7d2..406677b 100644 --- a/celerpy/settings.py +++ b/celerpy/settings.py @@ -1,6 +1,6 @@ # Copyright 2024 UT-Battelle, LLC, and other Celeritas developers. # See the top-level LICENSE file for details. # SPDX-License-Identifier: Apache-2.0 -from .conf.settings import Settings +from .conf.settings import LogLevel, Settings # noqa: F401 settings = Settings() diff --git a/test/mock-prefix/bin/celer-geo b/test/mock-prefix/bin/celer-geo index ea9d461..d0f35d4 100755 --- a/test/mock-prefix/bin/celer-geo +++ b/test/mock-prefix/bin/celer-geo @@ -2,51 +2,55 @@ # Copyright 2024 UT-Battelle, LLC, and other Celeritas developers. # See the top-level COPYRIGHT file for details. # SPDX-License-Identifier: (Apache-2.0 OR MIT) -"""Mock the celer-geo process. -""" -from mockutils import log, dump, read_input, setup_signals -import numpy as np -import json -import sys +"""Mock the celer-geo process.""" + +from os import environ + +from mockutils import dump, expect_trace, log, read_input, setup_signals setup_signals() -def expect_trace(expected_inp, expected_outp): - expected_inp = json.loads(expected_inp) - expected_outp = json.loads(expected_outp) - log("expecting input...") - inp = read_input() - if inp is None: - log("exiting early due to missing input") - sys.exit(1) - log("...read input") - bin_file = inp.pop('bin_file') - if inp != expected_inp: - raise RuntimeError("Unexpected output: got {!r}".format(json.dumps(inp))) - - with open(bin_file, 'wb') as f: - f.write(np.zeros([4, 4], dtype=np.int32).tobytes()) - - log("writing output...") - expected_outp['trace']['bin_file'] = bin_file - dump(expected_outp) +# Check environment +celer_log = environ.get("CELER_LOG") +assert celer_log == "debug", f"Expected CELER_LOG=debug, got {celer_log}" +celer_log_local = environ.get("CELER_LOG_LOCAL") +assert celer_log_local == "warning", f"Expected CELER_LOG_LOCAL=warning, got {celer_log_local}" +g4org_verbose = environ.get("G4ORG_VERBOSE") +assert g4org_verbose == "True", f"Expected G4ORG_VERBOSE=1, got {g4org_verbose}" + +# Read the initial command and echo it (with version) cmd = read_input() log("read model", repr(cmd)) -assert cmd['geometry_file'] +assert cmd["geometry_file"] +cmd["version"] = "0.7.0-dev" +cmd["version_hex"] = 0x000700 dump(cmd) log("entering loop") expect_trace( -'{"geometry":"orange","memspace":null,"volumes":true,"image":{"lower_left":[0,0,0],"upper_right":[1,1,0],"rightward":[1,0,0],"vertical_pixels":4,"horizontal_divisor":null}}' -,'{"image":{"_units":"cgs","dims":[4,4],"down":[0.0,-1.0,0.0],"origin":[0.0,1.0,0.0],"pixel_width":0.25,"right":[1.0,0.0,0.0]},"sizeof_int":4,"trace":{"geometry":"orange","memspace":"host","volumes":true},"volumes":["[EXTERIOR]","inner","world"]}\n' ) + '{"geometry":"orange","memspace":null,"volumes":true,"image":{"lower_left":[0,0,0],"upper_right":[1,1,0],"rightward":[1,0,0],"vertical_pixels":4,"horizontal_divisor":null}}', + '{"image":{"_units":"cgs","dims":[4,4],"down":[0.0,-1.0,0.0],"origin":[0.0,1.0,0.0],"pixel_width":0.25,"right":[1.0,0.0,0.0]},"sizeof_int":4,"trace":{"geometry":"orange","memspace":"host","volumes":true},"volumes":["[EXTERIOR]","inner","world"]}\n', +) expect_trace( -'{"geometry": "orange", "memspace": null, "volumes": false, "image": null}' -,'{"image":{"_units":"cgs","dims":[4,4],"down":[0.0,-1.0,0.0],"origin":[0.0,1.0,0.0],"pixel_width":0.25,"right":[1.0,0.0,0.0]},"sizeof_int":4,"trace":{"geometry":"orange","memspace":"host","volumes":false}}\n' ) + '{"geometry": "orange", "memspace": null, "volumes": false, "image": null}', + '{"image":{"_units":"cgs","dims":[4,4],"down":[0.0,-1.0,0.0],"origin":[0.0,1.0,0.0],"pixel_width":0.25,"right":[1.0,0.0,0.0]},"sizeof_int":4,"trace":{"geometry":"orange","memspace":"host","volumes":false}}\n', +) expect_trace( -'{"geometry":"geant4","memspace":null,"volumes":true,"image":null}' -,'{"image":{"_units":"cgs","dims":[4,4],"down":[0.0,-1.0,0.0],"origin":[0.0,1.0,0.0],"pixel_width":0.25,"right":[1.0,0.0,0.0]},"sizeof_int":4,"trace":{"geometry":"geant4","memspace":"host","volumes":true},"volumes":["inner","world"]}\n' ) + '{"geometry":"geant4","memspace":null,"volumes":true,"image":null}', + '{"image":{"_units":"cgs","dims":[4,4],"down":[0.0,-1.0,0.0],"origin":[0.0,1.0,0.0],"pixel_width":0.25,"right":[1.0,0.0,0.0]},"sizeof_int":4,"trace":{"geometry":"geant4","memspace":"host","volumes":true},"volumes":["inner","world"]}\n', +) cmd = read_input() assert cmd == None -dump({"runtime":{"device":None,"kernels":[],"version":"0.5.0-dev"},"timers":{"load_geant4":0.1,"load_orange":0.2,"raytrace_geant4_host":0.3,"raytrace_orange_host":0.4}}) +dump( + { + "runtime": {"device": None, "kernels": [], "version": "0.7.0-dev"}, + "timers": { + "load_geant4": 0.1, + "load_orange": 0.2, + "raytrace_geant4_host": 0.3, + "raytrace_orange_host": 0.4, + }, + } +) diff --git a/test/mock-prefix/bin/mockutils.py b/test/mock-prefix/bin/mockutils.py index 55c48b1..166d7db 100644 --- a/test/mock-prefix/bin/mockutils.py +++ b/test/mock-prefix/bin/mockutils.py @@ -5,6 +5,9 @@ import json import signal import sys +from typing import Any + +import numpy as np if not sys.warnoptions: import warnings @@ -31,7 +34,7 @@ def terminate(signum, frame): sys.exit(signum) -def read_input(): +def read_input() -> Any: try: return json.loads(input()) except EOFError: @@ -42,3 +45,24 @@ def read_input(): def setup_signals(): signal.signal(signal.SIGINT, terminate) signal.signal(signal.SIGTERM, terminate) + + +def expect_trace(expected_inp, expected_outp): + expected_inp = json.loads(expected_inp) + expected_outp = json.loads(expected_outp) + log("expecting input...") + inp = read_input() + if inp is None: + log("exiting early due to missing input") + sys.exit(1) + log("...read input") + bin_file = inp.pop("bin_file") + if inp != expected_inp: + raise RuntimeError(f"Unexpected output: got {json.dumps(inp)!r}") + + with open(bin_file, "wb") as f: + f.write(np.zeros([4, 4], dtype=np.int32).tobytes()) + + log("writing output...") + expected_outp["trace"]["bin_file"] = bin_file + dump(expected_outp) diff --git a/test/test_process.py b/test/test_process.py index 46b6afa..bbf3e6a 100644 --- a/test/test_process.py +++ b/test/test_process.py @@ -4,12 +4,8 @@ import json import signal -from pathlib import Path from celerpy.process import close, communicate, launch -from celerpy.settings import settings - -settings.prefix_path = Path(__file__).parent / "mock-prefix" def communicate_json(process, inp): diff --git a/test/test_visualize.py b/test/test_visualize.py index 64fcf5b..de670e3 100644 --- a/test/test_visualize.py +++ b/test/test_visualize.py @@ -9,11 +9,16 @@ from numpy.testing import assert_array_equal from celerpy import model, visualize -from celerpy.settings import settings +from celerpy.settings import LogLevel, settings local_path = Path(__file__).parent settings.prefix_path = local_path / "mock-prefix" +settings.prefix_path = Path(__file__).parent / "mock-prefix" +settings.log = LogLevel.debug +settings.log_local = LogLevel.warning +settings.g4org_verbose = True + def test_CelerGeo(): inp = local_path / "data" / "two-boxes.gdml"