# 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)}"
)