Source code for lsurf.propagation.propagators.factory

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

"""
Propagator Factory

Factory function for creating propagators with proper kernel/propagator
selection based on material compatibility declarations.

This module provides a unified interface for creating propagators,
handling the selection of the appropriate propagator and kernel based on:
1. Material's declared compatibility
2. User overrides at propagator creation time
3. Material instance preferences
4. Class defaults
"""

from __future__ import annotations

from typing import TYPE_CHECKING, Any

from ..kernels.registry import PropagationKernelID, PropagatorID

if TYPE_CHECKING:
    from ...materials.base.material_field import MaterialField

__all__ = ["create_propagator"]


[docs] def create_propagator( material: MaterialField, propagator_id: PropagatorID | None = None, kernel_id: PropagationKernelID | None = None, method: str | None = None, prefer_gpu: bool = True, **kwargs: Any, ) -> Any: """ Create the appropriate propagator for a material. Selection priority (highest to lowest): 1. If propagator_id/kernel_id passed here → use those (override) 2. Else use material's stored preference (material._propagator_id, material._kernel_id) 3. Else use material's class defaults Parameters ---------- material : MaterialField The material to propagate through. propagator_id : PropagatorID, optional Override propagator selection. If None, uses material's preference. kernel_id : PropagationKernelID, optional Override kernel selection. If None, uses material's preference. Note: Only used for GPU propagators. method : str, optional Integration method ("euler" or "rk4"). If None, determined from kernel_id. prefer_gpu : bool, default True If True and material supports GPU, use GPU propagator. If False, force CPU propagator. **kwargs Additional propagator configuration (e.g., threads_per_block). Returns ------- Propagator Configured propagator instance appropriate for the material. Raises ------ ValueError If propagator_id or kernel_id is not supported by the material. Examples -------- >>> from lsurf.materials import ExponentialAtmosphere >>> from lsurf.propagation.propagators import create_propagator >>> from lsurf.propagation.kernels import PropagatorID, PropagationKernelID >>> # Use defaults (RK4 kernel, GPU propagator) >>> atmo = ExponentialAtmosphere(n_sea_level=1.000293) >>> propagator = create_propagator(atmo) >>> # Override kernel at propagator creation time >>> propagator_euler = create_propagator( ... atmo, ... kernel_id=PropagationKernelID.SIMPLE_EULER ... ) >>> # Force CPU propagator >>> cpu_propagator = create_propagator(atmo, propagator_id=PropagatorID.CPU_GRADIENT) >>> # Or use prefer_gpu=False >>> cpu_propagator = create_propagator(atmo, prefer_gpu=False) """ # Resolve propagator: override > prefer_gpu > material instance > class default resolved_propagator_id = propagator_id if resolved_propagator_id is None: # If prefer_gpu is False, force CPU propagator if not prefer_gpu: resolved_propagator_id = PropagatorID.CPU_GRADIENT else: resolved_propagator_id = getattr(material, "_propagator_id", None) if resolved_propagator_id is None: resolved_propagator_id = material.default_propagator() # Validate propagator is supported supported_propagators = material.supported_propagators() if supported_propagators and resolved_propagator_id not in supported_propagators: raise ValueError( f"{material.__class__.__name__} does not support {resolved_propagator_id}. " f"Supported: {supported_propagators}" ) # Resolve kernel: override > material instance > class default resolved_kernel_id = kernel_id if resolved_kernel_id is None: resolved_kernel_id = getattr(material, "_kernel_id", None) if resolved_kernel_id is None: resolved_kernel_id = material.default_kernel() # Validate kernel is supported (if applicable) supported_kernels = material.supported_kernels() if supported_kernels and resolved_kernel_id is not None: if resolved_kernel_id not in supported_kernels: raise ValueError( f"{material.__class__.__name__} does not support {resolved_kernel_id}. " f"Supported: {supported_kernels}" ) # Resolve method: parameter > kernel_id > default resolved_method = method if resolved_method is None: resolved_method = "rk4" # Default if resolved_kernel_id is not None: # Map kernel ID to method string if "EULER" in resolved_kernel_id.name: resolved_method = "euler" elif "RK4" in resolved_kernel_id.name: resolved_method = "rk4" # Create the appropriate propagator if resolved_propagator_id == PropagatorID.CPU_GRADIENT: from .gradient import GradientPropagator return GradientPropagator(method=resolved_method, **kwargs) elif resolved_propagator_id == PropagatorID.GPU_GRADIENT: from .gpu_gradient import GPUGradientPropagator return GPUGradientPropagator( material=material, method=resolved_method, **kwargs ) elif resolved_propagator_id == PropagatorID.GPU_SPECTRAL: from .spectral_gpu_gradient import SpectralGPUGradientPropagator return SpectralGPUGradientPropagator( material=material, method=resolved_method, **kwargs ) elif resolved_propagator_id == PropagatorID.GPU_SURFACE: from .surface_propagator import SurfacePropagator return SurfacePropagator(material=material, method=resolved_method, **kwargs) else: raise ValueError( f"Unknown propagator ID: {resolved_propagator_id}. " f"Available: {list(PropagatorID)}" )