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:

  1. GeometryBuilder - Fluent interface for building geometries

  2. 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()
... )
__init__()[source]
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:
  • surface (Surface) – Surface geometry (materials will be replaced).

  • front (str) – Medium name for the front side material.

  • back (str) – Medium name for the back side material.

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")
surfaces: tuple[Surface, ...]
detectors: tuple[Surface, ...]
background_material: MaterialField
media: dict[str, MaterialField]
surface_names: dict[str, int]
detector_names: dict[str, int]
get_medium(name)[source]

Get the material for a named medium.

Parameters:

name (str) – The name of the medium.

Returns:

The material for the medium.

Return type:

MaterialField

Raises:

KeyError – If no medium with the given name exists.

get_surface(name)[source]

Get a surface by name.

Parameters:

name (str) – The name of the surface.

Returns:

The surface with the given name.

Return type:

Surface

Raises:

KeyError – If no surface with the given name exists.

get_surface_index(name)[source]

Get the index of a surface by name.

Parameters:

name (str) – The name of the surface.

Returns:

The index of the surface in the surfaces tuple.

Return type:

int

Raises:

KeyError – If no surface with the given name exists.

get_detector(name)[source]

Get a detector by name.

Parameters:

name (str) – The name of the detector.

Returns:

The detector with the given name.

Return type:

Surface

Raises:

KeyError – If no detector with the given name exists.

get_detector_index(name)[source]

Get the index of a detector by name.

Parameters:

name (str) – The name of the detector.

Returns:

The index of the detector in the detectors tuple.

Return type:

int

Raises:

KeyError – If no detector with the given name exists.

to_surface_list()[source]

Convert all surfaces and detectors to a list for SurfacePropagator.

Returns:

All surfaces and detectors as a mutable list.

Return type:

list of Surface

__len__()[source]

Return the total number of surfaces and detectors.

__iter__()[source]

Iterate over all surfaces and detectors.

__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 front and back parameters 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 Geometry object.

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)