Source code for optika.vectors._vectors_object
from __future__ import annotations
from typing import TypeVar
from typing_extensions import Self
import dataclasses
import numpy as np
import astropy.units as u
import named_arrays as na
import optika
from . import AbstractPupilVectorArray, PupilVectorArray
from . import AbstractSceneVectorArray, SceneVectorArray
__all__ = [
"AbstractObjectVectorArray",
"ObjectVectorArray",
]
WavelengthT = TypeVar("WavelengthT", bound=na.ScalarLike)
FieldT = TypeVar("FieldT", bound=na.AbstractCartesian2dVectorArray)
PupilT = TypeVar("PupilT", bound=na.AbstractCartesian2dVectorArray)
[docs]
@dataclasses.dataclass(eq=False, repr=False)
class AbstractObjectVectorArray(
AbstractPupilVectorArray,
AbstractSceneVectorArray,
):
"""An interface describing a field position, pupil position, and wavelength."""
@property
def type_abstract(self) -> type[AbstractObjectVectorArray]:
return AbstractObjectVectorArray
@property
def type_explicit(self) -> type[ObjectVectorArray]:
return ObjectVectorArray
@property
def type_matrix(self) -> type[na.AbstractMatrixArray]:
raise NotImplementedError
[docs]
def cell_area(
self,
axis_wavelength: str,
axis_field: tuple[str, str],
axis_pupil: tuple[str, str],
) -> na.AbstractScalar:
r"""
Compute the 5-dimensional area of each grid cell in units of wavelength
:math:`\times` area :math:`\times` solid angle.
This method does not work in general.
It only works for the special case where the wavelength grid
is not dependent on the field or pupil position,
and the field grid does not depend on the pupil position.
Parameters
----------
axis_wavelength
The logical axis corresponding to changing wavelength.
axis_field
The two logical axes corresponding to changing field position.
axis_pupil
the two logical axes corresponding to changing pupil position.
"""
wavelength = self.wavelength
field = self.field
pupil = self.pupil
shape_wavelength = wavelength.shape
shape_field = field.shape
shape_pupil = pupil.shape
if axis_wavelength not in shape_wavelength:
raise ValueError( # pragma: nocover
f"{axis_wavelength=} must be in {shape_wavelength=}",
)
if not set(axis_field).issubset(shape_field):
raise ValueError( # pragma: nocover
f"{axis_field=} must be a subset of {shape_field=}",
)
if set(axis_field).intersection(shape_wavelength):
raise ValueError( # pragma: nocover
f"{axis_field=} must not intersect {shape_wavelength=}"
)
if not set(axis_pupil).issubset(shape_pupil):
raise ValueError( # pragma: nocover
f"{axis_pupil=} must be a subset of {shape_pupil=}",
)
if set(axis_pupil).intersection(shape_wavelength | shape_field):
raise ValueError( # pragma: nocover
f"{axis_pupil=} must not intersect {shape_wavelength=} or {shape_field=}"
)
area_wavelength = wavelength.volume_cell(axis_wavelength)
shape_field = na.broadcast_shapes(
shape_wavelength,
shape_field,
)
field = field.broadcast_to(shape_field)
shape_pupil = na.broadcast_shapes(
shape_wavelength,
shape_field,
shape_pupil,
)
pupil = pupil.broadcast_to(shape_pupil)
if na.unit_normalized(field).is_equivalent(u.deg):
area_field = optika.direction(field).solid_angle_cell(axis_field)
else:
area_field = field.volume_cell(axis_field)
if na.unit_normalized(pupil).is_equivalent(u.deg):
area_pupil = optika.direction(pupil).solid_angle_cell(axis_pupil)
else:
area_pupil = pupil.volume_cell(axis_pupil)
area_field = np.abs(area_field)
area_pupil = np.abs(area_pupil)
area_field = area_field.cell_centers(
axis=axis_wavelength,
)
area_pupil = area_pupil.cell_centers(
axis=(axis_wavelength,) + axis_field,
)
return area_wavelength * area_field * area_pupil
[docs]
@dataclasses.dataclass(eq=False, repr=False)
class ObjectVectorArray(
PupilVectorArray,
SceneVectorArray,
AbstractObjectVectorArray,
):
"""A vector describing a field position, pupil position, and wavelength."""
[docs]
@classmethod
def from_scalar(
cls,
scalar: na.AbstractScalar,
like: None | Self = None,
) -> Self:
if like is not None:
return type(like)(
wavelength=scalar,
field=scalar,
pupil=scalar,
)
else:
return cls(
wavelength=scalar,
field=scalar,
pupil=scalar,
)