Source code for eradiate.converters

from __future__ import annotations

__all__ = [
    "auto_or",
    "convert_absdb",
    "convert_thermoprops",
    "on_quantity",
    "passthrough",
    "passthrough_type",
    "resolve_keyword",
    "resolve_path",
    "to_mi_scalar_transform",
]

import os
from pathlib import Path
from typing import Any, Callable

import attrs
import mitsuba as mi
import numpy as np
import pint
import xarray as xr
from axsdb import AbsorptionDatabase

import eradiate

from .attrs import AUTO
from .data import fresolver
from .exceptions import DataError, UnsupportedModeError
from .typing import PathLike


[docs] def on_quantity( wrapped_converter: Callable[[Any], Any], ) -> Callable[[Any], Any]: """ Apply a converter to the magnitude of a :class:`pint.Quantity`. Parameters ---------- wrapped_converter : callable The converter which will be applied to the magnitude of a :class:`pint.Quantity`. Returns ------- callable """ def f(value: Any) -> Any: if isinstance(value, pint.Quantity): return wrapped_converter(value.magnitude) * value.units else: return wrapped_converter(value) return f
[docs] def auto_or( wrapped_converter: Callable[[Any], Any], ) -> Callable[[Any], Any]: """ A converter that allows an attribute to be set to :data:`.AUTO`. Parameters ---------- wrapped_converter : callable The converter that is used for non-:data:`.AUTO` values. Returns ------- callable """ def f(value): if value is AUTO: return value return wrapped_converter(value) return f
[docs] def passthrough(predicate: Callable[[Any], bool]) -> Callable[[Any], Any]: """ Pass through values for which ``predicate`` returns ``True``; otherwise, apply wrapped converter. See Also -------- :func:`.passthrough_type` """ def wrapped_converter(converter: Callable | attrs.Converter) -> Any: if isinstance( converter, attrs.Converter ): # See https://github.com/python-attrs/attrs/pull/1372 def passthrough_converter(val, inst, field): return val if predicate(val) else converter(val, inst, field) return attrs.Converter( passthrough_converter, takes_self=True, takes_field=True ) else: def passthrough_converter(val): return val if predicate(val) else converter(val) return passthrough_converter return wrapped_converter
[docs] def passthrough_type(types: type | tuple[type, ...]) -> Callable: """ Pass through values of a specified type; otherwise, apply wrapped converter. See Also -------- :func:`.passthrough` """ return passthrough(lambda x: isinstance(x, types))
[docs] def resolve_keyword(path_forming_func: Callable[[Any], PathLike]) -> Callable: """ Attempt resolving a keyword into a path constructed from a keyword by the ``path_forming_func`` parameter. If the generated path points to a file, the path is returned; otherwise, ``value`` is returned without modification. Parameters ---------- path_forming_func : callable A callable with signature ``f(x: str) -> Path`` that constructs relative or absolute paths from keywords. Relative paths are then resolved by the file resolver. """ def resolve_keyword_converter(value): path = fresolver.resolve(path_forming_func(value)) if path.is_file(): return path else: return value return resolve_keyword_converter
[docs] def resolve_path(value: PathLike) -> Path: """ Resolve a file path with the file resolver. The current working directory is included in the path lookup. """ return fresolver.resolve(value, cwd=True)
def load_dataset(value: PathLike) -> xr.Dataset: """ Attempt loading a dataset given a path. If the path is relative, it is resolved by the file resolver first. Parameters ---------- value Path to the targeted dataset. Raises ------ DataError If the file could not be loaded. """ path = resolve_path(value) try: return xr.load_dataset(path) except Exception as e: raise DataError(f"could not load dataset '{value}'") from e
[docs] def to_mi_scalar_transform(value: Any): """ Convert an array-like value to a :class:`mitsuba.ScalarTransform4f`. If `value` is a Numpy array, it is used to initialize a :class:`mitsuba.ScalarTransform4f` without copy; if it is a list, a Numpy array is first created from it. Otherwise, `value` is forwarded without change. """ if isinstance(value, np.ndarray): return mi.ScalarTransform4f(value) elif isinstance(value, list): return mi.ScalarTransform4f(np.array(value)) else: return value
[docs] def convert_thermoprops(value: Any) -> xr.Dataset: """Converter for atmosphere thermophysical properties specifications.""" import joseki # Dataset: do nothing if isinstance(value, xr.Dataset): return value # PathLike: try to load dataset if isinstance(value, (os.PathLike, str)): path = fresolver.resolve(value) if path.is_file(): return joseki.load_dataset(path) else: raise ValueError( f"invalid path for 'thermoprops': {path} (expected a file)" ) # Dictionary: forward to joseki.make() if isinstance(value, dict): return joseki.make(**value) # Anything else: raise error raise TypeError( f"invalid type for 'thermoprops': {type(value)} (expected Dataset or path-like)" )
[docs] def convert_absdb(value: Any) -> AbsorptionDatabase: """ Attempt conversion of a value to an absorption database. Parameters ---------- value The value for which conversion is attempted. Returns ------- MonoAbsorptionDatabase or CKDAbsorptionDatabase Notes ----- Conversion rules are as follows: * If ``value`` is a string, try converting using the factory's :meth:`~axsdb.AbsorptionDatabaseFactory.create` method. Do not raise if this fails. * If ``value`` is a string or a path, try converting using the :meth:`~axsdb.AbsorptionDatabase.from_directory` constructor after passing through the file resolver. The returned type is consistent with the active mode. * If ``value`` is a dict, try converting using the :meth:`~axsdb.AbsorptionDatabase.from_dict` constructor. The returned type is consistent with the active mode. * Otherwise, do not convert. """ if isinstance(value, str): from .radprops import absdb_factory try: return absdb_factory.create(value) except (ValueError, KeyError): pass if isinstance(value, (str, Path)): value = fresolver.resolve(value) mode = eradiate.get_mode() if mode.is_mono: mode = "mono" elif mode.is_ckd: mode = "ckd" else: raise UnsupportedModeError(supported=["mono", "ckd"]) return AbsorptionDatabase.convert(value, mode)