Geometry Module
The geometry module provides the GeometryBuilder for constructing simulation
geometries and the Geometry container for the built result.
Overview
The geometry system uses a builder pattern to construct immutable simulation geometries:
GeometryBuilder - Fluent interface for building geometries
Geometry - Immutable container holding the built geometry
GeometryBuilder
- class lsurf.geometry.GeometryBuilder[source]
Fluent builder for constructing simulation geometries.
Provides a simple interface for registering named media and adding surfaces with validation for duplicate names, surface limits, and material consistency.
Supports two modes: 1. Standard mode: Use add_surface(surface, front=, back=) for simple geometries 2. Cell mode: Use add_surface_only() + add_cell() for complex geometries
Examples
Standard mode (parallel planes):
>>> from lsurf.geometry import GeometryBuilder >>> from lsurf.materials import WATER, ExponentialAtmosphere >>> from lsurf.surfaces import SphereSurface, PlaneSurface, SurfaceRole >>> >>> EARTH_RADIUS = 6.371e6 >>> atmosphere = ExponentialAtmosphere() >>> >>> ocean = SphereSurface( ... center=(0, 0, -EARTH_RADIUS), ... radius=EARTH_RADIUS, ... role=SurfaceRole.OPTICAL, ... name="ocean", ... ) >>> detector = PlaneSurface( ... point=(0, 0, 35000), ... normal=(0, 0, 1), ... role=SurfaceRole.DETECTOR, ... name="detector_35km", ... ) >>> >>> geometry = ( ... GeometryBuilder() ... .register_medium("atmosphere", atmosphere) ... .register_medium("ocean", WATER) ... .set_background("atmosphere") ... .add_surface(ocean, front="atmosphere", back="ocean") ... .add_detector(detector) ... .build() ... )
Cell mode (non-parallel planes with different materials):
>>> plane_x = PlaneSurface(point=(0, 0, 0), normal=(1, 0, 0), ...) >>> plane_y = PlaneSurface(point=(0, 0, 0), normal=(0, 1, 0), ...) >>> >>> geometry = ( ... GeometryBuilder() ... .register_medium("air", AIR) ... .register_medium("water", WATER) ... .register_medium("glass", GLASS) ... .register_medium("vacuum", VACUUM) ... .set_background("air") ... .add_surface_only(plane_x) ... .add_surface_only(plane_y) ... .add_cell("air", ("plane_x", True), ("plane_y", True)) # Q1 ... .add_cell("water", ("plane_x", False), ("plane_y", True)) # Q2 ... .add_cell("glass", ("plane_x", True), ("plane_y", False)) # Q3 ... .add_cell("vacuum", ("plane_x", False), ("plane_y", False)) # Q4 ... .build() ... )
- register_medium(name, material)[source]
Register a named medium with its material.
A medium is a named reference to a material. Surfaces reference media by name, ensuring material consistency when multiple surfaces share the same medium.
If the same medium name is registered twice, validates that the same material instance is used.
- Parameters:
name (str) – Name of the medium.
material (MaterialField) – Material for this medium.
- Returns:
The builder instance for chaining.
- Return type:
Self
- Raises:
ValueError – If medium already registered with different material instance.
- set_background(medium_name)[source]
Set the background/propagation medium by name.
The medium must be registered before calling this method.
- Parameters:
medium_name (str) – Name of the medium to use as background.
- Returns:
The builder instance for chaining.
- Return type:
Self
- Raises:
ValueError – If the medium is not registered.
- add_surface(surface, front, back)[source]
Add a surface with materials from named media.
- Parameters:
- Returns:
The builder instance for chaining.
- Return type:
Self
- Raises:
ValueError – If surface name is duplicate, medium names are not registered, or cell mode is active.
- add_surface_only(surface)[source]
Add a surface without material assignment (for cell mode).
Use this with add_cell() to define complex geometries where surfaces intersect and the simple front/back model is insufficient.
- Parameters:
surface (Surface) – Surface geometry. Materials should be left as None.
- Returns:
The builder instance for chaining.
- Return type:
Self
- Raises:
ValueError – If surface name is duplicate.
- add_cell(medium_name, *conditions, name='')[source]
Define a cell (region) by half-space intersection.
A cell is a region of space defined by being on specific sides of multiple surfaces. Each condition specifies a surface and which side (front=True means signed_distance > 0).
- Parameters:
medium_name (str) – Name of the medium for this cell.
*conditions (tuple[str, bool]) –
Each condition is (surface_name, front) where: - surface_name: Name of a surface added with add_surface_only() - front: True for front side (signed_distance > 0),
False for back side (signed_distance < 0)
name (str, optional) – Optional human-readable name for the cell.
- Returns:
The builder instance for chaining.
- Return type:
Self
- Raises:
ValueError – If medium is not registered or surface name not found.
Examples
>>> builder.add_cell("air", ("plane_x", True), ("plane_y", True), name="Q1")
- add_detector(detector)[source]
Add a detector Surface to the geometry.
- Parameters:
detector (Surface) – Any object implementing the Surface protocol. Must have a unique name attribute.
- Returns:
The builder instance for chaining.
- Return type:
Self
- Raises:
ValueError – If detector name is duplicate.
- build(validate=True)[source]
Build the immutable Geometry object.
- Parameters:
validate (bool, optional) – If True (default), validates that surface material assignments are consistent. Set to False to skip validation.
- Returns:
Standard Geometry if using add_surface(), or CellGeometry if using add_surface_only() + add_cell().
- Return type:
Geometry or CellGeometry
- Raises:
ValueError – If background medium not set or OPTICAL surfaces missing materials.
IntersectingSurfacesError – If non-parallel surfaces have conflicting material assignments (only when validate=True).
Geometry
- class lsurf.geometry.Geometry(surfaces, detectors, background_material, media, surface_names, detector_names)[source]
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")
- background_material: MaterialField
- media: dict[str, MaterialField]
- get_medium(name)[source]
Get the material for a named medium.
- __init__(surfaces, detectors, background_material, media, surface_names, detector_names)
Usage Example
from lsurf.geometry import GeometryBuilder
from lsurf.materials import ExponentialAtmosphere, WATER
from lsurf.surfaces import SphereSurface, PlaneSurface, SurfaceRole
EARTH_RADIUS = 6.371e6
# Create materials
atmosphere = ExponentialAtmosphere(n_sea_level=1.000293)
# Create surfaces
ocean = SphereSurface(
center=(0, 0, -EARTH_RADIUS),
radius=EARTH_RADIUS,
role=SurfaceRole.OPTICAL,
name="ocean",
)
detector = PlaneSurface(
point=(0, 0, 35000),
normal=(0, 0, 1),
role=SurfaceRole.DETECTOR,
name="detector_35km",
)
# Build geometry
geometry = (
GeometryBuilder()
.register_medium("atmosphere", atmosphere)
.register_medium("ocean", WATER)
.set_background("atmosphere")
.add_surface(ocean, front="atmosphere", back="ocean")
.add_detector(detector)
.build()
)
# Access geometry components
ocean_surface = geometry.get_surface("ocean")
detector_surface = geometry.get_detector("detector_35km")
atmo_material = geometry.get_medium("atmosphere")
Builder Methods
The GeometryBuilder provides a fluent interface:
- register_medium(name, material)
Register a named medium with its material. Multiple surfaces can reference the same medium by name, ensuring material consistency.
- set_background(medium_name)
Set the background propagation medium. This is the material through which rays propagate between surfaces.
- add_surface(surface, front, back)
Add an optical or absorber surface. The
frontandbackparameters specify which registered media are on each side of the surface.- add_detector(detector)
Add a detector surface. Detectors don’t need front/back materials since they simply record rays that hit them.
- build()
Finalize and return the immutable
Geometryobject.
Geometry Accessors
The Geometry object provides accessors for its components:
- get_surface(name)
Get a surface by name.
- get_detector(name)
Get a detector by name.
- get_medium(name)
Get a material by medium name.
- to_surface_list()
Get all surfaces and detectors as a list (for propagators).
Surface Roles
Surfaces must have a role that determines their behavior:
from lsurf.surfaces import SurfaceRole
# OPTICAL: Reflects and/or refracts rays (Fresnel coefficients)
ocean = SphereSurface(..., role=SurfaceRole.OPTICAL)
# DETECTOR: Records rays and terminates them
detector = PlaneSurface(..., role=SurfaceRole.DETECTOR)
# ABSORBER: Terminates rays without recording
boundary = SphereSurface(..., role=SurfaceRole.ABSORBER)