diff --git a/README.md b/README.md index b70023b..6d51dae 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ > **Warning** This repository is experimental and bare-bones at the moment. It's not ready -for use, but discussions and contributions are welcome! +for general use, but discussions and contributions are welcome! # Celeritas Python interface diff --git a/celerpy/conf/settings.py b/celerpy/conf/settings.py index 6f18abd..a98d517 100644 --- a/celerpy/conf/settings.py +++ b/celerpy/conf/settings.py @@ -3,7 +3,7 @@ # SPDX-License-Identifier: Apache-2.0 from typing import Optional -from pydantic import DirectoryPath +from pydantic import DirectoryPath, FilePath from pydantic_settings import BaseSettings, SettingsConfigDict from celerpy.model import LogLevel @@ -24,26 +24,32 @@ class Settings(BaseSettings): use_attribute_docstrings=True, ) + prefix_path: Optional[DirectoryPath] = None + "Path to the Celeritas build/install directory" + + # CELER_ environment variables + color: bool = True "Enable colorized terminal output" 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" + + # Geant4->ORANGE conversion + + g4org_options: Optional[FilePath] = None + "JSON file with conversion options" + + # Geant4 configuration + + g4_geo_optimize: bool = True + "Build Geant4 tracking acceleration structures" diff --git a/celerpy/model.py b/celerpy/model.py deleted file mode 100644 index d60594b..0000000 --- a/celerpy/model.py +++ /dev/null @@ -1,174 +0,0 @@ -# Copyright 2024 UT-Battelle, LLC, and other Celeritas developers. -# See the top-level LICENSE file for details. -# SPDX-License-Identifier: Apache-2.0 -"""Manage models used for JSON I/O with Celeritas.""" - -from enum import StrEnum, auto -from typing import Annotated, Literal, Optional - -from pydantic import ( - BaseModel, - ConfigDict, - Field, - FilePath, - NonNegativeInt, - PositiveFloat, - PositiveInt, - conlist, -) - -Real3 = Annotated[list, conlist(float, min_length=3, max_length=3)] -Size2 = Annotated[list, conlist(PositiveInt, min_length=2, max_length=2)] - - -class _Model(BaseModel): - """Base settings for Celeritas models. - - Note that attribute docstrings require Pydantic 2.7 or higher. - """ - - model_config = ConfigDict(use_attribute_docstrings=True) - - -# corecel/Types.hh -class MemSpace(StrEnum): - """Memory/execution space.""" - - HOST = auto() # CPU - DEVICE = auto() # GPU - - # Backward compatibility - host = HOST - device = DEVICE - - -# corecel/Types.hh -class UnitSystem(StrEnum): - CGS = auto() - SI = auto() - CLHEP = auto() - - # Backward compatibility - cgs = CGS - si = SI - clhep = CLHEP - - -# corecel/io/LoggerTypes.hh -class LogLevel(StrEnum): - """Minimum verbosity level for logging.""" - - DEBUG = auto() - INFO = auto() - WARNING = auto() - ERROR = auto() - CRITICAL = auto() - - -# celer-geo/Types.hh -class GeometryEngine(StrEnum): - """Geometry model implementation for execution and rendering.""" - - GEANT4 = auto() - VECGEOM = auto() - ORANGE = auto() - - # Backward compatibility - geant4 = GEANT4 - vecgeom = VECGEOM - orange = ORANGE - - -# celer-geo/GeoInput.hh -class ModelSetup(_Model): - cuda_stack_size: Optional[NonNegativeInt] = None - cuda_heap_size: Optional[NonNegativeInt] = None - - 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): - geometry: Optional[GeometryEngine] = None - "Geometry engine with which to perform the trace" - - memspace: Optional[MemSpace] = None - "Whether to perform the trace on CPU or GPU" - - volumes: bool = True - "Print a list of all volumes in the geometry" - - bin_file: FilePath - "Specify the path to write the image binary data" - - -# geocel/rasterize/Image.hh -class ImageInput(_Model): - lower_left: Real3 = [0, 0, 0] - """Spatial coordinate of the image's lower left point""" - - upper_right: Real3 - """Spatial coordinate of the images' upper right point""" - - rightward: Real3 = [1, 0, 0] - "Ray trace direction which points to the right in the image" - - vertical_pixels: NonNegativeInt = 512 - "Number of pixels along the y axis" - - horizontal_divisor: Optional[PositiveInt] = None - "Increase the horizontal window to be divisible by this number" - - -# geocel/rasterize/ImageData.hh: ImageParamsScalars -class ImageParams(_Model): - origin: Real3 - "Upper left point of the image" - - down: Real3 - "Direction vector rendered as 'downward' in the image" - - right: Real3 - "Direction vector rendered as 'rightward' in the image" - - pixel_width: PositiveFloat - "Size of a pixel in the image" - - dims: Size2 - "Size of a pixel in the generated image" - - units: UnitSystem = Field(alias="_units") - - # TODO: max length is not used or returned by celer-geo - # max_length: float - - -# ad hoc: input to a 'trace' command -class TraceInput(TraceSetup): - image: Optional[ImageInput] = None - "Reuse the existing image" - - -# ad hoc: result from a 'trace' command -class TraceOutput(_Model): - trace: TraceSetup - image: ImageParams - volumes: Optional[list[str]] = None - sizeof_int: PositiveInt - - -class ExceptionDump(_Model): - """Output of an exception message when a Celeritas app fails""" - - _category: Literal["result"] - _label: Literal["exception"] - type: str - condition: Optional[str] = None - file: Optional[str] = None - line: Optional[int] = None - which: Optional[str] = None - context: Optional["ExceptionDump"] = None diff --git a/celerpy/model/__init__.py b/celerpy/model/__init__.py new file mode 100644 index 0000000..4d461b4 --- /dev/null +++ b/celerpy/model/__init__.py @@ -0,0 +1,41 @@ +# Copyright 2024 UT-Battelle, LLC, and other Celeritas developers. +# See the top-level LICENSE file for details. +# SPDX-License-Identifier: Apache-2.0 +"""Manage models used for JSON I/O with Celeritas. + +This module re-exports all models from the individual modules for backward +compatibility. For new code, prefer importing from the specific modules: +- celerpy.types: Base types, enums, and type aliases +- celerpy.input: Command and input models +- celerpy.output: Output and result models +""" + +from . import input, output, types +from .input import ( + ImageInput, + ModelSetup, + OrangeConversionOptions, + TraceInput, + TraceSetup, +) +from .types import ( + GeometryEngine, + LogLevel, + MemSpace, + UnitSystem, +) + +__all__ = [ + "input", + "output", + "types", + "ImageInput", + "ModelSetup", + "OrangeConversionOptions", + "TraceInput", + "TraceSetup", + "GeometryEngine", + "LogLevel", + "MemSpace", + "UnitSystem", +] diff --git a/celerpy/model/input.py b/celerpy/model/input.py new file mode 100644 index 0000000..0ae94fe --- /dev/null +++ b/celerpy/model/input.py @@ -0,0 +1,150 @@ +# Copyright 2024 UT-Battelle, LLC, and other Celeritas developers. +# See the top-level LICENSE file for details. +# SPDX-License-Identifier: Apache-2.0 +"""Input models for commands and configuration with Celeritas.""" + +from enum import StrEnum, auto +from typing import Literal, Optional + +from pydantic import FilePath, NonNegativeInt, PositiveFloat, PositiveInt +from pydantic_core import to_json + +from .types import ( + GeometryEngine, + MemSpace, + Real3, + Tolerance, + _Model, +) + + +# orange/g4org/Options.hh +class InlineSingletons(StrEnum): + """How to inline volumes that are used only once.""" + + NONE = auto() # Never + UNTRANSFORMED = auto() # Only if not translated nor rotated + UNROTATED = auto() # Only if translated + ALL = auto() # Always + + +# orange/g4org/Options.hh +class OrangeConversionOptions(_Model): + """Construction options for Geant4-to-ORANGE conversion. + + Note that most of these should never be touched when running an actual + problem. If the length unit is changed, the resulting geometry is + inconsistent with Geant4's scale. + """ + + # Problem scale and tolerance + + unit_length: PositiveFloat = 1.0 + "Scale factor (input unit length), customizable for unit testing" + + tol: Optional[Tolerance] = None + "Construction and tracking tolerance (native units)" + + # Structural conversion + + explicit_interior_threshold: NonNegativeInt = 2 + "Volumes with up to this many children construct an explicit interior" + + inline_childless: bool = True + "Forcibly inline volumes that have no children" + + inline_singletons: InlineSingletons = InlineSingletons.UNTRANSFORMED + "Forcibly inline volumes that are only used once" + + inline_unions: bool = True + "Forcibly copy child volumes that have union boundaries" + + remove_interior: bool = True + "Replace 'interior' unit boundaries with 'true' and simplify" + + remove_negated_join: bool = False + "Use DeMorgan's law to replace 'not all of' with 'any of not'" + + # Debug output + + verbose_volumes: bool = False + "Write output about volumes being converted" + + verbose_structure: bool = False + "Write output about proto-universes being constructed" + + objects_output_file: Optional[str] = None + "Write converted Geant4 object structure to a JSON file" + + csg_output_file: Optional[str] = None + "Write constructed CSG surfaces and tree to a JSON file" + + org_output_file: Optional[str] = None + "Write final org.json to a JSON file" + + +# celer-geo/GeoInput.hh +class ModelSetup(_Model): + cuda_stack_size: Optional[NonNegativeInt] = None + cuda_heap_size: Optional[NonNegativeInt] = None + + 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): + _cmd: Literal["trace"] = "trace" + "Command name in the JSON file" + + geometry: Optional[GeometryEngine] = None + "Geometry engine with which to perform the trace" + + memspace: Optional[MemSpace] = None + "Whether to perform the trace on CPU or GPU" + + volumes: bool = True + "Print a list of all volumes in the geometry" + + bin_file: FilePath + "Specify the path to write the image binary data" + + +# geocel/rasterize/Image.hh +class ImageInput(_Model): + lower_left: Real3 = [0, 0, 0] + """Spatial coordinate of the image's lower left point""" + + upper_right: Real3 + """Spatial coordinate of the images' upper right point""" + + rightward: Real3 = [1, 0, 0] + "Ray trace direction which points to the right in the image" + + vertical_pixels: NonNegativeInt = 512 + "Number of pixels along the y axis" + + horizontal_divisor: Optional[PositiveInt] = None + "Increase the horizontal window to be divisible by this number" + + +# ad hoc: input to a 'trace' command +class TraceInput(TraceSetup): + image: Optional[ImageInput] = None + "Reuse the existing image" + + +# celer-geo/celer-geo.cc +class OrangeStats(_Model): + def model_dump_json(self, **kwargs): + """Override to ensure _cmd is always set to orange_stats.""" + result = self.model_dump(**kwargs) + result["_cmd"] = "orange_stats" + return to_json(result).decode() + + +# Union of available commands +Command = TraceInput | OrangeStats diff --git a/celerpy/model/output.py b/celerpy/model/output.py new file mode 100644 index 0000000..a95dbff --- /dev/null +++ b/celerpy/model/output.py @@ -0,0 +1,136 @@ +# Copyright 2024 UT-Battelle, LLC, and other Celeritas developers. +# See the top-level LICENSE file for details. +# SPDX-License-Identifier: Apache-2.0 +"""Output models for results from Celeritas.""" + +from typing import Literal, Optional + +from pydantic import Field, NonNegativeInt, PositiveFloat, PositiveInt + +from .input import TraceSetup +from .types import Real3, Size2, Tolerance, UnitSystem, _Model + + +# geocel/rasterize/ImageData.hh: ImageParamsScalars +class ImageParams(_Model): + origin: Real3 + "Upper left point of the image" + + down: Real3 + "Direction vector rendered as 'downward' in the image" + + right: Real3 + "Direction vector rendered as 'rightward' in the image" + + pixel_width: PositiveFloat + "Physical size of a pixel in the image" + + dims: Size2 + "Image dimensions (width, height) in pixels" + + units: UnitSystem = Field(alias="_units") + + # TODO: max length is not used or returned by celer-geo + # max_length: float + + +# ad hoc: result from a 'trace' command +class TraceOutput(_Model): + trace: TraceSetup + image: ImageParams + volumes: Optional[list[str]] = None + sizeof_int: PositiveInt + + +# orange/OrangeData.hh +class OrangeScalars(_Model): + """Scalar properties of an ORANGE geometry.""" + + max_depth: NonNegativeInt + "Maximum universe nesting depth" + + max_faces: NonNegativeInt + "Maximum number of faces intersecting a surface" + + max_intersections: NonNegativeInt + "Maximum number of surface intersections along a ray" + + max_logic_depth: NonNegativeInt + "Maximum CSG logic tree depth" + + tol: Tolerance + "Construction and tracking tolerance" + + +# orange/OrangeParamsOutput.hh +class BihSizes(_Model): + """Bounding Interval Hierarchy tree sizes.""" + + bboxes: NonNegativeInt + inner_nodes: NonNegativeInt + leaf_nodes: NonNegativeInt + local_volume_ids: NonNegativeInt + + +# orange/OrangeParamsOutput.hh +class UniverseIndexerSizes(_Model): + """Universe indexer sizes.""" + + surfaces: NonNegativeInt + volumes: NonNegativeInt + + +# orange/OrangeParamsOutput.hh +class OrangeSizes(_Model): + """Size properties of an ORANGE geometry.""" + + connectivity_records: NonNegativeInt + daughters: NonNegativeInt + fast_real3s: NonNegativeInt + local_surface_ids: NonNegativeInt + local_volume_ids: NonNegativeInt + logic_ints: NonNegativeInt + obz_records: NonNegativeInt + real_ids: NonNegativeInt + reals: NonNegativeInt + rect_arrays: NonNegativeInt + simple_units: NonNegativeInt + surface_types: NonNegativeInt + transforms: NonNegativeInt + universe_indices: NonNegativeInt + universe_types: NonNegativeInt + volume_ids: NonNegativeInt + volume_instance_ids: NonNegativeInt + volume_records: NonNegativeInt + + bih: BihSizes + universe_indexer: UniverseIndexerSizes + + +# orange/OrangeParamsOutput.hh +class OrangeParamsOutput(_Model): + """ORANGE geometry data structure sizes and scalars.""" + + _category: Literal["internal"] + _label: Literal["orange"] + scalars: OrangeScalars + sizes: OrangeSizes + + +class ExceptionDump(_Model): + """Output of an exception message when a Celeritas app fails""" + + # Output wrapper + _category: Literal["result"] + _label: Literal["exception"] + + # corecel/io/ExceptionOutput.cc + type: str + context: Optional["ExceptionDump"] = None + + # corecel/AssertIO.json.cc + what: Optional[str] + which: str + condition: Optional[str] = None + file: Optional[str] = None + line: Optional[int] = None diff --git a/celerpy/model/types.py b/celerpy/model/types.py new file mode 100644 index 0000000..cf4aeed --- /dev/null +++ b/celerpy/model/types.py @@ -0,0 +1,86 @@ +# Copyright 2024 UT-Battelle, LLC, and other Celeritas developers. +# See the top-level LICENSE file for details. +# SPDX-License-Identifier: Apache-2.0 +"""Base types and enums for Celeritas models.""" + +from enum import StrEnum, auto +from typing import Annotated + +from pydantic import ( + BaseModel, + ConfigDict, + Field, + conlist, +) + +Real3 = Annotated[list, conlist(float, min_length=3, max_length=3)] +Size2 = Annotated[list, conlist(int, min_length=2, max_length=2)] + + +class _Model(BaseModel): + """Base settings for Celeritas models. + + Note that attribute docstrings require Pydantic 2.7 or higher. + """ + + model_config = ConfigDict(use_attribute_docstrings=True) + + +# orange/OrangeTypes.hh +class Tolerance(_Model): + """Relative and absolute tolerance for construction and tracking.""" + + rel: Annotated[float, Field(gt=0, lt=1)] + "Relative tolerance: must be in (0, 1)" + + abs: Annotated[float, Field(gt=0)] + "Absolute tolerance: must be greater than zero" + + +# corecel/Types.hh +class MemSpace(StrEnum): + """Memory/execution space.""" + + HOST = auto() # CPU + DEVICE = auto() # GPU + + # Backward compatibility + host = HOST + device = DEVICE + + +# corecel/Types.hh +class UnitSystem(StrEnum): + CGS = auto() + SI = auto() + CLHEP = auto() + + # Backward compatibility + cgs = CGS + si = SI + clhep = CLHEP + + +# corecel/io/LoggerTypes.hh +class LogLevel(StrEnum): + """Minimum verbosity level for logging.""" + + DEBUG = auto() + INFO = auto() + WARNING = auto() + ERROR = auto() + CRITICAL = auto() + + +# celer-geo/Types.hh +class GeometryEngine(StrEnum): + """Geometry model implementation for execution and rendering.""" + + GEANT4 = auto() + VECGEOM = auto() + ORANGE = auto() + + # Backward compatibility + geant4 = GEANT4 + vecgeom = VECGEOM + orange = ORANGE diff --git a/celerpy/process.py b/celerpy/process.py index f542eb0..eb5c819 100644 --- a/celerpy/process.py +++ b/celerpy/process.py @@ -11,8 +11,8 @@ from pydantic import BaseModel, ValidationError -from .model import ExceptionDump -from .settings import settings +from . import settings +from .model.output import ExceptionDump M = TypeVar("M", bound=BaseModel) P = TypeVar("P", bound=Popen) @@ -21,15 +21,15 @@ "profiling": "CELER_ENABLE_PROFILING", } -for _attr, _ in settings: - if _attr.startswith("g4org"): +for _attr, _ in settings.settings: + if _attr.startswith("g4"): _settings_env[_attr] = _attr.upper() def settings_to_env() -> dict[str, str]: """Convert settings to environment variables.""" env = {} - for attr, value in settings: + for attr, value in settings.settings: if value is None: continue try: @@ -49,10 +49,11 @@ def launch(executable: str, *, env=None, **kwargs) -> Popen: # Create child process, which implicitly keeps a copy of the file # descriptors - if settings.prefix_path is None: + prefix_path = settings.settings.prefix_path + if prefix_path is None: raise RuntimeError("Celeritas prefix path is not set") return Popen( - [settings.prefix_path / "bin" / executable, "-"], + [prefix_path / "bin" / executable, "-"], stdin=PIPE, stdout=PIPE, bufsize=1, # buffer by line diff --git a/celerpy/visualize.py b/celerpy/visualize.py index b4f29c1..b50c382 100644 --- a/celerpy/visualize.py +++ b/celerpy/visualize.py @@ -20,7 +20,10 @@ from matplotlib.axes import Axes as mpl_Axes from matplotlib.colors import BoundaryNorm, ListedColormap -from . import model, process +from . import process +from .model.input import ImageInput, ModelSetup, TraceInput +from .model.output import ImageParams, TraceOutput +from .model.types import GeometryEngine, MemSpace, UnitSystem __all__ = ["CelerGeo", "Imager", "plot_all_geometry"] @@ -68,9 +71,9 @@ def _register_cmaps(): UNIT_LENGTH = { - model.UnitSystem.CGS: "cm", - model.UnitSystem.CLHEP: "mm", - model.UnitSystem.SI: "m", + UnitSystem.CGS: "cm", + UnitSystem.CLHEP: "mm", + UnitSystem.SI: "m", } @@ -122,26 +125,24 @@ class CelerGeo: refactor. """ - image: Optional[model.ImageParams] - volumes: dict[model.GeometryEngine, list[str]] + image: Optional[ImageParams] + volumes: dict[GeometryEngine, list[str]] @classmethod def with_setup(cls, *args, **kwargs): """Construct, forwarding args to ModelSetup.""" - return cls(setup=model.ModelSetup(*args, **kwargs)) + return cls(setup=ModelSetup(*args, **kwargs)) @classmethod def from_filename(cls, path: Path): """Construct from a geometry filename and default other setup.""" return cls.with_setup(geometry_file=path) - def __init__(self, setup: model.ModelSetup): + def __init__(self, setup: ModelSetup): # Create the process and attach stdin/stdout pipes self.process: process.Popen = process.launch("celer-geo") # Model setup with actual parameters is echoed back - self.setup = process.communicate_model( - self.process, setup, model.ModelSetup - ) + self.setup = process.communicate_model(self.process, setup, ModelSetup) # Cached image self.image = None # Cached volume names @@ -168,9 +169,9 @@ def reset_id_map(self): def trace( self, - image: Optional[model.ImageInput] = None, + image: Optional[ImageInput] = None, *, - geometry: Optional[model.GeometryEngine] = None, + geometry: Optional[GeometryEngine] = None, **kwargs, ): """Trace with a geometry, memspace, etc.""" @@ -185,16 +186,14 @@ def trace( volumes = self.volumes.setdefault(geometry, []) with NamedTemporaryFile(suffix=".bin", mode="w+b") as f: - inp = model.TraceInput( + inp = TraceInput( geometry=geometry, volumes=(not volumes), bin_file=Path(f.name), image=image, **kwargs, ) - result = process.communicate_model( - self.process, inp, model.TraceOutput - ) + result = process.communicate_model(self.process, inp, TraceOutput) img = f.read() # Cache the geometry names and ensure trace has them @@ -216,9 +215,10 @@ def trace( return (result, npimg) - def close(self, *, timeout: float = 0.25): + def close(self, *, timeout: float = 0.25) -> Union[dict[str, dict], str]: """Cleanly exit the ray trace loop, returning run statistics if - possible.""" + possible. + """ result = process.communicate(self.process, json.dumps(None)) with contextlib.suppress(TimeoutExpired): self.process.wait(timeout=timeout) @@ -254,7 +254,7 @@ class LabeledAxes(NamedTuple): y: LabeledAxis -def calc_image_axes(image: model.ImageParams) -> LabeledAxes: +def calc_image_axes(image: ImageParams) -> LabeledAxes: """Calculate label/min/max for x and y axes from an image result.""" down = np.array(image.down) right = np.array(image.right) @@ -287,7 +287,7 @@ def calc_axes(length, dir): class Imager: axes: Optional[LabeledAxes] = None - def __init__(self, celer_geo, image: model.ImageInput): + def __init__(self, celer_geo: CelerGeo, image: ImageInput): self.celer_geo = celer_geo self.image = image self.axes = None # Lazily update @@ -295,8 +295,8 @@ def __init__(self, celer_geo, image: model.ImageInput): def __call__( self, ax: mpl_Axes, - geometry: Optional[model.GeometryEngine] = None, - memspace: Optional[model.MemSpace] = None, + geometry: Optional[GeometryEngine] = None, + memspace: Optional[MemSpace] = None, colorbar: Union[bool, None, mpl_Axes] = None, ) -> dict[str, Any]: (trace_output, img) = self.celer_geo.trace( @@ -355,10 +355,10 @@ def plot_all_geometry( colorbar: bool = True, figsize: Optional[tuple] = None, engines: Optional[Iterable] = None, -) -> Mapping[model.GeometryEngine, Any]: +) -> Mapping[GeometryEngine, Any]: """Convenience function for plotting all available geometry types.""" if engines is None: - engines = model.GeometryEngine + engines = GeometryEngine engines = list(engines) width_ratios = [1.0] * len(engines) if colorbar: @@ -389,7 +389,7 @@ def centered_image( outdir, width: Union[float, tuple[float, float]], **kwargs: Any, -) -> model.ImageInput: +) -> ImageInput: """ Create an ImageInput with a centered view based on the given parameters. @@ -412,7 +412,7 @@ def centered_image( Returns ------- - model.ImageInput + ImageInput The input to ``visualize`` to generate the centered image. """ center = np.asarray(center) @@ -430,7 +430,7 @@ def centered_image( lower_left = (center - offset).tolist() upper_right = (center + offset).tolist() - return model.ImageInput( + return ImageInput( lower_left=lower_left, upper_right=upper_right, rightward=xdir.tolist(), diff --git a/test/data/options.json b/test/data/options.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/test/data/options.json @@ -0,0 +1 @@ +{} diff --git a/test/mock-prefix/bin/celer-geo b/test/mock-prefix/bin/celer-geo index d0f35d4..d7925e0 100755 --- a/test/mock-prefix/bin/celer-geo +++ b/test/mock-prefix/bin/celer-geo @@ -5,6 +5,7 @@ """Mock the celer-geo process.""" from os import environ +from pathlib import Path from mockutils import dump, expect_trace, log, read_input, setup_signals @@ -16,8 +17,11 @@ 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}" +g4org_opts = environ.get("G4ORG_OPTIONS") +assert g4org_opts and Path(g4org_opts).is_file(), f"Expected G4ORG_OPTIONS=1, got {g4org_opts}" +profiling = environ.get("CELER_ENABLE_PROFILING") +assert profiling == "True", f"Expected CELER_ENABLE_PROFILING=1, got {profiling}" + # Read the initial command and echo it (with version) cmd = read_input() diff --git a/test/test_model.py b/test/test_model.py index 865e04d..70b75b8 100644 --- a/test/test_model.py +++ b/test/test_model.py @@ -3,19 +3,22 @@ # SPDX-License-Identifier: Apache-2.0 import json +from pathlib import Path -from celerpy import model +from celerpy.model import input as minput +from celerpy.model import output as moutput +from celerpy.model import types as mtypes def test_enum_lowercase(): # NOTE: no deprecation warning issued :'( - result = model.GeometryEngine.geant4 - assert result == model.GeometryEngine.GEANT4 + result = mtypes.GeometryEngine.geant4 + assert result == mtypes.GeometryEngine.GEANT4 - assert model.GeometryEngine("geant4") == model.GeometryEngine.GEANT4 + assert mtypes.GeometryEngine("geant4") == mtypes.GeometryEngine.GEANT4 -def test_model_setup(tmp_path): +def test_model_setup(tmp_path: Path): gdml_file = tmp_path / "foo.gdml" gdml_file.write_text("") inp_json = json.dumps( @@ -23,18 +26,33 @@ def test_model_setup(tmp_path): "geometry_file": str(gdml_file), } ) - ms = model.ModelSetup.model_validate_json(inp_json) + ms = minput.ModelSetup.model_validate_json(inp_json) assert gdml_file == ms.geometry_file def test_image_input(): - ii = model.ImageInput( + ii = minput.ImageInput( lower_left=[-1, 0, 0], upper_right=[1, 1, 0], vertical_pixels=1024 ) - assert ii == model.ImageInput.model_construct( + assert ii == minput.ImageInput.model_construct( lower_left=[-1.0, 0.0, 0.0], upper_right=[1.0, 1.0, 0.0], rightward=[1, 0, 0], vertical_pixels=1024, horizontal_divisor=None, ) + + +def test_orange_stats_serialization(): + os = minput.OrangeStats() + result = json.loads(os.model_dump_json()) + assert result == {"_cmd": "orange_stats"} + + +def test_bih_load(): + result = moutput.BihSizes.model_validate_json( + '{"bboxes":8,"inner_nodes":0,"leaf_nodes":1,"local_volume_ids":7}' + ) + assert result == moutput.BihSizes( + bboxes=8, inner_nodes=0, leaf_nodes=1, local_volume_ids=7 + ) diff --git a/test/test_visualize.py b/test/test_visualize.py index 651511f..5549757 100644 --- a/test/test_visualize.py +++ b/test/test_visualize.py @@ -8,16 +8,19 @@ import pytest from numpy.testing import assert_array_equal -from celerpy import model, visualize +from celerpy import visualize +from celerpy.model.input import ImageInput +from celerpy.model.output import TraceOutput +from celerpy.model.types import GeometryEngine 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.g4org_verbose = True settings.log = LogLevel.DEBUG settings.log_local = LogLevel.WARNING +settings.g4org_options = Path(__file__).parent / "data" / "two-boxes.gdml" +settings.profiling = True def test_CelerGeo(): @@ -26,17 +29,17 @@ def test_CelerGeo(): with pytest.raises(RuntimeError): cg.trace() (result, img) = cg.trace( - model.ImageInput(upper_right=[1, 1, 0], vertical_pixels=4), - geometry=model.GeometryEngine.ORANGE, + ImageInput(upper_right=[1, 1, 0], vertical_pixels=4), + geometry=GeometryEngine.ORANGE, ) - assert isinstance(result, model.TraceOutput) + assert isinstance(result, TraceOutput) assert img.shape == (4, 4) assert img.dtype == np.int32 - (result, img) = cg.trace(geometry=model.GeometryEngine.ORANGE) - assert isinstance(result, model.TraceOutput) + (result, img) = cg.trace(geometry=GeometryEngine.ORANGE) + assert isinstance(result, TraceOutput) assert img.shape == (4, 4) - (result, img) = cg.trace(geometry=model.GeometryEngine.geant4) - assert isinstance(result, model.TraceOutput) + (result, img) = cg.trace(geometry=GeometryEngine.geant4) + assert isinstance(result, TraceOutput) assert img.shape == (4, 4) result = cg.close() assert result @@ -69,6 +72,7 @@ def test_IdMapper(): (img, vol) = map_ids(np.array([-1, 0, -1]), ["foo"]) assert_array_equal(img, np.array([2, 2, 2])) + assert isinstance(img, np.ma.MaskedArray) assert_array_equal(img.mask, [True, False, True]) assert vol == ["bar", "baz", "foo"]