From 52488cdbdd7e0289d56f3d8ef92cb3ed48fd555c Mon Sep 17 00:00:00 2001 From: Roy Smart Date: Sat, 30 May 2026 17:51:04 -0600 Subject: [PATCH 1/3] Add `optika.vignetting`, a polynomial model of optical vignetting. Introduce the `optika.vignetting` subpackage, which maps scene coordinates (wavelength + field position) to the fraction of light transmitted by the optical system: - `AbstractVignettingModel` defines the `__call__` interface and an `inverse` (1 / transmission) helper. - `AbstractInterpolatedVignettingModel` adds the calibration-point interface (`coordinates_scene`, `transmission`, axes). - `PolynomialVignettingModel` fits a mean-centered polynomial to the measured transmission, exposed via the public `fit` property, and provides `plot_residual()` to visualize the fit residual vs. field angle with a subplot per wavelength. Register the subpackage in `optika/__init__.py` and add tests. Co-Authored-By: Claude Opus 4.8 --- optika/__init__.py | 2 + optika/vignetting/__init__.py | 13 ++ optika/vignetting/_vignetting.py | 281 ++++++++++++++++++++++++++ optika/vignetting/_vignetting_test.py | 109 ++++++++++ 4 files changed, 405 insertions(+) create mode 100644 optika/vignetting/__init__.py create mode 100644 optika/vignetting/_vignetting.py create mode 100644 optika/vignetting/_vignetting_test.py diff --git a/optika/__init__.py b/optika/__init__.py index bbb8ea4..699bdcf 100644 --- a/optika/__init__.py +++ b/optika/__init__.py @@ -18,6 +18,7 @@ from . import surfaces from . import sensors from . import distortion +from . import vignetting from . import systems __all__ = [ @@ -39,5 +40,6 @@ "surfaces", "sensors", "distortion", + "vignetting", "systems", ] diff --git a/optika/vignetting/__init__.py b/optika/vignetting/__init__.py new file mode 100644 index 0000000..14f6989 --- /dev/null +++ b/optika/vignetting/__init__.py @@ -0,0 +1,13 @@ +"""Model the vignetting of a scene observed by an optical system.""" + +from ._vignetting import ( + AbstractVignettingModel, + AbstractInterpolatedVignettingModel, + PolynomialVignettingModel, +) + +__all__ = [ + "AbstractVignettingModel", + "AbstractInterpolatedVignettingModel", + "PolynomialVignettingModel", +] diff --git a/optika/vignetting/_vignetting.py b/optika/vignetting/_vignetting.py new file mode 100644 index 0000000..5f5c473 --- /dev/null +++ b/optika/vignetting/_vignetting.py @@ -0,0 +1,281 @@ +import abc +import dataclasses +import functools +import matplotlib.axes +import matplotlib.cm +import matplotlib.colors +import matplotlib.figure +import matplotlib.pyplot as plt +import astropy.visualization +import named_arrays as na +import optika + +__all__ = [ + "AbstractVignettingModel", + "AbstractInterpolatedVignettingModel", + "PolynomialVignettingModel", +] + + +@dataclasses.dataclass(eq=False, repr=False) +class AbstractVignettingModel( + optika.mixins.Printable, +): + """ + An interface describing an arbitrary vignetting model, which maps scene + coordinates to the fraction of light transmitted by the optical system. + """ + + @abc.abstractmethod + def __call__( + self, + coordinates: optika.vectors.SceneVectorArray, + ) -> na.AbstractScalar: + """ + Compute the fraction of light transmitted for the given scene + coordinates. + + Parameters + ---------- + coordinates + The wavelength and field position of each point in the scene. + """ + + def inverse( + self, + coordinates: optika.vectors.SceneVectorArray, + ) -> na.AbstractScalar: + r""" + Compute the inverse of the transmission, :math:`1 / T`, the factor + which corrects for the vignetting at the given scene coordinates. + + Parameters + ---------- + coordinates + The wavelength and field position of each point in the scene. + """ + return 1 / self(coordinates) + + +@dataclasses.dataclass(eq=False, repr=False) +class AbstractInterpolatedVignettingModel( + AbstractVignettingModel, +): + """ + A vignetting model defined by interpolating between known scene coordinates + and their measured transmission. + + This class has two main members, :attr:`coordinates_scene` and + :attr:`transmission`, the calibration points between which subclasses + interpolate. + """ + + @property + @abc.abstractmethod + def coordinates_scene(self) -> optika.vectors.SceneVectorArray: + """ + The wavelength and field position of each calibration point in the scene. + """ + + @property + @abc.abstractmethod + def transmission(self) -> na.AbstractScalar: + """ + The fraction of light transmitted at each calibration point. + """ + + @property + @abc.abstractmethod + def axis_wavelength(self) -> str: + """The logical axis corresponding to changing wavelength.""" + + @property + @abc.abstractmethod + def axis_field(self) -> tuple[str, str]: + """The logical axes corresponding to changing position in the scene.""" + + +@dataclasses.dataclass(eq=False, repr=False) +class PolynomialVignettingModel( + AbstractInterpolatedVignettingModel, +): + """ + A vignetting model which fits a polynomial to the measured transmission at + known scene coordinates. + + Examples + -------- + + Plot the fit residual of a vignetting model with a radial transmission + falloff and a deliberately underfit (linear) polynomial. + + .. jupyter-execute:: + + import numpy as np + import astropy.units as u + import named_arrays as na + import optika + + scene = optika.vectors.SceneVectorArray( + wavelength=na.linspace(500, 600, axis="wavelength", num=3) * u.nm, + field=na.Cartesian2dVectorLinearSpace( + start=-1 * u.deg, + stop=+1 * u.deg, + axis=na.Cartesian2dVectorArray("field_x", "field_y"), + num=13, + ), + ) + transmission = 1 - 0.1 * (scene.field.length / u.deg) ** 2 + + model = optika.vignetting.PolynomialVignettingModel( + coordinates_scene=scene, + transmission=transmission, + axis_wavelength="wavelength", + axis_field=("field_x", "field_y"), + degree=1, + ) + + fig, ax = model.plot_residual() + na.plt.set_aspect("equal", ax=ax); + """ + + coordinates_scene: optika.vectors.SceneVectorArray = dataclasses.MISSING + """The wavelength and field position of each calibration point in the scene.""" + + transmission: na.AbstractScalar = dataclasses.MISSING + """The fraction of light transmitted at each calibration point.""" + + axis_wavelength: str = dataclasses.MISSING + """The logical axis corresponding to changing wavelength.""" + + axis_field: tuple[str, str] = dataclasses.MISSING + """The logical axes corresponding to changing position in the scene.""" + + degree: int = 1 + """The degree of the polynomial used to model the vignetting.""" + + where: bool | na.AbstractScalar = True + """A boolean mask selecting which calibration points to use for fitting.""" + + @property + def _axis_scene(self) -> tuple[str, ...]: + """The logical axes over which the calibration points are distributed.""" + return (self.axis_wavelength, *self.axis_field) + + @functools.cached_property + def fit(self) -> na.PolynomialFitFunctionArray: + """The polynomial fit mapping scene coordinates to transmission.""" + scene = self.coordinates_scene + return na.PolynomialFitFunctionArray( + inputs=scene, + outputs=self.transmission, + center=scene.mean(self._axis_scene), + degree=self.degree, + where_polynomial=self.where, + ) + + def __call__( + self, + coordinates: optika.vectors.SceneVectorArray, + ) -> na.AbstractScalar: + return self.fit(coordinates).outputs + + def plot_residual( + self, + figsize: None | tuple[float, float] = None, + cmap: None | str | matplotlib.colors.Colormap = None, + vmin: None | na.ArrayLike = None, + vmax: None | na.ArrayLike = None, + **kwargs, + ) -> tuple[matplotlib.figure.Figure, na.ScalarArray]: + """ + Plot the residual of the :attr:`fit` as a function of field angle, with + a separate subplot for each wavelength. + + The residual is the absolute difference between the calibration + :attr:`transmission` and the transmission predicted by the polynomial + fit. + + Parameters + ---------- + figsize + The size of the returned figure in inches. + If :obj:`None`, the size is chosen automatically from the number + of wavelengths and the aspect ratio of the field of view. + cmap + The colormap used to map the residual to colors. + vmin + The residual value mapped to the lowest color. + If :obj:`None`, defaults to zero. + vmax + The residual value mapped to the highest color. + If :obj:`None`, defaults to the maximum residual. + kwargs + Additional keyword arguments passed to + :func:`named_arrays.plt.pcolormesh`. + """ + scene = self.coordinates_scene + field = scene.field + wavelength = na.as_named_array(scene.wavelength) + axis_wavelength = self.axis_wavelength + + residual = abs(self.transmission - self.fit.predictions) + + if vmin is None: + vmin = 0 + if vmax is None: + vmax = residual.max() + + ncols = na.shape(wavelength).get(axis_wavelength, 1) + + if figsize is None: + # shape each subplot to the field-of-view aspect ratio, and widen + # the figure to fit one subplot per wavelength + height_subplot = 3 + aspect = (field.x.ptp() / field.y.ptp()).ndarray.value + figsize = ( + ncols * height_subplot * aspect + 1.5, + height_subplot + 1, + ) + + with astropy.visualization.quantity_support(): + fig, ax = na.plt.subplots( + axis_cols=axis_wavelength, + ncols=ncols, + sharex=True, + sharey=True, + squeeze=False, + figsize=figsize, + constrained_layout=True, + ) + + colorizer = plt.Colorizer( + cmap=cmap, + norm=plt.Normalize( + vmin=na.as_named_array(vmin).ndarray, + vmax=na.as_named_array(vmax).ndarray, + ), + ) + + na.plt.pcolormesh( + field, + C=residual, + ax=ax, + colorizer=colorizer, + **kwargs, + ) + + na.plt.set_xlabel(f"field $x$ ({na.unit(field.x):latex_inline})", ax=ax) + na.plt.set_ylabel( + f"field $y$ ({na.unit(field.y):latex_inline})", + ax=ax[{axis_wavelength: 0}], + ) + na.plt.set_title(wavelength.to_string_array(), ax=ax) + + plt.colorbar( + mappable=matplotlib.cm.ScalarMappable(colorizer=colorizer), + ax=ax.ndarray, + label="transmission residual", + ) + + return fig, ax diff --git a/optika/vignetting/_vignetting_test.py b/optika/vignetting/_vignetting_test.py new file mode 100644 index 0000000..3bd5546 --- /dev/null +++ b/optika/vignetting/_vignetting_test.py @@ -0,0 +1,109 @@ +import pytest +import numpy as np +import astropy.units as u +import matplotlib.figure +import matplotlib.pyplot as plt +import named_arrays as na +import optika +from .._tests import test_mixins + + +def _scene() -> optika.vectors.SceneVectorArray: + return optika.vectors.SceneVectorArray( + wavelength=na.linspace(500, 600, axis="wavelength", num=3) * u.nm, + field=na.Cartesian2dVectorLinearSpace( + start=-1 * u.deg, + stop=+1 * u.deg, + axis=na.Cartesian2dVectorArray("field_x", "field_y"), + num=5, + ), + ) + + +def _transmission() -> na.AbstractScalar: + return 1 - 0.1 * (_scene().field.length / u.deg) ** 2 + + +class AbstractTestAbstractVignettingModel( + test_mixins.AbstractTestPrintable, +): + def test__call__(self, a: optika.vignetting.AbstractVignettingModel): + scene = _scene() + result = a(scene) + assert isinstance(result, na.AbstractScalar) + for ax in ("field_x", "field_y"): + assert ax in na.shape(result) + + def test_inverse(self, a: optika.vignetting.AbstractVignettingModel): + scene = _scene() + result = a.inverse(scene) + assert isinstance(result, na.AbstractScalar) + assert np.all(result == 1 / a(scene)) + + +class AbstractTestAbstractInterpolatedVignettingModel( + AbstractTestAbstractVignettingModel, +): + def test_coordinates_scene( + self, + a: optika.vignetting.AbstractInterpolatedVignettingModel, + ): + assert isinstance(a.coordinates_scene, optika.vectors.SceneVectorArray) + + def test_transmission( + self, + a: optika.vignetting.AbstractInterpolatedVignettingModel, + ): + assert isinstance(a.transmission, na.AbstractScalar) + + def test_axis_wavelength( + self, + a: optika.vignetting.AbstractInterpolatedVignettingModel, + ): + assert isinstance(a.axis_wavelength, str) + + def test_axis_field( + self, + a: optika.vignetting.AbstractInterpolatedVignettingModel, + ): + assert isinstance(a.axis_field, tuple) + assert all(isinstance(ax, str) for ax in a.axis_field) + + +@pytest.mark.parametrize( + argnames="a", + argvalues=[ + optika.vignetting.PolynomialVignettingModel( + coordinates_scene=_scene(), + transmission=_transmission(), + axis_wavelength="wavelength", + axis_field=("field_x", "field_y"), + degree=degree, + ) + for degree in [1, 2] + ], +) +class TestPolynomialVignettingModel( + AbstractTestAbstractInterpolatedVignettingModel, +): + def test_fit(self, a: optika.vignetting.PolynomialVignettingModel): + assert isinstance(a.fit, na.PolynomialFitFunctionArray) + assert a.fit.degree == a.degree + + @pytest.mark.parametrize( + argnames="kwargs", + argvalues=[ + dict(), + dict(figsize=(8, 4), cmap="viridis", vmin=0, vmax=0.01), + ], + ) + def test_plot_residual( + self, + a: optika.vignetting.PolynomialVignettingModel, + kwargs: dict, + ): + fig, ax = a.plot_residual(**kwargs) + assert isinstance(fig, matplotlib.figure.Figure) + assert isinstance(ax, na.ScalarArray) + assert a.axis_wavelength in na.shape(ax) + plt.close(fig) From 2b548d2933d4eda8781d1a7ed937e92588b4c9de Mon Sep 17 00:00:00 2001 From: Roy Smart Date: Sat, 30 May 2026 18:32:06 -0600 Subject: [PATCH 2/3] Add `PolynomialVignettingModel.plot()` for the transmission. Plot the calibration transmission as a function of field angle (one subplot per wavelength), mirroring `plot_residual`. Factor the shared plotting machinery into a private `_plot` helper used by both methods, and demonstrate `plot()` in the class example. Co-Authored-By: Claude Opus 4.8 --- optika/vignetting/_vignetting.py | 80 ++++++++++++++++++++++++--- optika/vignetting/_vignetting_test.py | 18 ++++++ 2 files changed, 91 insertions(+), 7 deletions(-) diff --git a/optika/vignetting/_vignetting.py b/optika/vignetting/_vignetting.py index 5f5c473..1662af3 100644 --- a/optika/vignetting/_vignetting.py +++ b/optika/vignetting/_vignetting.py @@ -106,8 +106,9 @@ class PolynomialVignettingModel( Examples -------- - Plot the fit residual of a vignetting model with a radial transmission - falloff and a deliberately underfit (linear) polynomial. + Build a vignetting model with a radial transmission falloff fit by a + deliberately underfit (linear) polynomial, then plot the transmission and + the fit residual. .. jupyter-execute:: @@ -135,6 +136,9 @@ class PolynomialVignettingModel( degree=1, ) + fig, ax = model.plot() + na.plt.set_aspect("equal", ax=ax); + fig, ax = model.plot_residual() na.plt.set_aspect("equal", ax=ax); """ @@ -214,17 +218,79 @@ def plot_residual( Additional keyword arguments passed to :func:`named_arrays.plt.pcolormesh`. """ + return self._plot( + abs(self.transmission - self.fit.predictions), + label="transmission residual", + figsize=figsize, + cmap=cmap, + vmin=vmin, + vmax=vmax, + **kwargs, + ) + + def plot( + self, + figsize: None | tuple[float, float] = None, + cmap: None | str | matplotlib.colors.Colormap = None, + vmin: None | na.ArrayLike = None, + vmax: None | na.ArrayLike = None, + **kwargs, + ) -> tuple[matplotlib.figure.Figure, na.ScalarArray]: + """ + Plot the calibration :attr:`transmission` as a function of field angle, + with a separate subplot for each wavelength. + + Parameters + ---------- + figsize + The size of the returned figure in inches. + If :obj:`None`, the size is chosen automatically from the number + of wavelengths and the aspect ratio of the field of view. + cmap + The colormap used to map the transmission to colors. + vmin + The transmission value mapped to the lowest color. + If :obj:`None`, defaults to zero. + vmax + The transmission value mapped to the highest color. + If :obj:`None`, defaults to the maximum transmission. + kwargs + Additional keyword arguments passed to + :func:`named_arrays.plt.pcolormesh`. + """ + return self._plot( + self.transmission, + label="transmission", + figsize=figsize, + cmap=cmap, + vmin=vmin, + vmax=vmax, + **kwargs, + ) + + def _plot( + self, + values: na.AbstractScalar, + label: str, + figsize: None | tuple[float, float] = None, + cmap: None | str | matplotlib.colors.Colormap = None, + vmin: None | na.ArrayLike = None, + vmax: None | na.ArrayLike = None, + **kwargs, + ) -> tuple[matplotlib.figure.Figure, na.ScalarArray]: + """ + Plot a scalar quantity as a function of field angle, with a separate + subplot for each wavelength. + """ scene = self.coordinates_scene field = scene.field wavelength = na.as_named_array(scene.wavelength) axis_wavelength = self.axis_wavelength - residual = abs(self.transmission - self.fit.predictions) - if vmin is None: vmin = 0 if vmax is None: - vmax = residual.max() + vmax = values.max() ncols = na.shape(wavelength).get(axis_wavelength, 1) @@ -259,7 +325,7 @@ def plot_residual( na.plt.pcolormesh( field, - C=residual, + C=values, ax=ax, colorizer=colorizer, **kwargs, @@ -275,7 +341,7 @@ def plot_residual( plt.colorbar( mappable=matplotlib.cm.ScalarMappable(colorizer=colorizer), ax=ax.ndarray, - label="transmission residual", + label=label, ) return fig, ax diff --git a/optika/vignetting/_vignetting_test.py b/optika/vignetting/_vignetting_test.py index 3bd5546..9079d58 100644 --- a/optika/vignetting/_vignetting_test.py +++ b/optika/vignetting/_vignetting_test.py @@ -90,6 +90,24 @@ def test_fit(self, a: optika.vignetting.PolynomialVignettingModel): assert isinstance(a.fit, na.PolynomialFitFunctionArray) assert a.fit.degree == a.degree + @pytest.mark.parametrize( + argnames="kwargs", + argvalues=[ + dict(), + dict(figsize=(8, 4), cmap="viridis", vmin=0, vmax=0.01), + ], + ) + def test_plot( + self, + a: optika.vignetting.PolynomialVignettingModel, + kwargs: dict, + ): + fig, ax = a.plot(**kwargs) + assert isinstance(fig, matplotlib.figure.Figure) + assert isinstance(ax, na.ScalarArray) + assert a.axis_wavelength in na.shape(ax) + plt.close(fig) + @pytest.mark.parametrize( argnames="kwargs", argvalues=[ From d285ab726e42e34d67641737034109271b8b857b Mon Sep 17 00:00:00 2001 From: Roy Smart Date: Sun, 31 May 2026 10:24:23 -0600 Subject: [PATCH 3/3] Retype vignetting models to `SpectralPositionalVectorArray`. Make `AbstractVignettingModel.__call__`/`inverse` accept `na.SpectralPositionalVectorArray` instead of `optika.vectors.SceneVectorArray`, matching the distortion models so scene coordinates share one named-arrays vector type across both. Updates the example, plots, and tests accordingly. Co-Authored-By: Claude Opus 4.8 --- optika/vignetting/_vignetting.py | 34 +++++++++++++-------------- optika/vignetting/_vignetting_test.py | 10 ++++---- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/optika/vignetting/_vignetting.py b/optika/vignetting/_vignetting.py index 1662af3..074f9c2 100644 --- a/optika/vignetting/_vignetting.py +++ b/optika/vignetting/_vignetting.py @@ -29,7 +29,7 @@ class AbstractVignettingModel( @abc.abstractmethod def __call__( self, - coordinates: optika.vectors.SceneVectorArray, + coordinates: na.AbstractSpectralPositionalVectorArray, ) -> na.AbstractScalar: """ Compute the fraction of light transmitted for the given scene @@ -38,12 +38,12 @@ def __call__( Parameters ---------- coordinates - The wavelength and field position of each point in the scene. + The wavelength and position of each point in the scene. """ def inverse( self, - coordinates: optika.vectors.SceneVectorArray, + coordinates: na.AbstractSpectralPositionalVectorArray, ) -> na.AbstractScalar: r""" Compute the inverse of the transmission, :math:`1 / T`, the factor @@ -52,7 +52,7 @@ def inverse( Parameters ---------- coordinates - The wavelength and field position of each point in the scene. + The wavelength and position of each point in the scene. """ return 1 / self(coordinates) @@ -72,9 +72,9 @@ class AbstractInterpolatedVignettingModel( @property @abc.abstractmethod - def coordinates_scene(self) -> optika.vectors.SceneVectorArray: + def coordinates_scene(self) -> na.AbstractSpectralPositionalVectorArray: """ - The wavelength and field position of each calibration point in the scene. + The wavelength and position of each calibration point in the scene. """ @property @@ -117,16 +117,16 @@ class PolynomialVignettingModel( import named_arrays as na import optika - scene = optika.vectors.SceneVectorArray( + scene = na.SpectralPositionalVectorArray( wavelength=na.linspace(500, 600, axis="wavelength", num=3) * u.nm, - field=na.Cartesian2dVectorLinearSpace( + position=na.Cartesian2dVectorLinearSpace( start=-1 * u.deg, stop=+1 * u.deg, axis=na.Cartesian2dVectorArray("field_x", "field_y"), num=13, ), ) - transmission = 1 - 0.1 * (scene.field.length / u.deg) ** 2 + transmission = 1 - 0.1 * (scene.position.length / u.deg) ** 2 model = optika.vignetting.PolynomialVignettingModel( coordinates_scene=scene, @@ -143,8 +143,8 @@ class PolynomialVignettingModel( na.plt.set_aspect("equal", ax=ax); """ - coordinates_scene: optika.vectors.SceneVectorArray = dataclasses.MISSING - """The wavelength and field position of each calibration point in the scene.""" + coordinates_scene: na.AbstractSpectralPositionalVectorArray = dataclasses.MISSING + """The wavelength and position of each calibration point in the scene.""" transmission: na.AbstractScalar = dataclasses.MISSING """The fraction of light transmitted at each calibration point.""" @@ -180,7 +180,7 @@ def fit(self) -> na.PolynomialFitFunctionArray: def __call__( self, - coordinates: optika.vectors.SceneVectorArray, + coordinates: na.AbstractSpectralPositionalVectorArray, ) -> na.AbstractScalar: return self.fit(coordinates).outputs @@ -283,7 +283,7 @@ def _plot( subplot for each wavelength. """ scene = self.coordinates_scene - field = scene.field + position = scene.position wavelength = na.as_named_array(scene.wavelength) axis_wavelength = self.axis_wavelength @@ -298,7 +298,7 @@ def _plot( # shape each subplot to the field-of-view aspect ratio, and widen # the figure to fit one subplot per wavelength height_subplot = 3 - aspect = (field.x.ptp() / field.y.ptp()).ndarray.value + aspect = (position.x.ptp() / position.y.ptp()).ndarray.value figsize = ( ncols * height_subplot * aspect + 1.5, height_subplot + 1, @@ -324,16 +324,16 @@ def _plot( ) na.plt.pcolormesh( - field, + position, C=values, ax=ax, colorizer=colorizer, **kwargs, ) - na.plt.set_xlabel(f"field $x$ ({na.unit(field.x):latex_inline})", ax=ax) + na.plt.set_xlabel(f"field $x$ ({na.unit(position.x):latex_inline})", ax=ax) na.plt.set_ylabel( - f"field $y$ ({na.unit(field.y):latex_inline})", + f"field $y$ ({na.unit(position.y):latex_inline})", ax=ax[{axis_wavelength: 0}], ) na.plt.set_title(wavelength.to_string_array(), ax=ax) diff --git a/optika/vignetting/_vignetting_test.py b/optika/vignetting/_vignetting_test.py index 9079d58..d6618db 100644 --- a/optika/vignetting/_vignetting_test.py +++ b/optika/vignetting/_vignetting_test.py @@ -8,10 +8,10 @@ from .._tests import test_mixins -def _scene() -> optika.vectors.SceneVectorArray: - return optika.vectors.SceneVectorArray( +def _scene() -> na.SpectralPositionalVectorArray: + return na.SpectralPositionalVectorArray( wavelength=na.linspace(500, 600, axis="wavelength", num=3) * u.nm, - field=na.Cartesian2dVectorLinearSpace( + position=na.Cartesian2dVectorLinearSpace( start=-1 * u.deg, stop=+1 * u.deg, axis=na.Cartesian2dVectorArray("field_x", "field_y"), @@ -21,7 +21,7 @@ def _scene() -> optika.vectors.SceneVectorArray: def _transmission() -> na.AbstractScalar: - return 1 - 0.1 * (_scene().field.length / u.deg) ** 2 + return 1 - 0.1 * (_scene().position.length / u.deg) ** 2 class AbstractTestAbstractVignettingModel( @@ -48,7 +48,7 @@ def test_coordinates_scene( self, a: optika.vignetting.AbstractInterpolatedVignettingModel, ): - assert isinstance(a.coordinates_scene, optika.vectors.SceneVectorArray) + assert isinstance(a.coordinates_scene, na.AbstractSpectralPositionalVectorArray) def test_transmission( self,