Source code for eradiate.converters
from __future__ import annotations
__all__ = [
"auto_or",
"on_quantity",
"to_dataset",
]
import os
import typing as t
import mitsuba as mi
import numpy as np
import pint
import xarray as xr
from . import data
from .attrs import AUTO
from .typing import PathLike
[docs]def on_quantity(
wrapped_converter: t.Callable[[t.Any], t.Any]
) -> t.Callable[[t.Any], t.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: t.Any) -> t.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: t.Callable[[t.Any], t.Any]
) -> t.Callable[[t.Any], t.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 to_dataset(
load_from_id: t.Callable[[str], xr.Dataset] | None = None,
) -> t.Callable[[xr.Dataset | PathLike], xr.Dataset]:
"""
Generates a converter that converts a value to a :class:`xarray.Dataset`.
Parameters
----------
load_from_id : callable, optional
A callable with the signature ``f(x: str) -> Dataset`` used to
interpret dataset identifiers.
Set this parameter to handle dataset identifiers.
If unset, dataset identifiers are not supported.
Returns
-------
A dataset converter.
Notes
-----
The conversion logic is as follows:
1. If the value is an xarray dataset, it is returned directly.
2. If the value is a path-like object ending with the ``.nc`` extension, the
converter tries to load a dataset from that location, first locally, then
(should that fail) from the Eradiate data store.
3. If the value is a string and ``load_from_id`` is not ``None``, it is
interpreted as a dataset identifier and ``load_from_id(value)`` is
returned.
4. Otherwise, a :class:`ValueError` is raised.
Examples
--------
A converter with basic dataset identifier interpretation (the passed
callable may implement more complex logic, *e.g.* with identifier
fallback substitution):
>>> aerosol_converter = to_dataset(
... lambda x: data.load_dataset(f"spectra/particles/{x}.nc")
... )
A converter without dataset identifier interpretation:
>>> aerosol_converter = to_dataset()
"""
def converter(value: xr.Dataset | PathLike) -> xr.Dataset:
if isinstance(value, xr.Dataset):
return value
# Path (local or remote)
if str(value).endswith(".nc"):
# Try and open a file if it is directly referenced
if os.path.isfile(value):
return xr.load_dataset(value)
# Try and serve the file from the data store
return data.load_dataset(value)
# Identifier for a dataset in the data store
if isinstance(value, str) and load_from_id is not None:
return load_from_id(value)
# Abnormal state
# Reference must be provided as a Dataset, a path-like or a str
raise ValueError(f"Cannot convert value '{value}'")
return converter
def to_mi_scalar_transform(value):
"""
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