Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion celerpy/conf/settings.py
Original file line number Diff line number Diff line change
@@ -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.

Expand All @@ -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"
3 changes: 3 additions & 0 deletions celerpy/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
30 changes: 23 additions & 7 deletions celerpy/process.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion celerpy/settings.py
Original file line number Diff line number Diff line change
@@ -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()
70 changes: 37 additions & 33 deletions test/mock-prefix/bin/celer-geo
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
}
)
26 changes: 25 additions & 1 deletion test/mock-prefix/bin/mockutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
import json
import signal
import sys
from typing import Any

import numpy as np

if not sys.warnoptions:
import warnings
Expand All @@ -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:
Expand All @@ -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)
4 changes: 0 additions & 4 deletions test/test_process.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
7 changes: 6 additions & 1 deletion test/test_visualize.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down