# The Clear BSD License
#
# Copyright (c) 2026 Tobias Heibges
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted (subject to the limitations in the disclaimer
# below) provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# * Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from this
# software without specific prior written permission.
#
# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
# THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
"""
Immutable Geometry Container
Holds the result of a GeometryBuilder.build() call.
Provides convenient accessors for surfaces, detectors, and materials.
"""
from dataclasses import dataclass
from ..surfaces import Surface
from ..materials import MaterialField
[docs]
@dataclass(frozen=True)
class Geometry:
"""
Immutable container for simulation geometry.
Created by GeometryBuilder.build(). Provides access to surfaces,
detectors, and named media by name or index.
Parameters
----------
surfaces : tuple of Surface
All optical/absorber surfaces in the geometry.
detectors : tuple of Surface
All detector surfaces in the geometry.
background_material : MaterialField
The background/ambient material (from set_background() medium).
media : dict
Mapping from medium name to MaterialField.
surface_names : dict
Mapping from surface name to index in surfaces tuple.
detector_names : dict
Mapping from detector name to index in detectors tuple.
Examples
--------
>>> geometry = builder.build()
>>> ocean = geometry.get_surface("ocean")
>>> detector = geometry.get_detector("detector_35km")
>>> atmosphere_material = geometry.get_medium("atmosphere")
"""
surfaces: tuple[Surface, ...]
detectors: tuple[Surface, ...]
background_material: MaterialField
media: dict[str, MaterialField]
surface_names: dict[str, int]
detector_names: dict[str, int]
[docs]
def get_medium(self, name: str) -> MaterialField:
"""
Get the material for a named medium.
Parameters
----------
name : str
The name of the medium.
Returns
-------
MaterialField
The material for the medium.
Raises
------
KeyError
If no medium with the given name exists.
"""
if name not in self.media:
available = ", ".join(sorted(self.media.keys()))
raise KeyError(f"No medium named '{name}'. Available: {available}")
return self.media[name]
[docs]
def get_surface(self, name: str) -> Surface:
"""
Get a surface by name.
Parameters
----------
name : str
The name of the surface.
Returns
-------
Surface
The surface with the given name.
Raises
------
KeyError
If no surface with the given name exists.
"""
if name not in self.surface_names:
available = ", ".join(sorted(self.surface_names.keys()))
raise KeyError(f"No surface named '{name}'. Available: {available}")
return self.surfaces[self.surface_names[name]]
[docs]
def get_surface_index(self, name: str) -> int:
"""
Get the index of a surface by name.
Parameters
----------
name : str
The name of the surface.
Returns
-------
int
The index of the surface in the surfaces tuple.
Raises
------
KeyError
If no surface with the given name exists.
"""
if name not in self.surface_names:
available = ", ".join(sorted(self.surface_names.keys()))
raise KeyError(f"No surface named '{name}'. Available: {available}")
return self.surface_names[name]
[docs]
def get_detector(self, name: str) -> Surface:
"""
Get a detector by name.
Parameters
----------
name : str
The name of the detector.
Returns
-------
Surface
The detector with the given name.
Raises
------
KeyError
If no detector with the given name exists.
"""
if name not in self.detector_names:
available = ", ".join(sorted(self.detector_names.keys()))
raise KeyError(f"No detector named '{name}'. Available: {available}")
return self.detectors[self.detector_names[name]]
[docs]
def get_detector_index(self, name: str) -> int:
"""
Get the index of a detector by name.
Parameters
----------
name : str
The name of the detector.
Returns
-------
int
The index of the detector in the detectors tuple.
Raises
------
KeyError
If no detector with the given name exists.
"""
if name not in self.detector_names:
available = ", ".join(sorted(self.detector_names.keys()))
raise KeyError(f"No detector named '{name}'. Available: {available}")
return self.detector_names[name]
[docs]
def to_surface_list(self) -> list[Surface]:
"""
Convert all surfaces and detectors to a list for SurfacePropagator.
Returns
-------
list of Surface
All surfaces and detectors as a mutable list.
"""
return list(self.surfaces) + list(self.detectors)
[docs]
def __len__(self) -> int:
"""Return the total number of surfaces and detectors."""
return len(self.surfaces) + len(self.detectors)
[docs]
def __iter__(self):
"""Iterate over all surfaces and detectors."""
return iter(self.surfaces + self.detectors)