Source code for lsurf.surfaces.registry

# 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.

"""
Surface Type Registry

A flexible registry for surface types (plane, sphere, wave, etc.).
Each surface type is registered with a name, capability ("gpu" or "cpu"),
an integer ID for GPU kernel dispatch, and the surface class itself.

The registry ensures:
- Each surface type name is unique
- Each (capability, id) combination is unique (no GPU id conflicts)
- GPU IDs are positive integers (used by GPU kernels)
- CPU-only surfaces use id=0

Usage
-----
Register new surface types (typically done in the surface class module):

    from lsurf.surfaces.registry import register_surface_type

    @register_surface_type("cylinder", "gpu", 3)
    class CylinderSurface(Surface):
        ...

Or register manually:

    register_surface_type("cylinder", "gpu", 3, CylinderSurface)

Query the registry:

    from lsurf.surfaces.registry import get_surface_type, get_surface_class

    info = get_surface_type("plane")
    # -> SurfaceTypeInfo('plane', 'gpu', id=1, cls=<class 'PlaneSurface'>)

    cls = get_surface_class("plane")
    # -> <class 'PlaneSurface'>
"""

from dataclasses import dataclass
from typing import Literal, TYPE_CHECKING

if TYPE_CHECKING:
    from .protocol import Surface


[docs] @dataclass(frozen=True) class SurfaceTypeInfo: """Information about a registered surface type.""" name: str capability: Literal["gpu", "cpu"] id: int cls: type["Surface"] | None = None def __repr__(self) -> str: cls_name = self.cls.__name__ if self.cls else "None" return f"SurfaceTypeInfo({self.name!r}, {self.capability!r}, id={self.id}, cls={cls_name})"
class SurfaceTypeRegistry: """ Registry for surface types. Ensures uniqueness of: - Surface type names - (capability, id) combinations for GPU surfaces """ def __init__(self) -> None: self._by_name: dict[str, SurfaceTypeInfo] = {} self._gpu_ids: dict[int, str] = {} # id -> name mapping for GPU surfaces self._by_class: dict[type, SurfaceTypeInfo] = {} # class -> info mapping def register( self, name: str, capability: Literal["gpu", "cpu"], surface_id: int | None = None, cls: type["Surface"] | None = None, ) -> SurfaceTypeInfo: """ Register a new surface type. Parameters ---------- name : str Unique name for the surface type (e.g., "plane", "sphere"). capability : "gpu" or "cpu" Whether this surface supports GPU acceleration. surface_id : int, optional GPU kernel ID. Required for GPU surfaces (must be > 0). CPU surfaces always use id=0. cls : type, optional The surface class to register. Returns ------- SurfaceTypeInfo The registered surface type information. Raises ------ ValueError If name already exists, or if GPU id is already taken. """ # Validate name uniqueness if name in self._by_name: existing = self._by_name[name] # Allow re-registration with the same class (for adding class after initial registration) if cls is not None and existing.cls is None: # Update existing entry with class new_info = SurfaceTypeInfo( name=existing.name, capability=existing.capability, id=existing.id, cls=cls, ) self._by_name[name] = new_info self._by_class[cls] = new_info return new_info elif cls is None or cls is existing.cls: return existing else: raise ValueError( f"Surface type '{name}' already registered as " f"({existing.capability}, id={existing.id})" ) # Handle GPU vs CPU if capability == "gpu": if surface_id is None: raise ValueError("GPU surfaces require a surface_id > 0") if surface_id <= 0: raise ValueError(f"GPU surface_id must be > 0, got {surface_id}") if surface_id in self._gpu_ids: existing_name = self._gpu_ids[surface_id] raise ValueError( f"GPU id {surface_id} already used by '{existing_name}'" ) self._gpu_ids[surface_id] = name final_id = surface_id else: # CPU surfaces always have id=0 if surface_id is not None and surface_id != 0: raise ValueError( f"CPU surfaces must have id=0 (or omit it), got {surface_id}" ) final_id = 0 info = SurfaceTypeInfo(name=name, capability=capability, id=final_id, cls=cls) self._by_name[name] = info if cls is not None: self._by_class[cls] = info return info def get(self, name: str) -> SurfaceTypeInfo: """ Get surface type info by name. Raises KeyError if not found. """ if name not in self._by_name: available = ", ".join(sorted(self._by_name.keys())) raise KeyError(f"Unknown surface type '{name}'. Registered: {available}") return self._by_name[name] def get_by_class(self, cls: type) -> SurfaceTypeInfo: """ Get surface type info by class. Raises KeyError if not found. """ if cls not in self._by_class: raise KeyError( f"Class {cls.__name__} not registered in surface type registry" ) return self._by_class[cls] def get_id(self, name: str) -> int: """Get the GPU kernel ID for a surface type.""" return self.get(name).id def get_class(self, name: str) -> type["Surface"] | None: """Get the surface class for a surface type.""" return self.get(name).cls def get_capability(self, name: str) -> Literal["gpu", "cpu"]: """Get the capability type for a surface type.""" return self.get(name).capability def is_gpu_capable(self, name: str) -> bool: """Check if a surface type is GPU-capable.""" return self.get(name).capability == "gpu" def list_all(self) -> list[SurfaceTypeInfo]: """List all registered surface types.""" return list(self._by_name.values()) def list_by_capability( self, capability: Literal["gpu", "cpu"] ) -> list[SurfaceTypeInfo]: """List surface types filtered by capability.""" return [ info for info in self._by_name.values() if info.capability == capability ] def __contains__(self, name: str) -> bool: """Check if a surface type name is registered.""" return name in self._by_name def __len__(self) -> int: """Number of registered surface types.""" return len(self._by_name) def __repr__(self) -> str: gpu_count = len(self._gpu_ids) cpu_count = len(self._by_name) - gpu_count return f"SurfaceTypeRegistry({gpu_count} GPU, {cpu_count} CPU)" # Global registry instance _registry = SurfaceTypeRegistry() # --- Module-level convenience functions ---
[docs] def register_surface_type( name: str, capability: Literal["gpu", "cpu"], surface_id: int | None = None, cls: type["Surface"] | None = None, ) -> SurfaceTypeInfo | type["Surface"]: """ Register a new surface type. Can be used as a function or decorator. Parameters ---------- name : str Unique name for the surface type (e.g., "plane", "sphere"). capability : "gpu" or "cpu" Whether this surface supports GPU acceleration. surface_id : int, optional GPU kernel ID. Required for GPU surfaces (must be > 0). CPU surfaces always use id=0. cls : type, optional The surface class. If None, returns a decorator. Returns ------- SurfaceTypeInfo or decorator If cls is provided, returns SurfaceTypeInfo. If cls is None, returns a decorator that registers the class. Examples -------- >>> # Direct registration >>> register_surface_type("plane", "gpu", 1, PlaneSurface) SurfaceTypeInfo('plane', 'gpu', id=1, cls=PlaneSurface) >>> # As decorator >>> @register_surface_type("cylinder", "gpu", 3) ... class CylinderSurface(Surface): ... pass """ if cls is not None: return _registry.register(name, capability, surface_id, cls) # Return decorator def decorator(cls: type["Surface"]) -> type["Surface"]: _registry.register(name, capability, surface_id, cls) return cls return decorator
[docs] def get_surface_type(name: str) -> SurfaceTypeInfo: """Get full surface type info by name.""" return _registry.get(name)
[docs] def get_surface_type_id(name: str) -> int: """Get the GPU kernel ID for a surface type.""" return _registry.get_id(name)
[docs] def get_surface_class(name: str) -> type["Surface"] | None: """Get the surface class for a surface type.""" return _registry.get_class(name)
[docs] def is_gpu_capable(name: str) -> bool: """Check if a surface type supports GPU acceleration.""" return _registry.is_gpu_capable(name)
[docs] def list_surface_types( capability: Literal["gpu", "cpu"] | None = None, ) -> list[SurfaceTypeInfo]: """ List registered surface types. Parameters ---------- capability : "gpu", "cpu", or None Filter by capability. None returns all. """ if capability is None: return _registry.list_all() return _registry.list_by_capability(capability)
[docs] def surface_type_exists(name: str) -> bool: """Check if a surface type is registered.""" return name in _registry
# --- Backwards compatibility aliases --- register_geometry = register_surface_type get_geometry_info = get_surface_type get_geometry_id = get_surface_type_id list_geometries = list_surface_types geometry_exists = surface_type_exists GeometryInfo = SurfaceTypeInfo # --- Register built-in surface types --- # Note: Classes are registered when their modules are imported. # Here we just reserve the names and IDs. # GPU-capable surfaces (used by GPU kernels) register_surface_type("plane", "gpu", 1) register_surface_type("sphere", "gpu", 2) # CPU-only surfaces (complex shapes requiring ray marching) register_surface_type("gerstner_wave", "cpu") register_surface_type("curved_wave", "cpu")