Source code for telescope_sim.apertures.external_pupil

"""ExternalPupilAperture — wraps an arbitrary Python callable as an aperture.

Two calling conventions are supported via the ``mode`` field:

- ``"field"``: the function returns an HCIPy ``Field`` directly when called
  with a ``pupil_grid`` keyword. Use for callables like ``miles_pupil.
  generate_pupil(outer=..., pupil_grid=...)`` and ``miles_synthpsf.
  generate_pupil(...)``.
- ``"callable"``: the function returns an HCIPy aperture *callable* when
  called with whatever kwargs are supplied; the pipeline then evaluates it
  on the pupil grid via ``evaluate_supersampled``. Use for callables like
  ``hcipy.make_keck_aperture()`` or ``hcipy.make_circular_aperture(D)``.

The module is imported via ``importlib``; the path can be either a
standard dotted module name (``my_package.my_module``) or an absolute
filesystem path to a ``.py`` file (``/path/to/file.py``).
"""

from __future__ import annotations

import importlib
import importlib.util
import sys
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any

import hcipy

from telescope_sim.abc import Aperture, ApertureResult
from telescope_sim.registry import register


def _load_callable(module: str, function: str) -> Any:
    """Resolve ``module:function`` to a callable; module is dotted or a path."""
    if module.endswith(".py") or "/" in module:
        mod_path = Path(module).resolve()
        if not mod_path.is_file():
            raise FileNotFoundError(f"module file not found: {mod_path}")
        spec = importlib.util.spec_from_file_location(mod_path.stem, mod_path)
        mod = importlib.util.module_from_spec(spec)
        sys.modules[mod_path.stem] = mod
        assert spec.loader is not None
        spec.loader.exec_module(mod)
    else:
        mod = importlib.import_module(module)
    try:
        return getattr(mod, function)
    except AttributeError as e:
        raise AttributeError(f"module {module!r} has no attribute {function!r}") from e


[docs] @register("aperture", "external_pupil") @dataclass class ExternalPupilAperture(Aperture): """Aperture built by an external Python callable. Parameters ---------- module Dotted module name or filesystem path to a ``.py`` file. function Name of the callable inside the module. mode ``"field"`` (default) or ``"callable"`` — see module docstring. kwargs Extra kwargs forwarded to the callable. area Effective collecting area (used for photon flux scaling). Optional; defaults to ``0.0`` (only matters for noise computations). supersample Used only when ``mode == "callable"``. """ module: str = "" function: str = "generate_pupil" mode: str = "field" kwargs: dict[str, Any] = field(default_factory=dict) area: float = 0.0 supersample: int = 16 def __post_init__(self) -> None: if self.mode not in ("field", "callable"): raise ValueError(f"mode must be 'field' or 'callable'; got {self.mode!r}") if not self.module: raise ValueError("ExternalPupilAperture requires a 'module' name or path")
[docs] def build(self, pupil_grid: Any) -> ApertureResult: fn = _load_callable(self.module, self.function) if self.mode == "field": aper_field = fn(pupil_grid=pupil_grid, **self.kwargs) else: aper_callable = fn(**self.kwargs) aper_field = hcipy.evaluate_supersampled(aper_callable, pupil_grid, self.supersample) return ApertureResult( field=aper_field, area=float(self.area), segments=None, segment_coords=None, metadata={ "source_module": self.module, "source_function": self.function, "mode": self.mode, }, )
__all__ = ["ExternalPupilAperture"]