From fbbb151c1cf75e6a69ef0426bfe9700ba760ae67 Mon Sep 17 00:00:00 2001 From: Roy Smart Date: Sat, 13 Apr 2024 16:01:36 -0600 Subject: [PATCH 01/28] Added `ctis.instruments.Instrument` class. --- ctis/__init__.py | 2 + ctis/instruments/__init__.py | 9 ++++ ctis/instruments/_instruments.py | 75 +++++++++++++++++++++++++++ ctis/instruments/_instruments_test.py | 30 +++++++++++ 4 files changed, 116 insertions(+) create mode 100644 ctis/instruments/__init__.py create mode 100644 ctis/instruments/_instruments.py create mode 100644 ctis/instruments/_instruments_test.py diff --git a/ctis/__init__.py b/ctis/__init__.py index 8107466..fe5117f 100644 --- a/ctis/__init__.py +++ b/ctis/__init__.py @@ -4,7 +4,9 @@ """ from . import scenes +from . import instruments __all__ = [ "scenes", + "instruments", ] diff --git a/ctis/instruments/__init__.py b/ctis/instruments/__init__.py new file mode 100644 index 0000000..6995870 --- /dev/null +++ b/ctis/instruments/__init__.py @@ -0,0 +1,9 @@ +""" +Models of CTIS instruments used during inversions. +""" +from ._instruments import AbstractInstrument, Instrument + +__all__ = [ + "AbstractInstrument", + "Instrument" +] diff --git a/ctis/instruments/_instruments.py b/ctis/instruments/_instruments.py new file mode 100644 index 0000000..6df925d --- /dev/null +++ b/ctis/instruments/_instruments.py @@ -0,0 +1,75 @@ +from typing import Callable +import abc +import dataclasses +import named_arrays as na + +__all__ = [ + "AbstractInstrument", + "Instrument", +] + + +ProjectionCallable = Callable[ + [na.FunctionArray[na.SpectralPositionalVectorArray, na.ScalarArray]], + na.FunctionArray[na.SpectralPositionalVectorArray, na.ScalarArray], +] + + +@dataclasses.dataclass +class AbstractInstrument( + abc.ABC, +): + """ + An interface describing a CTIS instrument. + + This consists of a forward model + (which maps spectral/spatial points on the skyplane to positions on the detector) + and a deprojection model + (which maps positions on the detector to spectral/spatial points on the skyplane). + """ + + @property + @abc.abstractmethod + def project( + self, + ) -> ProjectionCallable: + """ + The forward model of the CTIS instrument. + Maps spectral and spatial coordinates on the field to coordinates + on the detector. + """ + + @property + @abc.abstractmethod + def deproject( + self, + ) -> ProjectionCallable: + """ + The deprojection model of the CTIS instrument. + Maps spectral and spatial coordinates on the detector to coordinates + on the field. + """ + + +@dataclasses.dataclass +class Instrument( + AbstractInstrument, +): + """ + A CTIS instrument where the forward and deprojection models are explicitly + provided. + """ + + project: ProjectionCallable = dataclasses.MISSING + """ + The forward model of the CTIS instrument. + Maps spectral and spatial coordinates on the field to coordinates + on the detector. + """ + + deproject: ProjectionCallable = dataclasses.MISSING + """ + The deprojection model of the CTIS instrument. + Maps spectral and spatial coordinates on the detector to coordinates + on the field. + """ diff --git a/ctis/instruments/_instruments_test.py b/ctis/instruments/_instruments_test.py new file mode 100644 index 0000000..f35717d --- /dev/null +++ b/ctis/instruments/_instruments_test.py @@ -0,0 +1,30 @@ +import pytest +import abc +import ctis + + +class AbstractTestAbstractInstrument( + abc.ABC, +): + def test_project(self, a: ctis.instruments.AbstractInstrument): + result = a.project + assert hasattr(result, "__call__") + + def test_deproject(self, a: ctis.instruments.AbstractInstrument): + result = a.deproject + assert hasattr(result, "__call__") + + +@pytest.mark.parametrize( + argnames="a", + argvalues=[ + ctis.instruments.Instrument( + project=lambda x: x, + deproject=lambda x: x, + ) + ] +) +class TestInstrument( + abc.ABC, +): + pass From 2ef28cf2d90efe3e93bca9800bd5d89fb91afaa2 Mon Sep 17 00:00:00 2001 From: Roy Smart Date: Sat, 13 Apr 2024 16:10:50 -0600 Subject: [PATCH 02/28] Fixed test discovery. --- ctis/instruments/_instruments_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ctis/instruments/_instruments_test.py b/ctis/instruments/_instruments_test.py index f35717d..7c6480d 100644 --- a/ctis/instruments/_instruments_test.py +++ b/ctis/instruments/_instruments_test.py @@ -22,9 +22,9 @@ def test_deproject(self, a: ctis.instruments.AbstractInstrument): project=lambda x: x, deproject=lambda x: x, ) - ] + ], ) class TestInstrument( - abc.ABC, + AbstractTestAbstractInstrument, ): pass From 12b78083a9a0a7f457e68d474a6a60b69fd2f643 Mon Sep 17 00:00:00 2001 From: Roy Smart Date: Sat, 13 Apr 2024 16:18:35 -0600 Subject: [PATCH 03/28] Fixing black errors. --- ctis/instruments/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ctis/instruments/__init__.py b/ctis/instruments/__init__.py index 6995870..cd2312f 100644 --- a/ctis/instruments/__init__.py +++ b/ctis/instruments/__init__.py @@ -1,9 +1,10 @@ """ Models of CTIS instruments used during inversions. """ + from ._instruments import AbstractInstrument, Instrument __all__ = [ "AbstractInstrument", - "Instrument" + "Instrument", ] From 1b02aa35361db520e9b3460dbb9455e2fcfa8224 Mon Sep 17 00:00:00 2001 From: Roy Smart Date: Wed, 13 Nov 2024 12:18:58 -0700 Subject: [PATCH 04/28] old changes --- ctis/instruments/_instruments.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/ctis/instruments/_instruments.py b/ctis/instruments/_instruments.py index 6df925d..7b059ad 100644 --- a/ctis/instruments/_instruments.py +++ b/ctis/instruments/_instruments.py @@ -23,31 +23,41 @@ class AbstractInstrument( An interface describing a CTIS instrument. This consists of a forward model - (which maps spectral/spatial points on the skyplane to positions on the detector) + (which maps the spectral radiance of a physical scene to counts on a detector) and a deprojection model - (which maps positions on the detector to spectral/spatial points on the skyplane). + (which maps detector counts to the spectral radiance of a physical scene). """ - @property @abc.abstractmethod def project( self, - ) -> ProjectionCallable: + scene: na.FunctionArray[na.SpectralPositionalVectorArray, na.AbstractScalar], + ) -> na.FunctionArray[na.SpectralPositionalVectorArray, na.AbstractScalar]: """ The forward model of the CTIS instrument. Maps spectral and spatial coordinates on the field to coordinates on the detector. + + Parameters + ---------- + scene + The spectral radiance of each spatial/spectral point in the scene. """ - @property @abc.abstractmethod def deproject( self, + projections: na.FunctionArray[na.SpectralPositionalVectorArray, na.AbstractScalar], ) -> ProjectionCallable: """ The deprojection model of the CTIS instrument. Maps spectral and spatial coordinates on the detector to coordinates on the field. + + Parameters + ---------- + projections + The counts gathered by each detector in the CTIS instrument. """ From 7b96972644e322e887c8c2327686205c2744f686 Mon Sep 17 00:00:00 2001 From: Roy Smart Date: Mon, 21 Apr 2025 18:06:41 -0600 Subject: [PATCH 05/28] Added ideal instrument --- ctis/instruments/_instruments.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/ctis/instruments/_instruments.py b/ctis/instruments/_instruments.py index 7b059ad..9164596 100644 --- a/ctis/instruments/_instruments.py +++ b/ctis/instruments/_instruments.py @@ -1,6 +1,7 @@ -from typing import Callable +from typing import Callable, Sequence import abc import dataclasses +import astropy.units as u import named_arrays as na __all__ = [ @@ -61,6 +62,22 @@ def deproject( """ +@dataclasses.dataclass +class IdealInstrument( + AbstractInstrument, +): + """ + An idealized CTIS instrument which has a perfect point-spread function + and no noise. + """ + + dispersion: u.Quantity | na.AbstractScalar + r"""The magnitude of the dispersion in :math:`\text{m \AA} \,\text{pix}^-1`""" + + angle: u.Quantity | na.AbstractScalar + """The angle of the dispersion direction with respect to the scene.""" + + @dataclasses.dataclass class Instrument( AbstractInstrument, From aacadd28d511a9966c720324aff505dea2ec13bb Mon Sep 17 00:00:00 2001 From: Roy Smart Date: Mon, 21 Apr 2025 18:12:57 -0600 Subject: [PATCH 06/28] Make `IdealInstrument` public --- ctis/instruments/_instruments.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ctis/instruments/_instruments.py b/ctis/instruments/_instruments.py index 9164596..652481c 100644 --- a/ctis/instruments/_instruments.py +++ b/ctis/instruments/_instruments.py @@ -6,6 +6,7 @@ __all__ = [ "AbstractInstrument", + "IdealInstrument", "Instrument", ] From d1603b7d8734e5703ee18e837357e426f6c27e19 Mon Sep 17 00:00:00 2001 From: Roy Smart Date: Tue, 22 Apr 2025 11:11:26 -0600 Subject: [PATCH 07/28] intermediate commit --- ctis/instruments/_instruments.py | 56 ++++++++++++++++---------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/ctis/instruments/_instruments.py b/ctis/instruments/_instruments.py index 652481c..d42b854 100644 --- a/ctis/instruments/_instruments.py +++ b/ctis/instruments/_instruments.py @@ -7,7 +7,6 @@ __all__ = [ "AbstractInstrument", "IdealInstrument", - "Instrument", ] @@ -22,7 +21,7 @@ class AbstractInstrument( abc.ABC, ): """ - An interface describing a CTIS instrument. + An interface describing a general CTIS instrument. This consists of a forward model (which maps the spectral radiance of a physical scene to counts on a detector) @@ -47,9 +46,9 @@ def project( """ @abc.abstractmethod - def deproject( + def backproject( self, - projections: na.FunctionArray[na.SpectralPositionalVectorArray, na.AbstractScalar], + images: na.FunctionArray[na.SpectralPositionalVectorArray, na.AbstractScalar], ) -> ProjectionCallable: """ The deprojection model of the CTIS instrument. @@ -58,46 +57,47 @@ def deproject( Parameters ---------- - projections - The counts gathered by each detector in the CTIS instrument. + images + The number of electrons gathered by each pixel in every channel. """ @dataclasses.dataclass -class IdealInstrument( +class AbstractLinearInstrument( AbstractInstrument, ): """ - An idealized CTIS instrument which has a perfect point-spread function - and no noise. + An instrument that can be modeled using matrix multiplication. """ - dispersion: u.Quantity | na.AbstractScalar - r"""The magnitude of the dispersion in :math:`\text{m \AA} \,\text{pix}^-1`""" + @property + @abc.abstractmethod + def _weights(self) -> tuple[na.AbstractScalar, dict[str, int], dict[str, int]]: + """ + A sparse matrix which maps spectral radiance on the skyplane to + the number of electrons measured by the sensor. + """ + + def project( + self, + scene: na.FunctionArray[na.SpectralPositionalVectorArray, na.AbstractScalar], + ) -> na.FunctionArray[na.SpectralPositionalVectorArray, na.AbstractScalar]: + + pass - angle: u.Quantity | na.AbstractScalar - """The angle of the dispersion direction with respect to the scene.""" @dataclasses.dataclass -class Instrument( +class IdealInstrument( AbstractInstrument, ): """ - A CTIS instrument where the forward and deprojection models are explicitly - provided. + An idealized CTIS instrument which has a perfect point-spread function + and no noise. """ - project: ProjectionCallable = dataclasses.MISSING - """ - The forward model of the CTIS instrument. - Maps spectral and spatial coordinates on the field to coordinates - on the detector. - """ + dispersion: u.Quantity | na.AbstractScalar + r"""The magnitude of the dispersion in :math:`\text{m \AA} \,\text{pix}^-1`""" - deproject: ProjectionCallable = dataclasses.MISSING - """ - The deprojection model of the CTIS instrument. - Maps spectral and spatial coordinates on the detector to coordinates - on the field. - """ + angle: u.Quantity | na.AbstractScalar + """The angle of the dispersion direction with respect to the scene.""" From ea8bd956b0254b98c893b2eb9eb691e519558e1b Mon Sep 17 00:00:00 2001 From: Roy Smart Date: Tue, 22 Apr 2025 18:51:07 -0600 Subject: [PATCH 08/28] messing with the interface --- ctis/instruments/_instruments.py | 41 +++++++++++++++++--------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/ctis/instruments/_instruments.py b/ctis/instruments/_instruments.py index d42b854..95f548d 100644 --- a/ctis/instruments/_instruments.py +++ b/ctis/instruments/_instruments.py @@ -23,6 +23,9 @@ class AbstractInstrument( """ An interface describing a general CTIS instrument. + The only member of this interface is :meth:`image`, + which represents the forward model of the instrument. + This consists of a forward model (which maps the spectral radiance of a physical scene to counts on a detector) and a deprojection model @@ -30,35 +33,30 @@ class AbstractInstrument( """ @abc.abstractmethod - def project( + def image( self, scene: na.FunctionArray[na.SpectralPositionalVectorArray, na.AbstractScalar], ) -> na.FunctionArray[na.SpectralPositionalVectorArray, na.AbstractScalar]: - """ - The forward model of the CTIS instrument. - Maps spectral and spatial coordinates on the field to coordinates - on the detector. - + f""" + The forward model of this CTIS instrument, which maps spectral radiance + on the skyplane to counts on the detectors. + Parameters ---------- scene - The spectral radiance of each spatial/spectral point in the scene. + The spectral radiance in units equivalent to + {(u.erg / (u.cm**2 * u.sr * u.AA * u.s)):latex_inline}. """ + @property @abc.abstractmethod - def backproject( - self, - images: na.FunctionArray[na.SpectralPositionalVectorArray, na.AbstractScalar], - ) -> ProjectionCallable: + def coordinates_scene(self) -> na.AbstractSpectralPositionalVectorArray: """ - The deprojection model of the CTIS instrument. - Maps spectral and spatial coordinates on the detector to coordinates - on the field. + A grid of wavelength and position coordinates on the skyplane + which will be used to construct the inverted scene. - Parameters - ---------- - images - The number of electrons gathered by each pixel in every channel. + Normally the pitch of this grid is chosen to be the average + plate scale of the instrument. """ @@ -78,6 +76,12 @@ def _weights(self) -> tuple[na.AbstractScalar, dict[str, int], dict[str, int]]: the number of electrons measured by the sensor. """ + def image( + self, + scene: na.FunctionArray[na.SpectralPositionalVectorArray, na.AbstractScalar], + ) -> na.FunctionArray[na.SpectralPositionalVectorArray, na.AbstractScalar]: + pass + def project( self, scene: na.FunctionArray[na.SpectralPositionalVectorArray, na.AbstractScalar], @@ -86,7 +90,6 @@ def project( pass - @dataclasses.dataclass class IdealInstrument( AbstractInstrument, From 803ede57e4f5321fe926e9d5e5ef5df77c9f99ff Mon Sep 17 00:00:00 2001 From: Roy Smart Date: Tue, 22 Apr 2025 23:12:39 -0600 Subject: [PATCH 09/28] imports --- ctis/instruments/__init__.py | 9 +++++++-- ctis/instruments/_instruments.py | 1 + 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/ctis/instruments/__init__.py b/ctis/instruments/__init__.py index cd2312f..bcc5400 100644 --- a/ctis/instruments/__init__.py +++ b/ctis/instruments/__init__.py @@ -2,9 +2,14 @@ Models of CTIS instruments used during inversions. """ -from ._instruments import AbstractInstrument, Instrument +from ._instruments import ( + AbstractInstrument, + AbstractLinearInstrument, + IdealInstrument, +) __all__ = [ "AbstractInstrument", - "Instrument", + "AbstractLinearInstrument", + "IdealInstrument", ] diff --git a/ctis/instruments/_instruments.py b/ctis/instruments/_instruments.py index 95f548d..92ad102 100644 --- a/ctis/instruments/_instruments.py +++ b/ctis/instruments/_instruments.py @@ -6,6 +6,7 @@ __all__ = [ "AbstractInstrument", + "AbstractLinearInstrument", "IdealInstrument", ] From 814d1e47fd1c8af96b7a51db7a912f8f5293b289 Mon Sep 17 00:00:00 2001 From: Roy Smart Date: Fri, 3 Apr 2026 13:35:37 -0600 Subject: [PATCH 10/28] wip --- ctis/instruments/_instruments.py | 58 ++++++++++++++++++++++++++------ 1 file changed, 47 insertions(+), 11 deletions(-) diff --git a/ctis/instruments/_instruments.py b/ctis/instruments/_instruments.py index 92ad102..ad75e40 100644 --- a/ctis/instruments/_instruments.py +++ b/ctis/instruments/_instruments.py @@ -24,13 +24,13 @@ class AbstractInstrument( """ An interface describing a general CTIS instrument. - The only member of this interface is :meth:`image`, - which represents the forward model of the instrument. + The most important method of this interface is :meth:`image`, + which represents the forward model of the instrument and maps the + spectral radiance of the skyplane to detector counts. - This consists of a forward model - (which maps the spectral radiance of a physical scene to counts on a detector) - and a deprojection model - (which maps detector counts to the spectral radiance of a physical scene). + The other important method of this interface is :meth:`deproject`, + which is the transpose of image and maps detector counts from an + observed image to the corresponding spectral radiance on the skyplane. """ @abc.abstractmethod @@ -38,7 +38,7 @@ def image( self, scene: na.FunctionArray[na.SpectralPositionalVectorArray, na.AbstractScalar], ) -> na.FunctionArray[na.SpectralPositionalVectorArray, na.AbstractScalar]: - f""" + r""" The forward model of this CTIS instrument, which maps spectral radiance on the skyplane to counts on the detectors. @@ -46,9 +46,31 @@ def image( ---------- scene The spectral radiance in units equivalent to + erg / cm^2 / sr^2 / nm / s. + :math:`\unit{\erg\per\cm\squared\per\steradian\per\nm\per\second}` {(u.erg / (u.cm**2 * u.sr * u.AA * u.s)):latex_inline}. """ + @abc.abstractmethod + def deproject( + self, + image: na.FunctionArray[na.SpectralPositionalVectorArray, na.AbstractScalar], + ) -> na.FunctionArray[na.SpectralPositionalVectorArray, na.AbstractScalar]: + """ + A quasi-inverse model of this CTIS instrument, which maps counts + on the detectors to spectral radiance on the skyplane. + + This is not a true inverse, since this just spreads intensity out + evenly along each projection direction, and doesn't concentrate it + in its true location. + + Parameters + ---------- + image + A series of images captured by a CTIS instrument. + Should be in units of electrons. + """ + @property @abc.abstractmethod def coordinates_scene(self) -> na.AbstractSpectralPositionalVectorArray: @@ -60,21 +82,29 @@ def coordinates_scene(self) -> na.AbstractSpectralPositionalVectorArray: plate scale of the instrument. """ + @property + @abc.abstractmethod + def coordinates_sensor(self) -> na.AbstractSpectralPositionalVectorArray: + """ + A grid of wavelength and position coordinates on the detector plane. + """ + @dataclasses.dataclass class AbstractLinearInstrument( AbstractInstrument, ): """ - An instrument that can be modeled using matrix multiplication. + An instrument where the forward model can be represented using + matrix multiplication. """ @property @abc.abstractmethod - def _weights(self) -> tuple[na.AbstractScalar, dict[str, int], dict[str, int]]: + def weights(self) -> tuple[na.AbstractScalar, dict[str, int], dict[str, int]]: """ - A sparse matrix which maps spectral radiance on the skyplane to - the number of electrons measured by the sensor. + The contribution of each voxel on the skyplane to each pixel on the + detector. """ def image( @@ -100,6 +130,12 @@ class IdealInstrument( and no noise. """ + response: u.Quantity | na.AbstractScalar + """The number of electrons measured for a given spectral radiance on the skyplane.""" + + plate_scale: u.Quantity | na.AbstractScalar + r"""The spatial scale of the image on the sensor in :math:`\text{arcsec} \,\text{pix}^-1`""" + dispersion: u.Quantity | na.AbstractScalar r"""The magnitude of the dispersion in :math:`\text{m \AA} \,\text{pix}^-1`""" From c2c5573f028e10153a9fab6d8a3dc3e263a6a87b Mon Sep 17 00:00:00 2001 From: Roy Smart Date: Fri, 3 Apr 2026 15:51:40 -0600 Subject: [PATCH 11/28] added distortion --- ctis/instruments/_instruments.py | 101 ++++++++++++++++++++++++++++--- 1 file changed, 92 insertions(+), 9 deletions(-) diff --git a/ctis/instruments/_instruments.py b/ctis/instruments/_instruments.py index ad75e40..5e1ec38 100644 --- a/ctis/instruments/_instruments.py +++ b/ctis/instruments/_instruments.py @@ -45,10 +45,8 @@ def image( Parameters ---------- scene - The spectral radiance in units equivalent to - erg / cm^2 / sr^2 / nm / s. - :math:`\unit{\erg\per\cm\squared\per\steradian\per\nm\per\second}` - {(u.erg / (u.cm**2 * u.sr * u.AA * u.s)):latex_inline}. + The spectral radiance in units equivalent to + :math:`\text{erg} \, \text{cm}^{-2} \, \text{sr}^{-1} \, \AA^{-1} \, \text{s}^{-1}`. """ @abc.abstractmethod @@ -107,23 +105,44 @@ def weights(self) -> tuple[na.AbstractScalar, dict[str, int], dict[str, int]]: detector. """ + @property + @abc.abstractmethod + def weights_transpose(self): + """ + The contribution of each pixel on the detector to each voxel on the + skyplane. + """ + def image( self, scene: na.FunctionArray[na.SpectralPositionalVectorArray, na.AbstractScalar], ) -> na.FunctionArray[na.SpectralPositionalVectorArray, na.AbstractScalar]: - pass - def project( + return na.FunctionArray( + inputs=self.coordinates_scene, + outputs=na.regridding.regrid_from_weights( + *self.weights, + values_input=scene.outputs, + ) + ) + + def backproject( self, - scene: na.FunctionArray[na.SpectralPositionalVectorArray, na.AbstractScalar], + image: na.FunctionArray[na.SpectralPositionalVectorArray, na.AbstractScalar], ) -> na.FunctionArray[na.SpectralPositionalVectorArray, na.AbstractScalar]: - pass + return na.FunctionArray( + inputs=self.coordinates_sensor, + outputs=na.regridding.regrid_from_weights( + *self.weights_transpose, + values_input=image.outputs, + ) + ) @dataclasses.dataclass class IdealInstrument( - AbstractInstrument, + AbstractLinearInstrument, ): """ An idealized CTIS instrument which has a perfect point-spread function @@ -141,3 +160,67 @@ class IdealInstrument( angle: u.Quantity | na.AbstractScalar """The angle of the dispersion direction with respect to the scene.""" + + wavelength_ref: u.Quantity | na.AbstractScalar + """ + The reference wavelength at which the center of the FOV lands at :attr:`position_ref` + """ + + position_ref: u.Quantity | na.AbstractScalar + """ + The position where the reference wavelength is designed to land. + """ + + coordinates_scene: na.AbstractSpectralPositionalVectorArray + """ + A grid of wavelength and position coordinates on the skyplane + which will be used to construct the inverted scene. + + Normally the pitch of this grid is chosen to be the average + plate scale of the instrument. + """ + + coordinates_sensor: na.AbstractSpectralPositionalVectorArray + """ + A grid of wavelength and position coordinates on the detector plane. + """ + + def distortion(self, coordinates: na.SpectralPositionalVectorArray): + delta_lambda = self.plate_scale / self.dispersion + rot = na.Cartesian2dRotationMatrixArray(self.angle) + rot_grid = na.SpectralPositionalVectorArray( + wavelength=coordinates.wavelength - self.wavelength_ref, + position=rot @ coordinates.position + ) + disperse = na.SpectralPositionalMatrixArray( + wavelength=na.SpectralPositionalVectorArray( + wavelength=1, + position=na.Cartesian2dVectorArray( + x=0 * u.angstrom / u.arcsec, + y=0 * u.angstrom / u.arcsec, + ), + ), + position=na.Cartesian2dMatrixArray( + x=na.SpectralPositionalVectorArray( + wavelength=1 / delta_lambda, + position=na.Cartesian2dVectorArray( + # originally I had this as x = -1 which resulted in the grid not being in ascending order. This caused the interpolator to puke. + x=1, + y=0, + ), + ), + y=na.SpectralPositionalVectorArray( + wavelength=0 * u.arcsec / u.angstrom, + position=na.Cartesian2dVectorArray( + x=0, + y=1, + ), + ), + ), + ) + projected_grid = disperse @ rot_grid + # projected_grid = projected_grid - self.ref_position + return na.SpectralPositionalVectorArray( + wavelength=coordinates.wavelength, + position=projected_grid.position + self.position_ref, + ) From faf68143ba991d45c3ea24ad376a586a6dcb6277 Mon Sep 17 00:00:00 2001 From: Roy Smart Date: Fri, 3 Apr 2026 16:04:54 -0600 Subject: [PATCH 12/28] added weights --- ctis/instruments/_instruments.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/ctis/instruments/_instruments.py b/ctis/instruments/_instruments.py index 5e1ec38..480a97b 100644 --- a/ctis/instruments/_instruments.py +++ b/ctis/instruments/_instruments.py @@ -1,5 +1,6 @@ from typing import Callable, Sequence import abc +import functools import dataclasses import astropy.units as u import named_arrays as na @@ -185,6 +186,18 @@ class IdealInstrument( A grid of wavelength and position coordinates on the detector plane. """ + axis_scene_xy: tuple[str, str] + """ + The logical axes in :attr:`coordinates_scene` corresponding to changing + spatial coordinates. + """ + + axis_sensor_xy: tuple[str, str] + """ + The logical axes in :attr:`coordinates_sensor` corresponding to changing + spatial coordinates. + """ + def distortion(self, coordinates: na.SpectralPositionalVectorArray): delta_lambda = self.plate_scale / self.dispersion rot = na.Cartesian2dRotationMatrixArray(self.angle) @@ -224,3 +237,16 @@ def distortion(self, coordinates: na.SpectralPositionalVectorArray): wavelength=coordinates.wavelength, position=projected_grid.position + self.position_ref, ) + + @functools.cached_property + def weights(self) -> tuple[na.AbstractScalar, dict[str, int], dict[str, int]]: + return na.regridding.weights( + coordinates_input=self.distortion(self.coordinates_scene), + coordinates_output=self.coordinates_sensor, + axis_input=self.axis_scene_xy, + axis_output=self.axis_sensor_xy, + method="conservative", + ) + + def weights_transpose(self): + raise NotImplementedError From 491cdf63f9dab05225e2ec3556d7eb447598e830 Mon Sep 17 00:00:00 2001 From: Roy Smart Date: Sat, 4 Apr 2026 12:16:31 -0600 Subject: [PATCH 13/28] fixes --- ctis/instruments/_instruments.py | 34 +++++++++++++++++--------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/ctis/instruments/_instruments.py b/ctis/instruments/_instruments.py index 480a97b..c064655 100644 --- a/ctis/instruments/_instruments.py +++ b/ctis/instruments/_instruments.py @@ -51,7 +51,7 @@ def image( """ @abc.abstractmethod - def deproject( + def backproject( self, image: na.FunctionArray[na.SpectralPositionalVectorArray, na.AbstractScalar], ) -> na.FunctionArray[na.SpectralPositionalVectorArray, na.AbstractScalar]: @@ -172,7 +172,7 @@ class IdealInstrument( The position where the reference wavelength is designed to land. """ - coordinates_scene: na.AbstractSpectralPositionalVectorArray + coordinates_scene: na.AbstractSpectralPositionalVectorArray = dataclasses.MISSING """ A grid of wavelength and position coordinates on the skyplane which will be used to construct the inverted scene. @@ -181,7 +181,7 @@ class IdealInstrument( plate scale of the instrument. """ - coordinates_sensor: na.AbstractSpectralPositionalVectorArray + coordinates_sensor: na.AbstractSpectralPositionalVectorArray = dataclasses.MISSING """ A grid of wavelength and position coordinates on the detector plane. """ @@ -199,7 +199,7 @@ class IdealInstrument( """ def distortion(self, coordinates: na.SpectralPositionalVectorArray): - delta_lambda = self.plate_scale / self.dispersion + unit_wavelength = coordinates.wavelength.unit rot = na.Cartesian2dRotationMatrixArray(self.angle) rot_grid = na.SpectralPositionalVectorArray( wavelength=coordinates.wavelength - self.wavelength_ref, @@ -209,30 +209,28 @@ def distortion(self, coordinates: na.SpectralPositionalVectorArray): wavelength=na.SpectralPositionalVectorArray( wavelength=1, position=na.Cartesian2dVectorArray( - x=0 * u.angstrom / u.arcsec, - y=0 * u.angstrom / u.arcsec, + x=0 * unit_wavelength / u.arcsec, + y=0 * unit_wavelength / u.arcsec, ), ), position=na.Cartesian2dMatrixArray( x=na.SpectralPositionalVectorArray( - wavelength=1 / delta_lambda, + wavelength=1 / self.dispersion, position=na.Cartesian2dVectorArray( - # originally I had this as x = -1 which resulted in the grid not being in ascending order. This caused the interpolator to puke. - x=1, - y=0, + x=1 / self.plate_scale, + y=0 * u.pix / u.arcsec, ), ), y=na.SpectralPositionalVectorArray( - wavelength=0 * u.arcsec / u.angstrom, + wavelength=0 * u.pix / unit_wavelength, position=na.Cartesian2dVectorArray( - x=0, - y=1, + x=0 * u.pix / u.arcsec, + y=1 / self.plate_scale, ), ), ), ) projected_grid = disperse @ rot_grid - # projected_grid = projected_grid - self.ref_position return na.SpectralPositionalVectorArray( wavelength=coordinates.wavelength, position=projected_grid.position + self.position_ref, @@ -240,9 +238,13 @@ def distortion(self, coordinates: na.SpectralPositionalVectorArray): @functools.cached_property def weights(self) -> tuple[na.AbstractScalar, dict[str, int], dict[str, int]]: + + coordinates_input = self.distortion(self.coordinates_scene) + coordinates_output = self.coordinates_sensor + return na.regridding.weights( - coordinates_input=self.distortion(self.coordinates_scene), - coordinates_output=self.coordinates_sensor, + coordinates_input=coordinates_input.position, + coordinates_output=coordinates_output.position, axis_input=self.axis_scene_xy, axis_output=self.axis_sensor_xy, method="conservative", From 8808a43ab1588765c4ba2ede852d335723424b1b Mon Sep 17 00:00:00 2001 From: Roy Smart Date: Sat, 4 Apr 2026 12:29:15 -0600 Subject: [PATCH 14/28] bLack --- ctis/instruments/_instruments.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ctis/instruments/_instruments.py b/ctis/instruments/_instruments.py index c064655..89c6620 100644 --- a/ctis/instruments/_instruments.py +++ b/ctis/instruments/_instruments.py @@ -42,7 +42,7 @@ def image( r""" The forward model of this CTIS instrument, which maps spectral radiance on the skyplane to counts on the detectors. - + Parameters ---------- scene @@ -124,7 +124,7 @@ def image( outputs=na.regridding.regrid_from_weights( *self.weights, values_input=scene.outputs, - ) + ), ) def backproject( @@ -137,7 +137,7 @@ def backproject( outputs=na.regridding.regrid_from_weights( *self.weights_transpose, values_input=image.outputs, - ) + ), ) @@ -203,7 +203,7 @@ def distortion(self, coordinates: na.SpectralPositionalVectorArray): rot = na.Cartesian2dRotationMatrixArray(self.angle) rot_grid = na.SpectralPositionalVectorArray( wavelength=coordinates.wavelength - self.wavelength_ref, - position=rot @ coordinates.position + position=rot @ coordinates.position, ) disperse = na.SpectralPositionalMatrixArray( wavelength=na.SpectralPositionalVectorArray( From 82de7e8757b50eb362e07d3b8b4e8edf092a217d Mon Sep 17 00:00:00 2001 From: Roy Smart Date: Sat, 4 Apr 2026 12:31:31 -0600 Subject: [PATCH 15/28] ruff --- ctis/instruments/_instruments.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ctis/instruments/_instruments.py b/ctis/instruments/_instruments.py index 89c6620..9349353 100644 --- a/ctis/instruments/_instruments.py +++ b/ctis/instruments/_instruments.py @@ -1,4 +1,4 @@ -from typing import Callable, Sequence +from typing import Callable import abc import functools import dataclasses From 2ed7daf9df4577f7e0e2846a7ba70d662122103c Mon Sep 17 00:00:00 2001 From: Roy Smart Date: Sat, 4 Apr 2026 12:39:43 -0600 Subject: [PATCH 16/28] added tutorial --- .github/workflows/notebooks.yml | 15 ++ docs/index.rst | 9 + docs/tutorials/ideal-instrument.ipynb | 336 ++++++++++++++++++++++++++ 3 files changed, 360 insertions(+) create mode 100644 .github/workflows/notebooks.yml create mode 100644 docs/tutorials/ideal-instrument.ipynb diff --git a/.github/workflows/notebooks.yml b/.github/workflows/notebooks.yml new file mode 100644 index 0000000..29f1fec --- /dev/null +++ b/.github/workflows/notebooks.yml @@ -0,0 +1,15 @@ +name: Notebooks + +on: + push: + branches: + - main + pull_request: + +jobs: + nbstripout: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: pip install nbstripout + - run: find . -name "*.ipynb" -exec nbstripout --verify {} + diff --git a/docs/index.rst b/docs/index.rst index 26c1e22..e6f55f6 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -17,6 +17,15 @@ Some explanations of the theory behind inversion discussions/richardson-lucy-analogy/richardson-lucy-analogy +Tutorials +========= + +Examples on how to use this package. + +.. toctree:: + :maxdepth: 1 + + tutorials/ideal-instrument API Reference ============= diff --git a/docs/tutorials/ideal-instrument.ipynb b/docs/tutorials/ideal-instrument.ipynb new file mode 100644 index 0000000..97f46bc --- /dev/null +++ b/docs/tutorials/ideal-instrument.ipynb @@ -0,0 +1,336 @@ +{ + "cells": [ + { + "cell_type": "raw", + "id": "aea6baeb-f5c9-4353-84bb-9a5014c93e1d", + "metadata": { + "editable": true, + "raw_mimetype": "text/x-rst", + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Defining a Simple CTIS Instrument\n", + "---------------------------------\n", + "Here, we construct an idealized example of a CTIS instrument,\n", + "defined by a plate scale, dispersion, and angle of disperesion.\n", + "This instrument has no point-spread function, distortion, or vignetting." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "initial_id", + "metadata": { + "ExecuteTime": { + "end_time": "2026-04-03T22:06:30.387380200Z", + "start_time": "2026-04-03T22:06:25.022691200Z" + }, + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "import astropy.units as u\n", + "import astropy.visualization\n", + "import named_arrays as na\n", + "import ctis" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "30daf494-91a5-4c14-9a7d-37e7ce757757", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "wavelength_center = 171 * u.AA" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "f8edf09f-ee49-4d66-ab58-df4d0e642fca", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "wavelength = na.linspace(-500, 500, axis=\"wavelength\", num=21) * u.km / u.s" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "76a24148-01a8-47ab-a423-dad8a6fc8550", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "position_scene = na.Cartesian2dVectorLinearSpace(\n", + " start=-10 * u.arcsec,\n", + " stop=10 * u.arcsec,\n", + " axis=na.Cartesian2dVectorArray(\"fx\", \"fy\"),\n", + " num=128,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "37b5250a-11a0-4eb2-b784-2c221f2c9e24", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "position_sensor = na.Cartesian2dVectorArray(\n", + " x=na.arange(0, 256, axis=\"sx\") * u.pix,\n", + " y=na.arange(0, 128, axis=\"sy\") * u.pix,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "b051dfcf-7a27-42af-8173-c318e06b2c25", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "coordinates_scene = na.SpectralPositionalVectorArray(wavelength, position_scene)\n", + "coordinates_sensor = na.SpectralPositionalVectorArray(wavelength, position_sensor)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "7a531ee6fdccb5e9", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "instrument = ctis.instruments.IdealInstrument(\n", + " response=1,\n", + " plate_scale=1 * u.arcsec / u.pix,\n", + " dispersion=10 * u.km / u.s / u.pix,\n", + " angle=0*u.deg,\n", + " wavelength_ref=0 * u.km / u.s,\n", + " position_ref=128 * u.pix,\n", + " coordinates_scene=coordinates_scene,\n", + " coordinates_sensor=coordinates_sensor,\n", + " axis_scene_xy=(\"fx\", \"fy\"),\n", + " axis_sensor_xy=(\"sx\", \"sy\"),\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "c9e75ff0-1d86-425b-a9c6-af33dfc888e4", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "scene = ctis.scenes.gaussians(\n", + " inputs=coordinates_scene,\n", + " width=na.SpectralPositionalVectorArray(30 * u.km / u.s, 1 * u.arcsec),\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "0812c400-02d6-4682-a21b-2c32780fbcba", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAosAAAHrCAYAAACn9tfQAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjcsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvTLEjVAAAAAlwSFlzAAAPYQAAD2EBqD+naQAAdVFJREFUeJzt3Qu8XNPd+P/vzJxzkiAJIRKXxL2iTdzilrR1qTxC9aJVT4vHrSmlKKJKWnVtf2m1RbWq1b9G+6hSfdDyoIKibYIK6lI8KIIIiiTkci4z+/9aO5mT71oz33X2uc+ZfN5e4+z77Nkzk7PO/n7Xd+WSJEkEAAAAqCJfbSEAAADg0FgEAACAicYiAAAATDQWAQAAYKKxCAAAABONRQAAAJhoLAIAAMBEYxEAAAAmGosAAAAw0VgEAABAfTQW77//fvnkJz8pG2+8seRyObn55pu99W7kwnPOOUc22mgjGTJkiEyZMkWee+65Do97+eWXy+abby6DBw+W3XffXR566KFefBUAAAADx4BqLC5dulR22GGHtHFXzUUXXSSXXXaZ/OxnP5MHH3xQ1l57bZk6daqsWLHCPOb1118v06dPl3PPPVceeeSR9PhunzfffLMXXwkAAMDAkEvc7bgByN1ZvOmmm+Sggw5K593LcHccTz/9dPna176WLlu8eLGMGjVKrr76avnCF75Q9TjuTuKuu+4qP/nJT9L5UqkkY8aMkZNPPlnOOuusPnxFAAAAtadB6sSLL74oCxcuTEPPZcOHD08bg3Pnzq3aWGxpaZF58+bJjBkz2pfl8/n0GG4fS3Nzc/oocw3Md955R9Zff/20EQsAQG9yN0jee++99CaJ+73V29zvuQULFsjQoUMH1O+5vr5O9apuGouuoei4O4mamy+vC/373/+WYrFYdZ9nnnnGfK6ZM2fK+eef3yPnDQBAV73yyiuy6aab9vrzuIaii7oNVH11neoVzewucHciXYi7/Jg/f35/nxIAYA3k7vTV0/P0loF+/v2tbu4sjh49Ov35xhtvpL2hy9z8jjvuWHWfDTbYQAqFQrqN5ubLx6tm0KBB6QMAgP7UVyHhgRR6rsfz7291c2dxiy22SBt4d999d/uyJUuWpL2iJ02aVHWfpqYmmThxorePy8tw89Y+AAAAa5IBdWfx/fffl+eff97r1PLYY4/JiBEjZOzYsXLqqafKt7/9bdlmm23SxuO3vvWtNKm13GPa2XfffeUzn/mMnHTSSem8K5tz1FFHyS677CK77babXHrppWmJnmOOOaZfXiMAAEAtGVCNxYcfflj22Wef9nnX0HNcY8+Vx/n617+eNvSOO+44WbRokXzkIx+RO+64Iy22XfbCCy+kHVvKPv/5z8tbb72VFvN2HWFcyNrtE3Z6AQAAWBMN2DqLtcSFu12ZHgAA+pLrZDls2LBef56B/nuur65TvaqbnEUAAAD0PBqLAAAAMNFYBAAAgInGIgAAAEw0FgEAAGCisQgAAAATjUUAAACYaCwCAADARGMRAAAAJhqLAAAAMNFYBAAAgInGIgAAAEw0FgEAAGCisQgAAABTg70KAADAl1OP8rz0wDL9s7PrrOmSiPy7k68PlbizCAAAABONRQAAAJhoLAIAAMBEYxEAAAAmGosAAAAw0VgEAACAicYiAAAATDQWAQAAYKKxCAAAABONRQAAAJhoLAIAAMBEYxEAAAAmGosAAAAw0VgEAACAicYiAAAATDQWAQAAYKKxCAAAEPjud78ruVxOTj311PZlK1askBNPPFHWX399WWeddeTggw+WN954w9tv/vz5cuCBB8paa60lG264oZxxxhnS1tYmAxmNRQAAAOXvf/+7/PznP5ftt9/eW37aaafJLbfcIjfccIPcd999smDBAvnsZz/bvr5YLKYNxZaWFpkzZ4786le/kquvvlrOOeccGchoLAIAgLq2ZMkS79Hc3Gxu+/7778vhhx8uv/jFL2S99dZrX7548WK56qqr5OKLL5aPfexjMnHiRJk1a1baKHzggQfSbe6880755z//Kddcc43suOOOcsABB8iFF14ol19+edqAHKhoLAIAgEwKItIoIk0iMkRE1l71GCoiw0RkuIisKyKuiTVi1WN9Edlg1WOkemy46jFq1WO0emy06rFx8NhEPTZVjzHqMVb9dA9nzJgxMnz48PbHzJkzzdfowszu7uCUKVO85fPmzZPW1lZv+bhx42Ts2LEyd+7cdN79nDBhgowa5V7RSlOnTk0bqE899ZQMVA39fQJAX8t1eWUnJT26GQCgi1555RUZNsw1Z1caNGhQ1e2uu+46eeSRR9IwdGjhwoXS1NQk667rmsOruYahW1feRjcUy+vL6wYqGosAAKCuuYaibixaDcpTTjlFZs+eLYMH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ryFLSLkr+0d3ig49o1bp0LMOV+tDRS6wNfJNj8gYHq4oIVRtedL9MDKhZ6CmcWcRAAAAJhqLAAAAMBGGBuq9p3S1bbIerCLcbRy8K1HSjNHcqCz7WaHUiv2zvoiMJ5tlVJ3w3KxwdfTglq6MlpMxZk+oeY1VkvdEZLkk0iAlaZBces8pn/70H+4zuvJn5fzKZe5zvHL56qV62cpp9//yfa3KbVZ+E6ovc/+VpNiPV6t+cGcRAAAAJhqLAAAAMBGGBupN0oXIZVdC1Fn3725YvLsRy+6+tq6+oKQ337xeuihd6T2feTNCz8BAxZ1FAAAAmGgsAgAAwERjEQAAAPXfWLz33nsll8tVffz9738399t7770rtj/++OP79NyBPpFEHj1xjFp99PhrS4xH1ueNHDzp50fWCxndrLtvBIBaUzcdXCZPniyvv/66t+xb3/qW3H333bLLLrtE9z322GPlggsuaJ9fa621eu08AQAABpK6aSw2NTXJ6NGj2+dbW1vlD3/4g5x88snp3cIY1zjU+3akubk5fZQtWbKki2cNAABQ2+omDB364x//KG+//bYcc8wxHW77m9/8RjbYYAMZP368zJgxQ5YtWxbdfubMmTJ8+PD2x5gxY3rwzIF+0JuRw74KI3f3+Xv6ibKGq7sbuu7Ji5o1/N67FxJAjcklSXR8pwHr4x//ePrztttui2535ZVXymabbSYbb7yxPP7443LmmWfKbrvtJjfeeGOn7izSYERd6sowfrUmGUAXtb+vd4cNVtSaxYsXy7Bhw3r9edzvOXdzxAUk8+kwf+VH7Q/3t0D+2mfXqV7VfBj6rLPOku9973vRbZ5++mkZN25c+/yrr74qf/rTn+R3v/tdh8c/7rjj2qcnTJggG220key7777ywgsvyFZbbVV1n0GDBqUPAACAelfzjcXTTz9djj766Og2W265pTc/a9YsWX/99eVTn/pUp59v9913T38+//zzZmMRWGP0xM2k7t4tq7sbWh2EgXvtlmPdXUgAfaTmG4sjR45MH1m5qLprLB555JHS2NjY6ed77LHH0p/uDiMAAMCaru46uNxzzz3y4osvype+9KWKda+99loarn7ooYfSeRdqvvDCC2XevHny0ksvpZ1iXCNzzz33lO23374fzh4AAKC21Pydxc666qqr0pqLOodRl9N59tln23s7u3I7d911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btppxXHL99lnH3nxxRdl8803T5e5kLNrIN5yyy1pTsbBBx8sl112mayzzjp9/AoBAABqz4BpLAIAAKDvDZgwNAAAAPoejUUAAACYaCwCAADARGMRAAAAJhqLGbj6jZMnT5a11lorHUu6GlfS58ADD0y3cXUezzjjjLTAd4zriX344YentSDdcadNm9ZeCqivuB7iuVyu6uPvf/+7uZ/rgR5uf/zxx0stcD3dw3NzveJjVqxYkZZZWn/99dOe8K5XfLmoe3966aWX0s/FFltsIUOGDJGtttoqLTHR0tIS3a+W3p/LL788fU8GDx6c1i8Ni+eHbrjhhrTEldvejfd+2223SS1wY8LvuuuuMnTo0PQ7ftBBB8mzzz4b3efqq6+ueB/c66oF5513XsW5dTRaVq2+N9W+8+7hvtMD4X25//775ZOf/GRass2dy8033+ytd/1QzznnHNloo43SfwemTJkizz33XI9/97pzrCzfjyz/LmX5Xep+b+28885pqZ2tt946fT/Ry1xvaMSdc845ycUXX5xMnz49GT58eMX6tra2ZPz48cmUKVOSRx99NLntttuSDTbYIJkxY0b0uPvvv3+yww47JA888EDyl7/8Jdl6662TQw89NOlLzc3Nyeuvv+49vvSlLyVbbLFFUiqVzP322muv5Nhjj/X2W7x4cVILNttss+SCCy7wzu3999+P7nP88ccnY8aMSe6+++7k4YcfTvbYY49k8uTJSX+7/fbbk6OPPjr505/+lLzwwgvJH/7wh2TDDTdMTj/99Oh+tfL+XHfddUlTU1Pyy1/+MnnqqafSc1p33XWTN954o+r2f/vb35JCoZBcdNFFyT//+c/k7LPPThobG5Mnnngi6W9Tp05NZs2alTz55JPJY489lnz84x9Pxo4dG/1sue2HDRvmvQ8LFy5MasG5556bfOhDH/LO7a233jK3r+X35s033/Rex+zZs12Vj+TPf/7zgHhf3O+Mb37zm8mNN96YnvdNN93krf/ud7+b/u65+eabk3/84x/Jpz71qfTf6OXLl/fYdy8my7GyfD86+ncpy+/Sf/3rX8laa62V/j52n8Mf//jH6efyjjvu6PTrQnY0FjvBfRGqNRbdBzqfz3v/2FxxxRXpP0auMVaN+5C7fxT+/ve/ew2DXC6XvPbaa0l/aWlpSUaOHJk2tmLcl/6UU05JapFrLF5yySWZt1+0aFH6S++GG25oX/b000+n78/cuXOTWuN+WbtfFAPh/dltt92SE088sX2+WCwmG2+8cTJz5syq2//nf/5ncuCBB3rLdt999+TLX/5yUmtcA8V9Ru67775O/5tRK41F98dqVgPpvXGf/a222sr8g7eW35ewsehew+jRo5Pvf//73r9ZgwYNSn7729/22HcvpivHqvb96OjfpSy/S7/+9a+nf+Ron//859PGKnoPYegeMHfu3DQkM2rUqPZlU6dOlSVLlshTTz1l7uNCz7vsskv7MhdacIXBH3zwQekvf/zjH+Xtt9+WY445psNtf/Ob38gGG2wg48ePlxkzZsiyZcukVriwswsp77TTTvL9738/mhLghntsbW1Nr3+ZC7WNHTs2fZ9qzeLFi2XEiBE1//64ULm7tvq6us+3m7euq1uuty9/l2r1fXA6ei9caslmm20mY8aMkU9/+tPmvwn9wYUyXehzyy23TFNiXAjQMlDeG/e5u+aaa+SLX/xiGuYciO+L5gaRWLhwoXfthw8fnoaCrWvfle+epavHsr4fsX+XsvwuHSifw3rTr2ND1wv3RdYfbqc879ZZ+5THsC5zI824L5a1T1+46qqr0i/epptuGt3usMMOS/+hdb9oHn/8cTnzzDPT/JQbb7xR+ttXv/rVNJ/FXcs5c+ak/yC9/vrrcvHFF1fd3l3vpqaminxU9x7253tRzfPPPy8//vGP5Qc/+EHNvz///ve/06E5q303nnnmmU59l2rtfXDjx5966qny4Q9/OP2lZ9l2223ll7/8pWy//fbpL0/3vrn8Z/eLr6PvWG9zjQ2X6+XO0X0/zj//fPnoRz8qTz75ZJp3NlDfG5fvt2jRonTUr4H4voTK17cz174r3z1LV45lfT86+ncpy+9SaxvXoFy+fHma04met8Y2Fs866yz53ve+F93m6aef7jDhu55e36uvvpqOqf273/2uw+Mfd9xx7dPuL0GXeL3vvvvKCy+8kHbC6M/XM3369PZl7peBawh++ctfThOwa2Xs0a68P25MczeW+SGHHJKOZ15L78+axnWccI2qv/71r9HtJk2alD7KXIPEjUn/85//XC688ELpTwcccID3PXGNR/eL3H3/Xaeqgcr9wetem2uQDMT3pZ6/H/y7NHCtsY3F008/PfqXp+NCM1mMHj26omdYuSetW2ft8+abb3rLXKjU9ZC29unt1zdr1qw0dPupT32q08/nftGU73z1xpe+O++XOzd3bV3PYndHIeSutwu1uLsR+u6iew974r3oidezYMGCdFxz90vtyiuvrLn3pxoXaioUChW9ymPX1S3vzPb9wY0lf+utt6Y9WDt7F6qxsTFNjXDvQ61xn/0PfOAD5rkNhPfm5ZdflrvuuqvTd9Br+X0pX193rV3jqszN77jjjj323bN09lid+X6E/y5l+V1qfQ5dVRHuKvaiXsyHXOM6uOieYT//+c/TpNwVK1ZEO7i4nrdlrsdrf3VwcUnUrtNER71sLX/961/T1+N66tWaa665Jn1/3nnnnWgHl9///vfty5555pma6eDy6quvJttss03yhS98Ie0tOJDeH5cYf9JJJ3mJ8Ztsskm0g8snPvEJb9mkSZNqohOF+464JH+X2P9///d/XTqGe/+23Xbb5LTTTktqzXvvvZest956yY9+9KMB997oTjuuM0hra+uAfV+sDi4/+MEP2pe5HsRZOrh05rsXk+VYXfl+hP8uZfld6jq4uB7TmqsiQgeX3kVjMYOXX3457cZ//vnnJ+uss0467R7uH1fd3X+//fZLSwa4LvyuR7Hu7v/ggw+m/xi5X/y6dM5OO+2UrnNfGtcg6OvSOWV33XVX+qV1vYBD7pzdubvzdJ5//vm0t7Rr6L744otpOZctt9wy2XPPPZP+NmfOnLQntHsfXKkZ11B078WRRx5pvp5y6RxX5uGee+5JX5f7Jege/c2dqyuptO+++6bTuuTEQHh/XMkN90vt6quvTv9AOu6449KSG+XejkcccURy1llneeVZGhoa0l+M7rPofvnXSnmWE044If1j8d577/Xeh2XLlrVvE74e929GuezRvHnz0gb/4MGD0/Ij/c39Yehei/uMuOvuypW4MiWuF+tAe2/KDRj3HT7zzDMr1tX6++J+l5R/r7h/h12pNjftfveUS+e47437Lj/++OPJpz/96YrSOR/72MfSMjJZv3udkeVYHX0/svy7lOV3abl0zhlnnJF+Di+//HJK5/QBGosZHHXUUekXOHzoGl4vvfRScsABByRDhgxJ/8F1/xDrv27dtm4f9yUpe/vtt9PGoWuAur+cjjnmmPYGaF9z52HVFXTnrF/v/Pnz0y/4iBEj0n9AXGPGfXFroc6i+4fflfNw/2i5f/y322675P/9v//n3eENX4/j/tH9yle+kt5Zcf8QfeYzn/EaZP15N7vaZ08HBWr9/XG/wNwvcVenzd2hcHVFdSkN9/3Sfve73yUf+MAH0u1diYz//d//TWqB9T6498h6Paeeemr7ax81alRae+6RRx5JaoErN7LRRhul5+buErl59wt9IL43jmv8uffj2WefrVhX6+9L+fdD+Cifs7tr961vfSs9V/eddn88hq/TlQxzDfis373O6uhYHX0/sv671NHv0vL12nHHHdNzcQ1O/R1E78i5//VmmBsAAAADF3UWAQAAYKKxCAAAABONRQAAAJhoLAIAAMBEYxEAAAAmGosAAAAw0VgEAACAicYiAAAATDQWAQAYQG6++Wb57ne/29+ngTUIjUUA/eLss8+WH//4x+0/AWQzb948GTFihLz88sud2u8zn/mMrLfeevK5z32uV86rt49fi1555RXZe++95YMf/KBsv/32csMNN0g9nhuNRQD94qabbpK99tqr/SeAbFpbW+Xee++V0aNHd2q/U045RX7961/32nn19vFrUUNDg1x66aXyz3/+U+6880459dRTZenSpVJv59bQ42cHAB1YsGCBbLDBBu0P91cvgGy6GoJ2d5lcI7O39Pbxa9FGG22UPhzXeHf/nr3zzjuy9tpr19W5cWcRQJ+bPXu2nHTSSe0/gYHKNZDcHRtg3rx5UiwWZcyYMVJv58adRQB97sUXX0xzFb/97W+nPwF0bO7cufKRj3xE9t9/f/nf//3fivU77rijtLW1VSx3IciNN96428/f28evVTtmeN3ujt2RRx4pv/jFL2rqvHrq3GgsAuhz5513nvcTQMeuuuoqOfnkk9OfLpUjbKA99thjvfr8vX38alpaWqSpqUn602MdvO7m5mY56KCD5KyzzpLJkyfXzHn15LkRhgYAIBJmdqkS7jF8+PA07+tb3/qWJEnSvk2pVJKvf/3raQ9llxtm/RHkjuUaey5s7XoNjxo1Kr3b4zodHHPMMTJ06FDZeuut5fbbb6/Y9/3335frr79eTjjhBDnwwAPl6quv7vHX+vvf/14mTJggQ4YMkfXXX1+mTJnS45017rjjjvTu6Lrrrps+xyc+8Ql54YUXKq63u0buWk+dOrX9Gl900UXp9Rk0aJCMHTtWvvOd72Q6d7fvzJkzZYsttkjX77DDDun2ZR0dO8Z9Do4++mj52Mc+JkcccYS5XUfP0Z3PRnfPLevBAABAFXvttVeyzjrrJKecckryzDPPJNdcc02y1lprJVdeeWX7+mHDhiXnnXde8n//93/Jr371qySXyyV33nln1WMNHTo0ufDCC9Nt3c9CoZAccMAB6fHcshNOOCFZf/31k6VLl3r7XnXVVckuu+ySTt9yyy3JVlttlZRKpU69ln333TfZYIMNkiFDhiSbbLJJMmfOnPZ1CxYsSBoaGpKLL744efHFF5PHH388ufzyy5P33nuvR45f9vvf/z75n//5n+S5555LHn300eSTn/xkMmHChKRYLHrX+4wzzkivt3s4X//615P11lsvufrqq5Pnn38++ctf/pL84he/yHTu3/72t5Nx48Yld9xxR/LCCy8ks2bNSgYNGpTce++9HR67I3/5y1/S93uHHXZof7jnD3X0HN35bHT33LKgsQgAgMH9Et9uu+28htmZZ56ZLiuv/8hHPuLts+uuu6bbVDuW3ratrS1Ze+21kyOOOKJ92euvv+5uWSZz58719p08eXJy6aWXptOtra1po+zPf/5zj73OefPmpc/70ksvJX3prbfeSp/3iSeeaL9GO+20k7fNkiVL0sad1YCLnfuKFSvSxn3YcJ02bVpy6KGHdnjsnrAkw3N057PRFwhDAwAQsccee0gul2ufnzRpkjz33HNp71InLP3kypW8+eabVY+lty0UCmnI1IVPy1z40dH7P/vss/LQQw/JoYce2l4/7/Of/3yau9hTXGh23333Tc/lkEMOSUOg7777rvQ0d93c69hyyy1l2LBhsvnmm6fL58+f377NxIkTvX2efvrpNPfOnV9nz/3555+XZcuWyX/8x3/IOuus0/5w9SBd+LujY/eEpzM+R1c+G32FDi4AAHRDY2OjN+8ali5HLeu2elm5Uar3d41C1+tVd2hxkUGX+/aTn/wkzaXsLtc4caWs5syZk/amdaMqffOb35QHH3wwzfXrKZ/85Cdls802Sxt07vW41zl+/Pi0I0tZWAfQ5Rl29dxdrqfjeo9vsskm3n7u+i1atEh625AOzr87n42+wp1FAAAiXKNDe+CBB2SbbbZJGym9zTUS3V2wH/7wh2nv1/LjH//4R9rY+u1vf9tjz+UaIx/+8Ifl/PPPl0cffTTthexGWOopb7/9dnqX1JXLcnfZtttuu0x3L921dg2uu+++u9Pn7oa6c41Cd+fSdRDRD1dzMMuxu6svnqO3cWcRAIAI19CYPn26fPnLX5ZHHnkkvXPlGm994dZbb00bVNOmTau4g3jwwQendx2PP/74HmkQu8bMfvvtJxtuuGE6/9Zbb6UNup7ievm60OqVV16ZhurddXUlXToyePBgOfPMM9Me564R6BqF7tyeeuqp9LrEzt31Iv7a174mp512WnpHzvXEXrx4sfztb39Lw+BHHXVU9Ng9YXAH5z8Q0FgEACDCFTRevny57LbbbundRDcG8nHHHdcnz+0ag64MTLVQs2ssunIsjz/+eLeHzHQNp/vvvz8dS3jJkiVpqNg1iA844ADpKfl8Xq677jr56le/moaet912W7nsssvSsjEdceWKXK7mOeeck9aYdI3NciO5o3O/8MILZeTIkWn5nH/9619p2Z6dd95ZvvGNb3R47J7yrT54jt6Uc71c+vskAACoRa4h40bKcA0RYE1FziIAAABMNBYBAABgIgwNAAAAE3cWAQAAYKKxCAAAABONRQAAAJhoLAIAAMBEYxEAAAAmGosAAAAw0VgEAACAicYiAAAATDQWAQAAYKKxCAAAABONRQAAAIjl/wfdVC0RGcsdzgAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "with astropy.visualization.quantity_support():\n", + " fig, axs = plt.subplots(\n", + " ncols=2,\n", + " gridspec_kw=dict(width_ratios=[.9,.1]),\n", + " constrained_layout=True,\n", + " )\n", + " colorbar = na.plt.rgbmesh(\n", + " C=scene,\n", + " axis_wavelength=\"wavelength\",\n", + " ax=axs[0],\n", + " vmin=0,\n", + " vmax=scene.outputs.max(),\n", + " )\n", + " na.plt.pcolormesh(\n", + " C=colorbar,\n", + " axis_rgb=\"wavelength\",\n", + " ax=axs[1],\n", + " )\n", + " axs[1].yaxis.tick_right()\n", + " axs[1].yaxis.set_label_position(\"right\")" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "9cb5334a-042b-4fc3-a658-550d7bb7ab09", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "axis_input=(1, 2)\n", + "axis_output=(1, 2)\n", + "axis_input=(np.int64(-1), np.int64(-2))\n", + "axis_output=(np.int64(-1), np.int64(-2))\n", + "axis_output_orthogonal=(np.int64(-3),)\n", + "shape_orthogonal=(21,)\n", + "ax=np.int64(-2)\n", + "ax=np.int64(-1)\n", + "shape_output=(256, 21, 128)\n" + ] + }, + { + "ename": "ValueError", + "evalue": "operands could not be broadcast together with remapped shapes [original->remapped]: (21,128,128) and requested shape (128,21,128)", + "output_type": "error", + "traceback": [ + "\u001b[31m---------------------------------------------------------------------------\u001b[39m", + "\u001b[31mValueError\u001b[39m Traceback (most recent call last)", + "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[10]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m image = \u001b[43minstrument\u001b[49m\u001b[43m.\u001b[49m\u001b[43mimage\u001b[49m\u001b[43m(\u001b[49m\u001b[43mscene\u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[36mFile \u001b[39m\u001b[32m~\\Kankelborg-Group\\ctis\\ctis\\instruments\\_instruments.py:125\u001b[39m, in \u001b[36mAbstractLinearInstrument.image\u001b[39m\u001b[34m(self, scene)\u001b[39m\n\u001b[32m 117\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34mimage\u001b[39m(\n\u001b[32m 118\u001b[39m \u001b[38;5;28mself\u001b[39m,\n\u001b[32m 119\u001b[39m scene: na.FunctionArray[na.SpectralPositionalVectorArray, na.AbstractScalar],\n\u001b[32m 120\u001b[39m ) -> na.FunctionArray[na.SpectralPositionalVectorArray, na.AbstractScalar]:\n\u001b[32m 122\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m na.FunctionArray(\n\u001b[32m 123\u001b[39m inputs=\u001b[38;5;28mself\u001b[39m.coordinates_scene,\n\u001b[32m 124\u001b[39m outputs=na.regridding.regrid_from_weights(\n\u001b[32m--> \u001b[39m\u001b[32m125\u001b[39m *\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mweights\u001b[49m,\n\u001b[32m 126\u001b[39m values_input=scene.outputs,\n\u001b[32m 127\u001b[39m )\n\u001b[32m 128\u001b[39m )\n", + "\u001b[36mFile \u001b[39m\u001b[32m~\\AppData\\Local\\Programs\\Python\\Python313\\Lib\\functools.py:1026\u001b[39m, in \u001b[36mcached_property.__get__\u001b[39m\u001b[34m(self, instance, owner)\u001b[39m\n\u001b[32m 1024\u001b[39m val = cache.get(\u001b[38;5;28mself\u001b[39m.attrname, _NOT_FOUND)\n\u001b[32m 1025\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m val \u001b[38;5;129;01mis\u001b[39;00m _NOT_FOUND:\n\u001b[32m-> \u001b[39m\u001b[32m1026\u001b[39m val = \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mfunc\u001b[49m\u001b[43m(\u001b[49m\u001b[43minstance\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 1027\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m 1028\u001b[39m cache[\u001b[38;5;28mself\u001b[39m.attrname] = val\n", + "\u001b[36mFile \u001b[39m\u001b[32m~\\Kankelborg-Group\\ctis\\ctis\\instruments\\_instruments.py:247\u001b[39m, in \u001b[36mIdealInstrument.weights\u001b[39m\u001b[34m(self)\u001b[39m\n\u001b[32m 244\u001b[39m coordinates_input = \u001b[38;5;28mself\u001b[39m.distortion(\u001b[38;5;28mself\u001b[39m.coordinates_scene)\n\u001b[32m 245\u001b[39m coordinates_output = \u001b[38;5;28mself\u001b[39m.coordinates_sensor\n\u001b[32m--> \u001b[39m\u001b[32m247\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mna\u001b[49m\u001b[43m.\u001b[49m\u001b[43mregridding\u001b[49m\u001b[43m.\u001b[49m\u001b[43mweights\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 248\u001b[39m \u001b[43m \u001b[49m\u001b[43mcoordinates_input\u001b[49m\u001b[43m=\u001b[49m\u001b[43mcoordinates_input\u001b[49m\u001b[43m.\u001b[49m\u001b[43mposition\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 249\u001b[39m \u001b[43m \u001b[49m\u001b[43mcoordinates_output\u001b[49m\u001b[43m=\u001b[49m\u001b[43mcoordinates_output\u001b[49m\u001b[43m.\u001b[49m\u001b[43mposition\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 250\u001b[39m \u001b[43m \u001b[49m\u001b[43maxis_input\u001b[49m\u001b[43m=\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43maxis_scene_xy\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 251\u001b[39m \u001b[43m \u001b[49m\u001b[43maxis_output\u001b[49m\u001b[43m=\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43maxis_sensor_xy\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 252\u001b[39m \u001b[43m \u001b[49m\u001b[43mmethod\u001b[49m\u001b[43m=\u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mconservative\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[32m 253\u001b[39m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[36mFile \u001b[39m\u001b[32m~\\Kankelborg-Group\\named_arrays\\named_arrays\\regridding.py:177\u001b[39m, in \u001b[36mweights\u001b[39m\u001b[34m(coordinates_input, coordinates_output, axis_input, axis_output, method)\u001b[39m\n\u001b[32m 131\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34mweights\u001b[39m(\n\u001b[32m 132\u001b[39m coordinates_input: na.AbstractScalar | na.AbstractVectorArray,\n\u001b[32m 133\u001b[39m coordinates_output: na.AbstractScalar | na.AbstractVectorArray,\n\u001b[32m (...)\u001b[39m\u001b[32m 136\u001b[39m method: Literal[\u001b[33m'\u001b[39m\u001b[33mmultilinear\u001b[39m\u001b[33m'\u001b[39m, \u001b[33m'\u001b[39m\u001b[33mconservative\u001b[39m\u001b[33m'\u001b[39m] = \u001b[33m'\u001b[39m\u001b[33mmultilinear\u001b[39m\u001b[33m'\u001b[39m,\n\u001b[32m 137\u001b[39m ) -> \u001b[38;5;28mtuple\u001b[39m[na.AbstractScalar, \u001b[38;5;28mdict\u001b[39m[\u001b[38;5;28mstr\u001b[39m, \u001b[38;5;28mint\u001b[39m], \u001b[38;5;28mdict\u001b[39m[\u001b[38;5;28mstr\u001b[39m, \u001b[38;5;28mint\u001b[39m]]:\n\u001b[32m 138\u001b[39m \u001b[38;5;250m \u001b[39m\u001b[33;03m\"\"\"\u001b[39;00m\n\u001b[32m 139\u001b[39m \u001b[33;03m Save the results of a regridding operation as a sequence of weights,\u001b[39;00m\n\u001b[32m 140\u001b[39m \u001b[33;03m which can be used in subsequent regridding operations on the same grid.\u001b[39;00m\n\u001b[32m (...)\u001b[39m\u001b[32m 175\u001b[39m \n\u001b[32m 176\u001b[39m \u001b[33;03m \"\"\"\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m177\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mna\u001b[49m\u001b[43m.\u001b[49m\u001b[43m_named_array_function\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 178\u001b[39m \u001b[43m \u001b[49m\u001b[43mfunc\u001b[49m\u001b[43m=\u001b[49m\u001b[43mweights\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 179\u001b[39m \u001b[43m \u001b[49m\u001b[43mcoordinates_input\u001b[49m\u001b[43m=\u001b[49m\u001b[43mcoordinates_input\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 180\u001b[39m \u001b[43m \u001b[49m\u001b[43mcoordinates_output\u001b[49m\u001b[43m=\u001b[49m\u001b[43mcoordinates_output\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 181\u001b[39m \u001b[43m \u001b[49m\u001b[43maxis_input\u001b[49m\u001b[43m=\u001b[49m\u001b[43maxis_input\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 182\u001b[39m \u001b[43m \u001b[49m\u001b[43maxis_output\u001b[49m\u001b[43m=\u001b[49m\u001b[43maxis_output\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 183\u001b[39m \u001b[43m \u001b[49m\u001b[43mmethod\u001b[49m\u001b[43m=\u001b[49m\u001b[43mmethod\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 184\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[36mFile \u001b[39m\u001b[32m~\\Kankelborg-Group\\named_arrays\\named_arrays\\_named_array_functions.py:68\u001b[39m, in \u001b[36m_named_array_function\u001b[39m\u001b[34m(func, *args, **kwargs)\u001b[39m\n\u001b[32m 65\u001b[39m arrays = \u001b[38;5;28msorted\u001b[39m(arrays, key=functools.cmp_to_key(_is_subclass))\n\u001b[32m 67\u001b[39m \u001b[38;5;28;01mfor\u001b[39;00m array \u001b[38;5;129;01min\u001b[39;00m arrays:\n\u001b[32m---> \u001b[39m\u001b[32m68\u001b[39m res = \u001b[43marray\u001b[49m\u001b[43m.\u001b[49m\u001b[43m__named_array_function__\u001b[49m\u001b[43m(\u001b[49m\u001b[43mfunc\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 69\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m res \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28mNotImplemented\u001b[39m:\n\u001b[32m 70\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m res\n", + "\u001b[36mFile \u001b[39m\u001b[32m~\\Kankelborg-Group\\named_arrays\\named_arrays\\_vectors\\vectors.py:478\u001b[39m, in \u001b[36mAbstractVectorArray.__named_array_function__\u001b[39m\u001b[34m(self, func, *args, **kwargs)\u001b[39m\n\u001b[32m 475\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m vector_named_array_functions.ndfilter(func, *args, **kwargs)\n\u001b[32m 477\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m func \u001b[38;5;129;01min\u001b[39;00m vector_named_array_functions.HANDLED_FUNCTIONS:\n\u001b[32m--> \u001b[39m\u001b[32m478\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mvector_named_array_functions\u001b[49m\u001b[43m.\u001b[49m\u001b[43mHANDLED_FUNCTIONS\u001b[49m\u001b[43m[\u001b[49m\u001b[43mfunc\u001b[49m\u001b[43m]\u001b[49m\u001b[43m(\u001b[49m\u001b[43m*\u001b[49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 480\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mNotImplemented\u001b[39m\n", + "\u001b[36mFile \u001b[39m\u001b[32m~\\Kankelborg-Group\\named_arrays\\named_arrays\\_vectors\\vector_named_array_functions.py:944\u001b[39m, in \u001b[36mregridding_weights\u001b[39m\u001b[34m(coordinates_input, coordinates_output, axis_input, axis_output, method)\u001b[39m\n\u001b[32m 941\u001b[39m axis_input = \u001b[38;5;28mtuple\u001b[39m(\u001b[38;5;28mtuple\u001b[39m(shape_input).index(a) \u001b[38;5;28;01mfor\u001b[39;00m a \u001b[38;5;129;01min\u001b[39;00m axis_input)\n\u001b[32m 942\u001b[39m axis_output = \u001b[38;5;28mtuple\u001b[39m(\u001b[38;5;28mtuple\u001b[39m(shape_output).index(a) \u001b[38;5;28;01mfor\u001b[39;00m a \u001b[38;5;129;01min\u001b[39;00m axis_output)\n\u001b[32m--> \u001b[39m\u001b[32m944\u001b[39m result, _shape_input, _shape_output = \u001b[43mregridding\u001b[49m\u001b[43m.\u001b[49m\u001b[43mweights\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 945\u001b[39m \u001b[43m \u001b[49m\u001b[43mcoordinates_input\u001b[49m\u001b[43m=\u001b[49m\u001b[43mcoordinates_input\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 946\u001b[39m \u001b[43m \u001b[49m\u001b[43mcoordinates_output\u001b[49m\u001b[43m=\u001b[49m\u001b[43mcoordinates_output\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 947\u001b[39m \u001b[43m \u001b[49m\u001b[43maxis_input\u001b[49m\u001b[43m=\u001b[49m\u001b[43maxis_input\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 948\u001b[39m \u001b[43m \u001b[49m\u001b[43maxis_output\u001b[49m\u001b[43m=\u001b[49m\u001b[43maxis_output\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 949\u001b[39m \u001b[43m \u001b[49m\u001b[43mmethod\u001b[49m\u001b[43m=\u001b[49m\u001b[43mmethod\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 950\u001b[39m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 952\u001b[39m result = na.ScalarArray(result, \u001b[38;5;28mtuple\u001b[39m(shape_orthogonal))\n\u001b[32m 954\u001b[39m shape_input = \u001b[38;5;28mdict\u001b[39m(\u001b[38;5;28mzip\u001b[39m(shape_input, _shape_input))\n", + "\u001b[36mFile \u001b[39m\u001b[32m~\\Kankelborg-Group\\regridding\\regridding\\_weights\\_weights.py:131\u001b[39m, in \u001b[36mweights\u001b[39m\u001b[34m(coordinates_input, coordinates_output, axis_input, axis_output, method)\u001b[39m\n\u001b[32m 124\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m _weights_multilinear(\n\u001b[32m 125\u001b[39m coordinates_input=coordinates_input,\n\u001b[32m 126\u001b[39m coordinates_output=coordinates_output,\n\u001b[32m 127\u001b[39m axis_input=axis_input,\n\u001b[32m 128\u001b[39m axis_output=axis_output,\n\u001b[32m 129\u001b[39m )\n\u001b[32m 130\u001b[39m \u001b[38;5;28;01melif\u001b[39;00m method == \u001b[33m\"\u001b[39m\u001b[33mconservative\u001b[39m\u001b[33m\"\u001b[39m:\n\u001b[32m--> \u001b[39m\u001b[32m131\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43m_weights_conservative\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 132\u001b[39m \u001b[43m \u001b[49m\u001b[43mcoordinates_input\u001b[49m\u001b[43m=\u001b[49m\u001b[43mcoordinates_input\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 133\u001b[39m \u001b[43m \u001b[49m\u001b[43mcoordinates_output\u001b[49m\u001b[43m=\u001b[49m\u001b[43mcoordinates_output\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 134\u001b[39m \u001b[43m \u001b[49m\u001b[43maxis_input\u001b[49m\u001b[43m=\u001b[49m\u001b[43maxis_input\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 135\u001b[39m \u001b[43m \u001b[49m\u001b[43maxis_output\u001b[49m\u001b[43m=\u001b[49m\u001b[43maxis_output\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 136\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 137\u001b[39m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[32m 138\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33munrecognized method \u001b[39m\u001b[33m'\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mmethod\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m'\u001b[39m\u001b[33m\"\u001b[39m)\n", + "\u001b[36mFile \u001b[39m\u001b[32m~\\Kankelborg-Group\\regridding\\regridding\\_weights\\_weights_conservative.py:25\u001b[39m, in \u001b[36m_weights_conservative\u001b[39m\u001b[34m(coordinates_input, coordinates_output, axis_input, axis_output)\u001b[39m\n\u001b[32m 11\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34m_weights_conservative\u001b[39m(\n\u001b[32m 12\u001b[39m coordinates_input: \u001b[38;5;28mtuple\u001b[39m[np.ndarray, ...],\n\u001b[32m 13\u001b[39m coordinates_output: \u001b[38;5;28mtuple\u001b[39m[np.ndarray, ...],\n\u001b[32m 14\u001b[39m axis_input: \u001b[38;5;28;01mNone\u001b[39;00m | \u001b[38;5;28mint\u001b[39m | Sequence[\u001b[38;5;28mint\u001b[39m] = \u001b[38;5;28;01mNone\u001b[39;00m,\n\u001b[32m 15\u001b[39m axis_output: \u001b[38;5;28;01mNone\u001b[39;00m | \u001b[38;5;28mint\u001b[39m | Sequence[\u001b[38;5;28mint\u001b[39m] = \u001b[38;5;28;01mNone\u001b[39;00m,\n\u001b[32m 16\u001b[39m ) -> \u001b[38;5;28mtuple\u001b[39m[np.ndarray, \u001b[38;5;28mtuple\u001b[39m[\u001b[38;5;28mint\u001b[39m, ...], \u001b[38;5;28mtuple\u001b[39m[\u001b[38;5;28mint\u001b[39m, ...]]:\n\u001b[32m 17\u001b[39m (\n\u001b[32m 18\u001b[39m coordinates_input,\n\u001b[32m 19\u001b[39m coordinates_output,\n\u001b[32m 20\u001b[39m axis_input,\n\u001b[32m 21\u001b[39m axis_output,\n\u001b[32m 22\u001b[39m shape_input,\n\u001b[32m 23\u001b[39m shape_output,\n\u001b[32m 24\u001b[39m shape_orthogonal,\n\u001b[32m---> \u001b[39m\u001b[32m25\u001b[39m ) = \u001b[43m_util\u001b[49m\u001b[43m.\u001b[49m\u001b[43m_normalize_input_output_coordinates\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 26\u001b[39m \u001b[43m \u001b[49m\u001b[43mcoordinates_input\u001b[49m\u001b[43m=\u001b[49m\u001b[43mcoordinates_input\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 27\u001b[39m \u001b[43m \u001b[49m\u001b[43mcoordinates_output\u001b[49m\u001b[43m=\u001b[49m\u001b[43mcoordinates_output\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 28\u001b[39m \u001b[43m \u001b[49m\u001b[43maxis_input\u001b[49m\u001b[43m=\u001b[49m\u001b[43maxis_input\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 29\u001b[39m \u001b[43m \u001b[49m\u001b[43maxis_output\u001b[49m\u001b[43m=\u001b[49m\u001b[43maxis_output\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 30\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 32\u001b[39m shape_values_input = \u001b[38;5;28mlist\u001b[39m(shape_input)\n\u001b[32m 33\u001b[39m \u001b[38;5;28;01mfor\u001b[39;00m ax \u001b[38;5;129;01min\u001b[39;00m axis_input:\n", + "\u001b[36mFile \u001b[39m\u001b[32m~\\Kankelborg-Group\\regridding\\regridding\\_util.py:106\u001b[39m, in \u001b[36m_normalize_input_output_coordinates\u001b[39m\u001b[34m(coordinates_input, coordinates_output, axis_input, axis_output)\u001b[39m\n\u001b[32m 102\u001b[39m shape_output = \u001b[38;5;28mtuple\u001b[39m(\u001b[38;5;28mreversed\u001b[39m(shape_output))\n\u001b[32m 104\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mshape_output\u001b[38;5;132;01m=}\u001b[39;00m\u001b[33m\"\u001b[39m)\n\u001b[32m--> \u001b[39m\u001b[32m106\u001b[39m coordinates_input = \u001b[38;5;28;43mtuple\u001b[39;49m\u001b[43m(\u001b[49m\n\u001b[32m 107\u001b[39m \u001b[43m \u001b[49m\u001b[43mnp\u001b[49m\u001b[43m.\u001b[49m\u001b[43mbroadcast_to\u001b[49m\u001b[43m(\u001b[49m\u001b[43mcoord\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mshape_input\u001b[49m\u001b[43m)\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43;01mfor\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mcoord\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;129;43;01min\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mcoordinates_input\u001b[49m\n\u001b[32m 108\u001b[39m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 109\u001b[39m coordinates_output = \u001b[38;5;28mtuple\u001b[39m(\n\u001b[32m 110\u001b[39m np.broadcast_to(coord, shape_output) \u001b[38;5;28;01mfor\u001b[39;00m coord \u001b[38;5;129;01min\u001b[39;00m coordinates_output\n\u001b[32m 111\u001b[39m )\n\u001b[32m 113\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m (\n\u001b[32m 114\u001b[39m coordinates_input,\n\u001b[32m 115\u001b[39m coordinates_output,\n\u001b[32m (...)\u001b[39m\u001b[32m 120\u001b[39m shape_orthogonal,\n\u001b[32m 121\u001b[39m )\n", + "\u001b[36mFile \u001b[39m\u001b[32m~\\Kankelborg-Group\\regridding\\regridding\\_util.py:107\u001b[39m, in \u001b[36m\u001b[39m\u001b[34m(.0)\u001b[39m\n\u001b[32m 102\u001b[39m shape_output = \u001b[38;5;28mtuple\u001b[39m(\u001b[38;5;28mreversed\u001b[39m(shape_output))\n\u001b[32m 104\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mshape_output\u001b[38;5;132;01m=}\u001b[39;00m\u001b[33m\"\u001b[39m)\n\u001b[32m 106\u001b[39m coordinates_input = \u001b[38;5;28mtuple\u001b[39m(\n\u001b[32m--> \u001b[39m\u001b[32m107\u001b[39m \u001b[43mnp\u001b[49m\u001b[43m.\u001b[49m\u001b[43mbroadcast_to\u001b[49m\u001b[43m(\u001b[49m\u001b[43mcoord\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mshape_input\u001b[49m\u001b[43m)\u001b[49m \u001b[38;5;28;01mfor\u001b[39;00m coord \u001b[38;5;129;01min\u001b[39;00m coordinates_input\n\u001b[32m 108\u001b[39m )\n\u001b[32m 109\u001b[39m coordinates_output = \u001b[38;5;28mtuple\u001b[39m(\n\u001b[32m 110\u001b[39m np.broadcast_to(coord, shape_output) \u001b[38;5;28;01mfor\u001b[39;00m coord \u001b[38;5;129;01min\u001b[39;00m coordinates_output\n\u001b[32m 111\u001b[39m )\n\u001b[32m 113\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m (\n\u001b[32m 114\u001b[39m coordinates_input,\n\u001b[32m 115\u001b[39m coordinates_output,\n\u001b[32m (...)\u001b[39m\u001b[32m 120\u001b[39m shape_orthogonal,\n\u001b[32m 121\u001b[39m )\n", + "\u001b[36mFile \u001b[39m\u001b[32m~\\AppData\\Local\\Programs\\Python\\Python313\\Lib\\site-packages\\astropy\\units\\quantity.py:1879\u001b[39m, in \u001b[36mQuantity.__array_function__\u001b[39m\u001b[34m(self, function, types, args, kwargs)\u001b[39m\n\u001b[32m 1866\u001b[39m \u001b[38;5;66;03m# A function should be in one of the following sets or dicts:\u001b[39;00m\n\u001b[32m 1867\u001b[39m \u001b[38;5;66;03m# 1. SUBCLASS_SAFE_FUNCTIONS (set), if the numpy implementation\u001b[39;00m\n\u001b[32m 1868\u001b[39m \u001b[38;5;66;03m# supports Quantity; we pass on to ndarray.__array_function__.\u001b[39;00m\n\u001b[32m (...)\u001b[39m\u001b[32m 1876\u001b[39m \u001b[38;5;66;03m# function is in none of the above, we simply call the numpy\u001b[39;00m\n\u001b[32m 1877\u001b[39m \u001b[38;5;66;03m# implementation.\u001b[39;00m\n\u001b[32m 1878\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m function \u001b[38;5;129;01min\u001b[39;00m SUBCLASS_SAFE_FUNCTIONS:\n\u001b[32m-> \u001b[39m\u001b[32m1879\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43msuper\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[43m.\u001b[49m\u001b[43m__array_function__\u001b[49m\u001b[43m(\u001b[49m\u001b[43mfunction\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mtypes\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 1881\u001b[39m \u001b[38;5;28;01melif\u001b[39;00m function \u001b[38;5;129;01min\u001b[39;00m FUNCTION_HELPERS:\n\u001b[32m 1882\u001b[39m function_helper = FUNCTION_HELPERS[function]\n", + "\u001b[36mFile \u001b[39m\u001b[32m~\\AppData\\Local\\Programs\\Python\\Python313\\Lib\\site-packages\\numpy\\lib\\_stride_tricks_impl.py:443\u001b[39m, in \u001b[36mbroadcast_to\u001b[39m\u001b[34m(array, shape, subok)\u001b[39m\n\u001b[32m 400\u001b[39m \u001b[38;5;129m@array_function_dispatch\u001b[39m(_broadcast_to_dispatcher, module=\u001b[33m'\u001b[39m\u001b[33mnumpy\u001b[39m\u001b[33m'\u001b[39m)\n\u001b[32m 401\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34mbroadcast_to\u001b[39m(array, shape, subok=\u001b[38;5;28;01mFalse\u001b[39;00m):\n\u001b[32m 402\u001b[39m \u001b[38;5;250m \u001b[39m\u001b[33;03m\"\"\"Broadcast an array to a new shape.\u001b[39;00m\n\u001b[32m 403\u001b[39m \n\u001b[32m 404\u001b[39m \u001b[33;03m Parameters\u001b[39;00m\n\u001b[32m (...)\u001b[39m\u001b[32m 441\u001b[39m \u001b[33;03m [1, 2, 3]])\u001b[39;00m\n\u001b[32m 442\u001b[39m \u001b[33;03m \"\"\"\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m443\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43m_broadcast_to\u001b[49m\u001b[43m(\u001b[49m\u001b[43marray\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mshape\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43msubok\u001b[49m\u001b[43m=\u001b[49m\u001b[43msubok\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mreadonly\u001b[49m\u001b[43m=\u001b[49m\u001b[38;5;28;43;01mTrue\u001b[39;49;00m\u001b[43m)\u001b[49m\n", + "\u001b[36mFile \u001b[39m\u001b[32m~\\AppData\\Local\\Programs\\Python\\Python313\\Lib\\site-packages\\numpy\\lib\\_stride_tricks_impl.py:382\u001b[39m, in \u001b[36m_broadcast_to\u001b[39m\u001b[34m(array, shape, subok, readonly)\u001b[39m\n\u001b[32m 379\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[33m'\u001b[39m\u001b[33mall elements of broadcast shape must be non-\u001b[39m\u001b[33m'\u001b[39m\n\u001b[32m 380\u001b[39m \u001b[33m'\u001b[39m\u001b[33mnegative\u001b[39m\u001b[33m'\u001b[39m)\n\u001b[32m 381\u001b[39m extras = []\n\u001b[32m--> \u001b[39m\u001b[32m382\u001b[39m it = \u001b[43mnp\u001b[49m\u001b[43m.\u001b[49m\u001b[43mnditer\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 383\u001b[39m \u001b[43m \u001b[49m\u001b[43m(\u001b[49m\u001b[43marray\u001b[49m\u001b[43m,\u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mflags\u001b[49m\u001b[43m=\u001b[49m\u001b[43m[\u001b[49m\u001b[33;43m'\u001b[39;49m\u001b[33;43mmulti_index\u001b[39;49m\u001b[33;43m'\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[33;43m'\u001b[39;49m\u001b[33;43mrefs_ok\u001b[39;49m\u001b[33;43m'\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[33;43m'\u001b[39;49m\u001b[33;43mzerosize_ok\u001b[39;49m\u001b[33;43m'\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m \u001b[49m\u001b[43m+\u001b[49m\u001b[43m \u001b[49m\u001b[43mextras\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 384\u001b[39m \u001b[43m \u001b[49m\u001b[43mop_flags\u001b[49m\u001b[43m=\u001b[49m\u001b[43m[\u001b[49m\u001b[33;43m'\u001b[39;49m\u001b[33;43mreadonly\u001b[39;49m\u001b[33;43m'\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mitershape\u001b[49m\u001b[43m=\u001b[49m\u001b[43mshape\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43morder\u001b[49m\u001b[43m=\u001b[49m\u001b[33;43m'\u001b[39;49m\u001b[33;43mC\u001b[39;49m\u001b[33;43m'\u001b[39;49m\u001b[43m)\u001b[49m\n\u001b[32m 385\u001b[39m \u001b[38;5;28;01mwith\u001b[39;00m it:\n\u001b[32m 386\u001b[39m \u001b[38;5;66;03m# never really has writebackifcopy semantics\u001b[39;00m\n\u001b[32m 387\u001b[39m broadcast = it.itviews[\u001b[32m0\u001b[39m]\n", + "\u001b[31mValueError\u001b[39m: operands could not be broadcast together with remapped shapes [original->remapped]: (21,128,128) and requested shape (128,21,128)" + ] + } + ], + "source": [ + "image = instrument.image(scene)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "31089bac-3668-4ddd-9109-75f6cacca3a6", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "image.shape" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8b06f7c7-848a-4f77-8f03-afde1b50bb47", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From cbea0ae4d4468ff24f284232124f96e2e01b7936 Mon Sep 17 00:00:00 2001 From: Roy Smart Date: Sat, 4 Apr 2026 12:52:54 -0600 Subject: [PATCH 17/28] added nbsphinx --- docs/conf.py | 1 + pyproject.toml | 1 + 2 files changed, 2 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index 3ec527a..877a694 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -39,6 +39,7 @@ 'sphinx.ext.viewcode', 'sphinxcontrib.bibtex', 'jupyter_sphinx', + 'nbsphinx', ] autosummary_generate = True # Turn on sphinx.ext.autosummary autosummary_imported_members = True diff --git a/pyproject.toml b/pyproject.toml index bc4253a..e38ae8b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,6 +34,7 @@ doc = [ "pydata-sphinx-theme", "ipykernel", "jupyter-sphinx", + "nbsphinx", "sphinx-codeautolink", "sphinx-favicon", ] From 50210e604b02b2cd612ec5cdaee79b4c4d737d6c Mon Sep 17 00:00:00 2001 From: Roy Smart Date: Sat, 4 Apr 2026 13:04:40 -0600 Subject: [PATCH 18/28] update named-arrays --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index e38ae8b..ebacb30 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,7 @@ classifiers = [ ] dependencies = [ "astropy", - "named-arrays==0.21.0", + "named-arrays~=1.0", ] dynamic = ["version"] From d726cc7202247e3e1011efe54f70b4747fc83b8f Mon Sep 17 00:00:00 2001 From: Roy Smart Date: Sun, 5 Apr 2026 20:34:11 -0600 Subject: [PATCH 19/28] remove output --- docs/tutorials/ideal-instrument.ipynb | 97 +++++++++------------------ 1 file changed, 32 insertions(+), 65 deletions(-) diff --git a/docs/tutorials/ideal-instrument.ipynb b/docs/tutorials/ideal-instrument.ipynb index 97f46bc..88a7ca7 100644 --- a/docs/tutorials/ideal-instrument.ipynb +++ b/docs/tutorials/ideal-instrument.ipynb @@ -21,7 +21,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "initial_id", "metadata": { "ExecuteTime": { @@ -45,7 +45,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "id": "30daf494-91a5-4c14-9a7d-37e7ce757757", "metadata": { "editable": true, @@ -61,7 +61,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "id": "f8edf09f-ee49-4d66-ab58-df4d0e642fca", "metadata": { "editable": true, @@ -77,7 +77,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "id": "76a24148-01a8-47ab-a423-dad8a6fc8550", "metadata": { "editable": true, @@ -98,7 +98,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "id": "37b5250a-11a0-4eb2-b784-2c221f2c9e24", "metadata": { "editable": true, @@ -117,7 +117,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "id": "b051dfcf-7a27-42af-8173-c318e06b2c25", "metadata": { "editable": true, @@ -134,7 +134,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "id": "7a531ee6fdccb5e9", "metadata": { "editable": true, @@ -161,7 +161,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "id": "c9e75ff0-1d86-425b-a9c6-af33dfc888e4", "metadata": { "editable": true, @@ -180,7 +180,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "id": "0812c400-02d6-4682-a21b-2c32780fbcba", "metadata": { "editable": true, @@ -189,18 +189,7 @@ }, "tags": [] }, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAosAAAHrCAYAAACn9tfQAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjcsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvTLEjVAAAAAlwSFlzAAAPYQAAD2EBqD+naQAAdVFJREFUeJzt3Qu8XNPd+P/vzJxzkiAJIRKXxL2iTdzilrR1qTxC9aJVT4vHrSmlKKJKWnVtf2m1RbWq1b9G+6hSfdDyoIKibYIK6lI8KIIIiiTkci4z+/9aO5mT71oz33X2uc+ZfN5e4+z77Nkzk7PO/n7Xd+WSJEkEAAAAqCJfbSEAAADg0FgEAACAicYiAAAATDQWAQAAYKKxCAAAABONRQAAAJhoLAIAAMBEYxEAAAAmGosAAAAw0VgEAABAfTQW77//fvnkJz8pG2+8seRyObn55pu99W7kwnPOOUc22mgjGTJkiEyZMkWee+65Do97+eWXy+abby6DBw+W3XffXR566KFefBUAAAADx4BqLC5dulR22GGHtHFXzUUXXSSXXXaZ/OxnP5MHH3xQ1l57bZk6daqsWLHCPOb1118v06dPl3PPPVceeeSR9PhunzfffLMXXwkAAMDAkEvc7bgByN1ZvOmmm+Sggw5K593LcHccTz/9dPna176WLlu8eLGMGjVKrr76avnCF75Q9TjuTuKuu+4qP/nJT9L5UqkkY8aMkZNPPlnOOuusPnxFAAAAtadB6sSLL74oCxcuTEPPZcOHD08bg3Pnzq3aWGxpaZF58+bJjBkz2pfl8/n0GG4fS3Nzc/oocw3Md955R9Zff/20EQsAQG9yN0jee++99CaJ+73V29zvuQULFsjQoUMH1O+5vr5O9apuGouuoei4O4mamy+vC/373/+WYrFYdZ9nnnnGfK6ZM2fK+eef3yPnDQBAV73yyiuy6aab9vrzuIaii7oNVH11neoVzewucHciXYi7/Jg/f35/nxIAYA3k7vTV0/P0loF+/v2tbu4sjh49Ov35xhtvpL2hy9z8jjvuWHWfDTbYQAqFQrqN5ubLx6tm0KBB6QMAgP7UVyHhgRR6rsfz7291c2dxiy22SBt4d999d/uyJUuWpL2iJ02aVHWfpqYmmThxorePy8tw89Y+AAAAa5IBdWfx/fffl+eff97r1PLYY4/JiBEjZOzYsXLqqafKt7/9bdlmm23SxuO3vvWtNKm13GPa2XfffeUzn/mMnHTSSem8K5tz1FFHyS677CK77babXHrppWmJnmOOOaZfXiMAAEAtGVCNxYcfflj22Wef9nnX0HNcY8+Vx/n617+eNvSOO+44WbRokXzkIx+RO+64Iy22XfbCCy+kHVvKPv/5z8tbb72VFvN2HWFcyNrtE3Z6AQAAWBMN2DqLtcSFu12ZHgAA+pLrZDls2LBef56B/nuur65TvaqbnEUAAAD0PBqLAAAAMNFYBAAAgInGIgAAAEw0FgEAAGCisQgAAAATjUUAAACYaCwCAADARGMRAAAAJhqLAAAAMNFYBAAAgInGIgAAAEw0FgEAAGCisQgAAABTg70KAADAl1OP8rz0wDL9s7PrrOmSiPy7k68PlbizCAAAABONRQAAAJhoLAIAAMBEYxEAAAAmGosAAAAw0VgEAACAicYiAAAATDQWAQAAYKKxCAAAABONRQAAAJhoLAIAAMBEYxEAAAAmGosAAAAw0VgEAACAicYiAAAATDQWAQAAYKKxCAAAEPjud78ruVxOTj311PZlK1askBNPPFHWX399WWeddeTggw+WN954w9tv/vz5cuCBB8paa60lG264oZxxxhnS1tYmAxmNRQAAAOXvf/+7/PznP5ftt9/eW37aaafJLbfcIjfccIPcd999smDBAvnsZz/bvr5YLKYNxZaWFpkzZ4786le/kquvvlrOOeccGchoLAIAgLq2ZMkS79Hc3Gxu+/7778vhhx8uv/jFL2S99dZrX7548WK56qqr5OKLL5aPfexjMnHiRJk1a1baKHzggQfSbe6880755z//Kddcc43suOOOcsABB8iFF14ol19+edqAHKhoLAIAgEwKItIoIk0iMkRE1l71GCoiw0RkuIisKyKuiTVi1WN9Edlg1WOkemy46jFq1WO0emy06rFx8NhEPTZVjzHqMVb9dA9nzJgxMnz48PbHzJkzzdfowszu7uCUKVO85fPmzZPW1lZv+bhx42Ts2LEyd+7cdN79nDBhgowa5V7RSlOnTk0bqE899ZQMVA39fQJAX8t1eWUnJT26GQCgi1555RUZNsw1Z1caNGhQ1e2uu+46eeSRR9IwdGjhwoXS1NQk667rmsOruYahW1feRjcUy+vL6wYqGosAAKCuuYaibixaDcpTTjlFZs+eLYMH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ryFLSLkr+0d3ig49o1bp0LMOV+tDRS6wNfJNj8gYHq4oIVRtedL9MDKhZ6CmcWcRAAAAJhqLAAAAMBGGBuq9p3S1bbIerCLcbRy8K1HSjNHcqCz7WaHUiv2zvoiMJ5tlVJ3w3KxwdfTglq6MlpMxZk+oeY1VkvdEZLkk0iAlaZBces8pn/70H+4zuvJn5fzKZe5zvHL56qV62cpp9//yfa3KbVZ+E6ovc/+VpNiPV6t+cGcRAAAAJhqLAAAAMBGGBupN0oXIZVdC1Fn3725YvLsRy+6+tq6+oKQ337xeuihd6T2feTNCz8BAxZ1FAAAAmGgsAgAAwERjEQAAAPXfWLz33nsll8tVffz9738399t7770rtj/++OP79NyBPpFEHj1xjFp99PhrS4xH1ueNHDzp50fWCxndrLtvBIBaUzcdXCZPniyvv/66t+xb3/qW3H333bLLLrtE9z322GPlggsuaJ9fa621eu08AQAABpK6aSw2NTXJ6NGj2+dbW1vlD3/4g5x88snp3cIY1zjU+3akubk5fZQtWbKki2cNAABQ2+omDB364x//KG+//bYcc8wxHW77m9/8RjbYYAMZP368zJgxQ5YtWxbdfubMmTJ8+PD2x5gxY3rwzIF+0JuRw74KI3f3+Xv6ibKGq7sbuu7Ji5o1/N67FxJAjcklSXR8pwHr4x//ePrztttui2535ZVXymabbSYbb7yxPP7443LmmWfKbrvtJjfeeGOn7izSYERd6sowfrUmGUAXtb+vd4cNVtSaxYsXy7Bhw3r9edzvOXdzxAUk8+kwf+VH7Q/3t0D+2mfXqV7VfBj6rLPOku9973vRbZ5++mkZN25c+/yrr74qf/rTn+R3v/tdh8c/7rjj2qcnTJggG220key7777ywgsvyFZbbVV1n0GDBqUPAACAelfzjcXTTz9djj766Og2W265pTc/a9YsWX/99eVTn/pUp59v9913T38+//zzZmMRWGP0xM2k7t4tq7sbWh2EgXvtlmPdXUgAfaTmG4sjR45MH1m5qLprLB555JHS2NjY6ed77LHH0p/uDiMAAMCaru46uNxzzz3y4osvype+9KWKda+99loarn7ooYfSeRdqvvDCC2XevHny0ksvpZ1iXCNzzz33lO23374fzh4AAKC21Pydxc666qqr0pqLOodRl9N59tln23s7u3I7d911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btppxXHL99lnH3nxxRdl8803T5e5kLNrIN5yyy1pTsbBBx8sl112mayzzjp9/AoBAABqz4BpLAIAAKDvDZgwNAAAAPoejUUAAACYaCwCAADARGMRAAAAJhqLGbj6jZMnT5a11lorHUu6GlfS58ADD0y3cXUezzjjjLTAd4zriX344YentSDdcadNm9ZeCqivuB7iuVyu6uPvf/+7uZ/rgR5uf/zxx0stcD3dw3NzveJjVqxYkZZZWn/99dOe8K5XfLmoe3966aWX0s/FFltsIUOGDJGtttoqLTHR0tIS3a+W3p/LL788fU8GDx6c1i8Ni+eHbrjhhrTEldvejfd+2223SS1wY8LvuuuuMnTo0PQ7ftBBB8mzzz4b3efqq6+ueB/c66oF5513XsW5dTRaVq2+N9W+8+7hvtMD4X25//775ZOf/GRass2dy8033+ytd/1QzznnHNloo43SfwemTJkizz33XI9/97pzrCzfjyz/LmX5Xep+b+28885pqZ2tt946fT/Ry1xvaMSdc845ycUXX5xMnz49GT58eMX6tra2ZPz48cmUKVOSRx99NLntttuSDTbYIJkxY0b0uPvvv3+yww47JA888EDyl7/8Jdl6662TQw89NOlLzc3Nyeuvv+49vvSlLyVbbLFFUiqVzP322muv5Nhjj/X2W7x4cVILNttss+SCCy7wzu3999+P7nP88ccnY8aMSe6+++7k4YcfTvbYY49k8uTJSX+7/fbbk6OPPjr505/+lLzwwgvJH/7wh2TDDTdMTj/99Oh+tfL+XHfddUlTU1Pyy1/+MnnqqafSc1p33XWTN954o+r2f/vb35JCoZBcdNFFyT//+c/k7LPPThobG5Mnnngi6W9Tp05NZs2alTz55JPJY489lnz84x9Pxo4dG/1sue2HDRvmvQ8LFy5MasG5556bfOhDH/LO7a233jK3r+X35s033/Rex+zZs12Vj+TPf/7zgHhf3O+Mb37zm8mNN96YnvdNN93krf/ud7+b/u65+eabk3/84x/Jpz71qfTf6OXLl/fYdy8my7GyfD86+ncpy+/Sf/3rX8laa62V/j52n8Mf//jH6efyjjvu6PTrQnY0FjvBfRGqNRbdBzqfz3v/2FxxxRXpP0auMVaN+5C7fxT+/ve/ew2DXC6XvPbaa0l/aWlpSUaOHJk2tmLcl/6UU05JapFrLF5yySWZt1+0aFH6S++GG25oX/b000+n78/cuXOTWuN+WbtfFAPh/dltt92SE088sX2+WCwmG2+8cTJz5syq2//nf/5ncuCBB3rLdt999+TLX/5yUmtcA8V9Ru67775O/5tRK41F98dqVgPpvXGf/a222sr8g7eW35ewsehew+jRo5Pvf//73r9ZgwYNSn7729/22HcvpivHqvb96OjfpSy/S7/+9a+nf+Ron//859PGKnoPYegeMHfu3DQkM2rUqPZlU6dOlSVLlshTTz1l7uNCz7vsskv7MhdacIXBH3zwQekvf/zjH+Xtt9+WY445psNtf/Ob38gGG2wg48ePlxkzZsiyZcukVriwswsp77TTTvL9738/mhLghntsbW1Nr3+ZC7WNHTs2fZ9qzeLFi2XEiBE1//64ULm7tvq6us+3m7euq1uuty9/l2r1fXA6ei9caslmm20mY8aMkU9/+tPmvwn9wYUyXehzyy23TFNiXAjQMlDeG/e5u+aaa+SLX/xiGuYciO+L5gaRWLhwoXfthw8fnoaCrWvfle+epavHsr4fsX+XsvwuHSifw3rTr2ND1wv3RdYfbqc879ZZ+5THsC5zI824L5a1T1+46qqr0i/epptuGt3usMMOS/+hdb9oHn/8cTnzzDPT/JQbb7xR+ttXv/rVNJ/FXcs5c+ak/yC9/vrrcvHFF1fd3l3vpqaminxU9x7253tRzfPPPy8//vGP5Qc/+EHNvz///ve/06E5q303nnnmmU59l2rtfXDjx5966qny4Q9/OP2lZ9l2223ll7/8pWy//fbpL0/3vrn8Z/eLr6PvWG9zjQ2X6+XO0X0/zj//fPnoRz8qTz75ZJp3NlDfG5fvt2jRonTUr4H4voTK17cz174r3z1LV45lfT86+ncpy+9SaxvXoFy+fHma04met8Y2Fs866yz53ve+F93m6aef7jDhu55e36uvvpqOqf273/2uw+Mfd9xx7dPuL0GXeL3vvvvKCy+8kHbC6M/XM3369PZl7peBawh++ctfThOwa2Xs0a68P25MczeW+SGHHJKOZ15L78+axnWccI2qv/71r9HtJk2alD7KXIPEjUn/85//XC688ELpTwcccID3PXGNR/eL3H3/Xaeqgcr9wetem2uQDMT3pZ6/H/y7NHCtsY3F008/PfqXp+NCM1mMHj26omdYuSetW2ft8+abb3rLXKjU9ZC29unt1zdr1qw0dPupT32q08/nftGU73z1xpe+O++XOzd3bV3PYndHIeSutwu1uLsR+u6iew974r3oidezYMGCdFxz90vtyiuvrLn3pxoXaioUChW9ymPX1S3vzPb9wY0lf+utt6Y9WDt7F6qxsTFNjXDvQ61xn/0PfOAD5rkNhPfm5ZdflrvuuqvTd9Br+X0pX193rV3jqszN77jjjj323bN09lid+X6E/y5l+V1qfQ5dVRHuKvaiXsyHXOM6uOieYT//+c/TpNwVK1ZEO7i4nrdlrsdrf3VwcUnUrtNER71sLX/961/T1+N66tWaa665Jn1/3nnnnWgHl9///vfty5555pma6eDy6quvJttss03yhS98Ie0tOJDeH5cYf9JJJ3mJ8Ztsskm0g8snPvEJb9mkSZNqohOF+464JH+X2P9///d/XTqGe/+23Xbb5LTTTktqzXvvvZest956yY9+9KMB997oTjuuM0hra+uAfV+sDi4/+MEP2pe5HsRZOrh05rsXk+VYXfl+hP8uZfld6jq4uB7TmqsiQgeX3kVjMYOXX3457cZ//vnnJ+uss0467R7uH1fd3X+//fZLSwa4LvyuR7Hu7v/ggw+m/xi5X/y6dM5OO+2UrnNfGtcg6OvSOWV33XVX+qV1vYBD7pzdubvzdJ5//vm0t7Rr6L744otpOZctt9wy2XPPPZP+NmfOnLQntHsfXKkZ11B078WRRx5pvp5y6RxX5uGee+5JX5f7Jege/c2dqyuptO+++6bTuuTEQHh/XMkN90vt6quvTv9AOu6449KSG+XejkcccURy1llneeVZGhoa0l+M7rPofvnXSnmWE044If1j8d577/Xeh2XLlrVvE74e929GuezRvHnz0gb/4MGD0/Ij/c39Yehei/uMuOvuypW4MiWuF+tAe2/KDRj3HT7zzDMr1tX6++J+l5R/r7h/h12pNjftfveUS+e47437Lj/++OPJpz/96YrSOR/72MfSMjJZv3udkeVYHX0/svy7lOV3abl0zhlnnJF+Di+//HJK5/QBGosZHHXUUekXOHzoGl4vvfRScsABByRDhgxJ/8F1/xDrv27dtm4f9yUpe/vtt9PGoWuAur+cjjnmmPYGaF9z52HVFXTnrF/v/Pnz0y/4iBEj0n9AXGPGfXFroc6i+4fflfNw/2i5f/y322675P/9v//n3eENX4/j/tH9yle+kt5Zcf8QfeYzn/EaZP15N7vaZ08HBWr9/XG/wNwvcVenzd2hcHVFdSkN9/3Sfve73yUf+MAH0u1diYz//d//TWqB9T6498h6Paeeemr7ax81alRae+6RRx5JaoErN7LRRhul5+buErl59wt9IL43jmv8uffj2WefrVhX6+9L+fdD+Cifs7tr961vfSs9V/eddn88hq/TlQxzDfis373O6uhYHX0/sv671NHv0vL12nHHHdNzcQ1O/R1E78i5//VmmBsAAAADF3UWAQAAYKKxCAAAABONRQAAAJhoLAIAAMBEYxEAAAAmGosAAAAw0VgEAACAicYiAAAATDQWAQAYQG6++Wb57ne/29+ngTUIjUUA/eLss8+WH//4x+0/AWQzb948GTFihLz88sud2u8zn/mMrLfeevK5z32uV86rt49fi1555RXZe++95YMf/KBsv/32csMNN0g9nhuNRQD94qabbpK99tqr/SeAbFpbW+Xee++V0aNHd2q/U045RX7961/32nn19vFrUUNDg1x66aXyz3/+U+6880459dRTZenSpVJv59bQ42cHAB1YsGCBbLDBBu0P91cvgGy6GoJ2d5lcI7O39Pbxa9FGG22UPhzXeHf/nr3zzjuy9tpr19W5cWcRQJ+bPXu2nHTSSe0/gYHKNZDcHRtg3rx5UiwWZcyYMVJv58adRQB97sUXX0xzFb/97W+nPwF0bO7cufKRj3xE9t9/f/nf//3fivU77rijtLW1VSx3IciNN96428/f28evVTtmeN3ujt2RRx4pv/jFL2rqvHrq3GgsAuhz5513nvcTQMeuuuoqOfnkk9OfLpUjbKA99thjvfr8vX38alpaWqSpqUn602MdvO7m5mY56KCD5KyzzpLJkyfXzHn15LkRhgYAIBJmdqkS7jF8+PA07+tb3/qWJEnSvk2pVJKvf/3raQ9llxtm/RHkjuUaey5s7XoNjxo1Kr3b4zodHHPMMTJ06FDZeuut5fbbb6/Y9/3335frr79eTjjhBDnwwAPl6quv7vHX+vvf/14mTJggQ4YMkfXXX1+mTJnS45017rjjjvTu6Lrrrps+xyc+8Ql54YUXKq63u0buWk+dOrX9Gl900UXp9Rk0aJCMHTtWvvOd72Q6d7fvzJkzZYsttkjX77DDDun2ZR0dO8Z9Do4++mj52Mc+JkcccYS5XUfP0Z3PRnfPLevBAABAFXvttVeyzjrrJKecckryzDPPJNdcc02y1lprJVdeeWX7+mHDhiXnnXde8n//93/Jr371qySXyyV33nln1WMNHTo0ufDCC9Nt3c9CoZAccMAB6fHcshNOOCFZf/31k6VLl3r7XnXVVckuu+ySTt9yyy3JVlttlZRKpU69ln333TfZYIMNkiFDhiSbbLJJMmfOnPZ1CxYsSBoaGpKLL744efHFF5PHH388ufzyy5P33nuvR45f9vvf/z75n//5n+S5555LHn300eSTn/xkMmHChKRYLHrX+4wzzkivt3s4X//615P11lsvufrqq5Pnn38++ctf/pL84he/yHTu3/72t5Nx48Yld9xxR/LCCy8ks2bNSgYNGpTce++9HR67I3/5y1/S93uHHXZof7jnD3X0HN35bHT33LKgsQgAgMH9Et9uu+28htmZZ56ZLiuv/8hHPuLts+uuu6bbVDuW3ratrS1Ze+21kyOOOKJ92euvv+5uWSZz58719p08eXJy6aWXptOtra1po+zPf/5zj73OefPmpc/70ksvJX3prbfeSp/3iSeeaL9GO+20k7fNkiVL0sad1YCLnfuKFSvSxn3YcJ02bVpy6KGHdnjsnrAkw3N057PRFwhDAwAQsccee0gul2ufnzRpkjz33HNp71InLP3kypW8+eabVY+lty0UCmnI1IVPy1z40dH7P/vss/LQQw/JoYce2l4/7/Of/3yau9hTXGh23333Tc/lkEMOSUOg7777rvQ0d93c69hyyy1l2LBhsvnmm6fL58+f377NxIkTvX2efvrpNPfOnV9nz/3555+XZcuWyX/8x3/IOuus0/5w9SBd+LujY/eEpzM+R1c+G32FDi4AAHRDY2OjN+8ali5HLeu2elm5Uar3d41C1+tVd2hxkUGX+/aTn/wkzaXsLtc4caWs5syZk/amdaMqffOb35QHH3wwzfXrKZ/85Cdls802Sxt07vW41zl+/Pi0I0tZWAfQ5Rl29dxdrqfjeo9vsskm3n7u+i1atEh625AOzr87n42+wp1FAAAiXKNDe+CBB2SbbbZJGym9zTUS3V2wH/7wh2nv1/LjH//4R9rY+u1vf9tjz+UaIx/+8Ifl/PPPl0cffTTthexGWOopb7/9dnqX1JXLcnfZtttuu0x3L921dg2uu+++u9Pn7oa6c41Cd+fSdRDRD1dzMMuxu6svnqO3cWcRAIAI19CYPn26fPnLX5ZHHnkkvXPlGm994dZbb00bVNOmTau4g3jwwQendx2PP/74HmkQu8bMfvvtJxtuuGE6/9Zbb6UNup7ievm60OqVV16ZhurddXUlXToyePBgOfPMM9Me564R6BqF7tyeeuqp9LrEzt31Iv7a174mp512WnpHzvXEXrx4sfztb39Lw+BHHXVU9Ng9YXAH5z8Q0FgEACDCFTRevny57LbbbundRDcG8nHHHdcnz+0ag64MTLVQs2ssunIsjz/+eLeHzHQNp/vvvz8dS3jJkiVpqNg1iA844ADpKfl8Xq677jr56le/moaet912W7nsssvSsjEdceWKXK7mOeeck9aYdI3NciO5o3O/8MILZeTIkWn5nH/9619p2Z6dd95ZvvGNb3R47J7yrT54jt6Uc71c+vskAACoRa4h40bKcA0RYE1FziIAAABMNBYBAABgIgwNAAAAE3cWAQAAYKKxCAAAABONRQAAAJhoLAIAAMBEYxEAAAAmGosAAAAw0VgEAACAicYiAAAATDQWAQAAYKKxCAAAABONRQAAAIjl/wfdVC0RGcsdzgAAAABJRU5ErkJggg==", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "with astropy.visualization.quantity_support():\n", " fig, axs = plt.subplots(\n", @@ -226,7 +215,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "id": "9cb5334a-042b-4fc3-a658-550d7bb7ab09", "metadata": { "editable": true, @@ -235,48 +224,7 @@ }, "tags": [] }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "axis_input=(1, 2)\n", - "axis_output=(1, 2)\n", - "axis_input=(np.int64(-1), np.int64(-2))\n", - "axis_output=(np.int64(-1), np.int64(-2))\n", - "axis_output_orthogonal=(np.int64(-3),)\n", - "shape_orthogonal=(21,)\n", - "ax=np.int64(-2)\n", - "ax=np.int64(-1)\n", - "shape_output=(256, 21, 128)\n" - ] - }, - { - "ename": "ValueError", - "evalue": "operands could not be broadcast together with remapped shapes [original->remapped]: (21,128,128) and requested shape (128,21,128)", - "output_type": "error", - "traceback": [ - "\u001b[31m---------------------------------------------------------------------------\u001b[39m", - "\u001b[31mValueError\u001b[39m Traceback (most recent call last)", - "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[10]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m image = \u001b[43minstrument\u001b[49m\u001b[43m.\u001b[49m\u001b[43mimage\u001b[49m\u001b[43m(\u001b[49m\u001b[43mscene\u001b[49m\u001b[43m)\u001b[49m\n", - "\u001b[36mFile \u001b[39m\u001b[32m~\\Kankelborg-Group\\ctis\\ctis\\instruments\\_instruments.py:125\u001b[39m, in \u001b[36mAbstractLinearInstrument.image\u001b[39m\u001b[34m(self, scene)\u001b[39m\n\u001b[32m 117\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34mimage\u001b[39m(\n\u001b[32m 118\u001b[39m \u001b[38;5;28mself\u001b[39m,\n\u001b[32m 119\u001b[39m scene: na.FunctionArray[na.SpectralPositionalVectorArray, na.AbstractScalar],\n\u001b[32m 120\u001b[39m ) -> na.FunctionArray[na.SpectralPositionalVectorArray, na.AbstractScalar]:\n\u001b[32m 122\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m na.FunctionArray(\n\u001b[32m 123\u001b[39m inputs=\u001b[38;5;28mself\u001b[39m.coordinates_scene,\n\u001b[32m 124\u001b[39m outputs=na.regridding.regrid_from_weights(\n\u001b[32m--> \u001b[39m\u001b[32m125\u001b[39m *\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mweights\u001b[49m,\n\u001b[32m 126\u001b[39m values_input=scene.outputs,\n\u001b[32m 127\u001b[39m )\n\u001b[32m 128\u001b[39m )\n", - "\u001b[36mFile \u001b[39m\u001b[32m~\\AppData\\Local\\Programs\\Python\\Python313\\Lib\\functools.py:1026\u001b[39m, in \u001b[36mcached_property.__get__\u001b[39m\u001b[34m(self, instance, owner)\u001b[39m\n\u001b[32m 1024\u001b[39m val = cache.get(\u001b[38;5;28mself\u001b[39m.attrname, _NOT_FOUND)\n\u001b[32m 1025\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m val \u001b[38;5;129;01mis\u001b[39;00m _NOT_FOUND:\n\u001b[32m-> \u001b[39m\u001b[32m1026\u001b[39m val = \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mfunc\u001b[49m\u001b[43m(\u001b[49m\u001b[43minstance\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 1027\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m 1028\u001b[39m cache[\u001b[38;5;28mself\u001b[39m.attrname] = val\n", - "\u001b[36mFile \u001b[39m\u001b[32m~\\Kankelborg-Group\\ctis\\ctis\\instruments\\_instruments.py:247\u001b[39m, in \u001b[36mIdealInstrument.weights\u001b[39m\u001b[34m(self)\u001b[39m\n\u001b[32m 244\u001b[39m coordinates_input = \u001b[38;5;28mself\u001b[39m.distortion(\u001b[38;5;28mself\u001b[39m.coordinates_scene)\n\u001b[32m 245\u001b[39m coordinates_output = \u001b[38;5;28mself\u001b[39m.coordinates_sensor\n\u001b[32m--> \u001b[39m\u001b[32m247\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mna\u001b[49m\u001b[43m.\u001b[49m\u001b[43mregridding\u001b[49m\u001b[43m.\u001b[49m\u001b[43mweights\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 248\u001b[39m \u001b[43m \u001b[49m\u001b[43mcoordinates_input\u001b[49m\u001b[43m=\u001b[49m\u001b[43mcoordinates_input\u001b[49m\u001b[43m.\u001b[49m\u001b[43mposition\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 249\u001b[39m \u001b[43m \u001b[49m\u001b[43mcoordinates_output\u001b[49m\u001b[43m=\u001b[49m\u001b[43mcoordinates_output\u001b[49m\u001b[43m.\u001b[49m\u001b[43mposition\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 250\u001b[39m \u001b[43m \u001b[49m\u001b[43maxis_input\u001b[49m\u001b[43m=\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43maxis_scene_xy\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 251\u001b[39m \u001b[43m \u001b[49m\u001b[43maxis_output\u001b[49m\u001b[43m=\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43maxis_sensor_xy\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 252\u001b[39m \u001b[43m \u001b[49m\u001b[43mmethod\u001b[49m\u001b[43m=\u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mconservative\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[32m 253\u001b[39m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n", - "\u001b[36mFile \u001b[39m\u001b[32m~\\Kankelborg-Group\\named_arrays\\named_arrays\\regridding.py:177\u001b[39m, in \u001b[36mweights\u001b[39m\u001b[34m(coordinates_input, coordinates_output, axis_input, axis_output, method)\u001b[39m\n\u001b[32m 131\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34mweights\u001b[39m(\n\u001b[32m 132\u001b[39m coordinates_input: na.AbstractScalar | na.AbstractVectorArray,\n\u001b[32m 133\u001b[39m coordinates_output: na.AbstractScalar | na.AbstractVectorArray,\n\u001b[32m (...)\u001b[39m\u001b[32m 136\u001b[39m method: Literal[\u001b[33m'\u001b[39m\u001b[33mmultilinear\u001b[39m\u001b[33m'\u001b[39m, \u001b[33m'\u001b[39m\u001b[33mconservative\u001b[39m\u001b[33m'\u001b[39m] = \u001b[33m'\u001b[39m\u001b[33mmultilinear\u001b[39m\u001b[33m'\u001b[39m,\n\u001b[32m 137\u001b[39m ) -> \u001b[38;5;28mtuple\u001b[39m[na.AbstractScalar, \u001b[38;5;28mdict\u001b[39m[\u001b[38;5;28mstr\u001b[39m, \u001b[38;5;28mint\u001b[39m], \u001b[38;5;28mdict\u001b[39m[\u001b[38;5;28mstr\u001b[39m, \u001b[38;5;28mint\u001b[39m]]:\n\u001b[32m 138\u001b[39m \u001b[38;5;250m \u001b[39m\u001b[33;03m\"\"\"\u001b[39;00m\n\u001b[32m 139\u001b[39m \u001b[33;03m Save the results of a regridding operation as a sequence of weights,\u001b[39;00m\n\u001b[32m 140\u001b[39m \u001b[33;03m which can be used in subsequent regridding operations on the same grid.\u001b[39;00m\n\u001b[32m (...)\u001b[39m\u001b[32m 175\u001b[39m \n\u001b[32m 176\u001b[39m \u001b[33;03m \"\"\"\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m177\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mna\u001b[49m\u001b[43m.\u001b[49m\u001b[43m_named_array_function\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 178\u001b[39m \u001b[43m \u001b[49m\u001b[43mfunc\u001b[49m\u001b[43m=\u001b[49m\u001b[43mweights\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 179\u001b[39m \u001b[43m \u001b[49m\u001b[43mcoordinates_input\u001b[49m\u001b[43m=\u001b[49m\u001b[43mcoordinates_input\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 180\u001b[39m \u001b[43m \u001b[49m\u001b[43mcoordinates_output\u001b[49m\u001b[43m=\u001b[49m\u001b[43mcoordinates_output\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 181\u001b[39m \u001b[43m \u001b[49m\u001b[43maxis_input\u001b[49m\u001b[43m=\u001b[49m\u001b[43maxis_input\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 182\u001b[39m \u001b[43m \u001b[49m\u001b[43maxis_output\u001b[49m\u001b[43m=\u001b[49m\u001b[43maxis_output\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 183\u001b[39m \u001b[43m \u001b[49m\u001b[43mmethod\u001b[49m\u001b[43m=\u001b[49m\u001b[43mmethod\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 184\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n", - "\u001b[36mFile \u001b[39m\u001b[32m~\\Kankelborg-Group\\named_arrays\\named_arrays\\_named_array_functions.py:68\u001b[39m, in \u001b[36m_named_array_function\u001b[39m\u001b[34m(func, *args, **kwargs)\u001b[39m\n\u001b[32m 65\u001b[39m arrays = \u001b[38;5;28msorted\u001b[39m(arrays, key=functools.cmp_to_key(_is_subclass))\n\u001b[32m 67\u001b[39m \u001b[38;5;28;01mfor\u001b[39;00m array \u001b[38;5;129;01min\u001b[39;00m arrays:\n\u001b[32m---> \u001b[39m\u001b[32m68\u001b[39m res = \u001b[43marray\u001b[49m\u001b[43m.\u001b[49m\u001b[43m__named_array_function__\u001b[49m\u001b[43m(\u001b[49m\u001b[43mfunc\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 69\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m res \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28mNotImplemented\u001b[39m:\n\u001b[32m 70\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m res\n", - "\u001b[36mFile \u001b[39m\u001b[32m~\\Kankelborg-Group\\named_arrays\\named_arrays\\_vectors\\vectors.py:478\u001b[39m, in \u001b[36mAbstractVectorArray.__named_array_function__\u001b[39m\u001b[34m(self, func, *args, **kwargs)\u001b[39m\n\u001b[32m 475\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m vector_named_array_functions.ndfilter(func, *args, **kwargs)\n\u001b[32m 477\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m func \u001b[38;5;129;01min\u001b[39;00m vector_named_array_functions.HANDLED_FUNCTIONS:\n\u001b[32m--> \u001b[39m\u001b[32m478\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mvector_named_array_functions\u001b[49m\u001b[43m.\u001b[49m\u001b[43mHANDLED_FUNCTIONS\u001b[49m\u001b[43m[\u001b[49m\u001b[43mfunc\u001b[49m\u001b[43m]\u001b[49m\u001b[43m(\u001b[49m\u001b[43m*\u001b[49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 480\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mNotImplemented\u001b[39m\n", - "\u001b[36mFile \u001b[39m\u001b[32m~\\Kankelborg-Group\\named_arrays\\named_arrays\\_vectors\\vector_named_array_functions.py:944\u001b[39m, in \u001b[36mregridding_weights\u001b[39m\u001b[34m(coordinates_input, coordinates_output, axis_input, axis_output, method)\u001b[39m\n\u001b[32m 941\u001b[39m axis_input = \u001b[38;5;28mtuple\u001b[39m(\u001b[38;5;28mtuple\u001b[39m(shape_input).index(a) \u001b[38;5;28;01mfor\u001b[39;00m a \u001b[38;5;129;01min\u001b[39;00m axis_input)\n\u001b[32m 942\u001b[39m axis_output = \u001b[38;5;28mtuple\u001b[39m(\u001b[38;5;28mtuple\u001b[39m(shape_output).index(a) \u001b[38;5;28;01mfor\u001b[39;00m a \u001b[38;5;129;01min\u001b[39;00m axis_output)\n\u001b[32m--> \u001b[39m\u001b[32m944\u001b[39m result, _shape_input, _shape_output = \u001b[43mregridding\u001b[49m\u001b[43m.\u001b[49m\u001b[43mweights\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 945\u001b[39m \u001b[43m \u001b[49m\u001b[43mcoordinates_input\u001b[49m\u001b[43m=\u001b[49m\u001b[43mcoordinates_input\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 946\u001b[39m \u001b[43m \u001b[49m\u001b[43mcoordinates_output\u001b[49m\u001b[43m=\u001b[49m\u001b[43mcoordinates_output\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 947\u001b[39m \u001b[43m \u001b[49m\u001b[43maxis_input\u001b[49m\u001b[43m=\u001b[49m\u001b[43maxis_input\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 948\u001b[39m \u001b[43m \u001b[49m\u001b[43maxis_output\u001b[49m\u001b[43m=\u001b[49m\u001b[43maxis_output\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 949\u001b[39m \u001b[43m \u001b[49m\u001b[43mmethod\u001b[49m\u001b[43m=\u001b[49m\u001b[43mmethod\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 950\u001b[39m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 952\u001b[39m result = na.ScalarArray(result, \u001b[38;5;28mtuple\u001b[39m(shape_orthogonal))\n\u001b[32m 954\u001b[39m shape_input = \u001b[38;5;28mdict\u001b[39m(\u001b[38;5;28mzip\u001b[39m(shape_input, _shape_input))\n", - "\u001b[36mFile \u001b[39m\u001b[32m~\\Kankelborg-Group\\regridding\\regridding\\_weights\\_weights.py:131\u001b[39m, in \u001b[36mweights\u001b[39m\u001b[34m(coordinates_input, coordinates_output, axis_input, axis_output, method)\u001b[39m\n\u001b[32m 124\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m _weights_multilinear(\n\u001b[32m 125\u001b[39m coordinates_input=coordinates_input,\n\u001b[32m 126\u001b[39m coordinates_output=coordinates_output,\n\u001b[32m 127\u001b[39m axis_input=axis_input,\n\u001b[32m 128\u001b[39m axis_output=axis_output,\n\u001b[32m 129\u001b[39m )\n\u001b[32m 130\u001b[39m \u001b[38;5;28;01melif\u001b[39;00m method == \u001b[33m\"\u001b[39m\u001b[33mconservative\u001b[39m\u001b[33m\"\u001b[39m:\n\u001b[32m--> \u001b[39m\u001b[32m131\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43m_weights_conservative\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 132\u001b[39m \u001b[43m \u001b[49m\u001b[43mcoordinates_input\u001b[49m\u001b[43m=\u001b[49m\u001b[43mcoordinates_input\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 133\u001b[39m \u001b[43m \u001b[49m\u001b[43mcoordinates_output\u001b[49m\u001b[43m=\u001b[49m\u001b[43mcoordinates_output\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 134\u001b[39m \u001b[43m \u001b[49m\u001b[43maxis_input\u001b[49m\u001b[43m=\u001b[49m\u001b[43maxis_input\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 135\u001b[39m \u001b[43m \u001b[49m\u001b[43maxis_output\u001b[49m\u001b[43m=\u001b[49m\u001b[43maxis_output\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 136\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 137\u001b[39m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[32m 138\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33munrecognized method \u001b[39m\u001b[33m'\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mmethod\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m'\u001b[39m\u001b[33m\"\u001b[39m)\n", - "\u001b[36mFile \u001b[39m\u001b[32m~\\Kankelborg-Group\\regridding\\regridding\\_weights\\_weights_conservative.py:25\u001b[39m, in \u001b[36m_weights_conservative\u001b[39m\u001b[34m(coordinates_input, coordinates_output, axis_input, axis_output)\u001b[39m\n\u001b[32m 11\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34m_weights_conservative\u001b[39m(\n\u001b[32m 12\u001b[39m coordinates_input: \u001b[38;5;28mtuple\u001b[39m[np.ndarray, ...],\n\u001b[32m 13\u001b[39m coordinates_output: \u001b[38;5;28mtuple\u001b[39m[np.ndarray, ...],\n\u001b[32m 14\u001b[39m axis_input: \u001b[38;5;28;01mNone\u001b[39;00m | \u001b[38;5;28mint\u001b[39m | Sequence[\u001b[38;5;28mint\u001b[39m] = \u001b[38;5;28;01mNone\u001b[39;00m,\n\u001b[32m 15\u001b[39m axis_output: \u001b[38;5;28;01mNone\u001b[39;00m | \u001b[38;5;28mint\u001b[39m | Sequence[\u001b[38;5;28mint\u001b[39m] = \u001b[38;5;28;01mNone\u001b[39;00m,\n\u001b[32m 16\u001b[39m ) -> \u001b[38;5;28mtuple\u001b[39m[np.ndarray, \u001b[38;5;28mtuple\u001b[39m[\u001b[38;5;28mint\u001b[39m, ...], \u001b[38;5;28mtuple\u001b[39m[\u001b[38;5;28mint\u001b[39m, ...]]:\n\u001b[32m 17\u001b[39m (\n\u001b[32m 18\u001b[39m coordinates_input,\n\u001b[32m 19\u001b[39m coordinates_output,\n\u001b[32m 20\u001b[39m axis_input,\n\u001b[32m 21\u001b[39m axis_output,\n\u001b[32m 22\u001b[39m shape_input,\n\u001b[32m 23\u001b[39m shape_output,\n\u001b[32m 24\u001b[39m shape_orthogonal,\n\u001b[32m---> \u001b[39m\u001b[32m25\u001b[39m ) = \u001b[43m_util\u001b[49m\u001b[43m.\u001b[49m\u001b[43m_normalize_input_output_coordinates\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 26\u001b[39m \u001b[43m \u001b[49m\u001b[43mcoordinates_input\u001b[49m\u001b[43m=\u001b[49m\u001b[43mcoordinates_input\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 27\u001b[39m \u001b[43m \u001b[49m\u001b[43mcoordinates_output\u001b[49m\u001b[43m=\u001b[49m\u001b[43mcoordinates_output\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 28\u001b[39m \u001b[43m \u001b[49m\u001b[43maxis_input\u001b[49m\u001b[43m=\u001b[49m\u001b[43maxis_input\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 29\u001b[39m \u001b[43m \u001b[49m\u001b[43maxis_output\u001b[49m\u001b[43m=\u001b[49m\u001b[43maxis_output\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 30\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 32\u001b[39m shape_values_input = \u001b[38;5;28mlist\u001b[39m(shape_input)\n\u001b[32m 33\u001b[39m \u001b[38;5;28;01mfor\u001b[39;00m ax \u001b[38;5;129;01min\u001b[39;00m axis_input:\n", - "\u001b[36mFile \u001b[39m\u001b[32m~\\Kankelborg-Group\\regridding\\regridding\\_util.py:106\u001b[39m, in \u001b[36m_normalize_input_output_coordinates\u001b[39m\u001b[34m(coordinates_input, coordinates_output, axis_input, axis_output)\u001b[39m\n\u001b[32m 102\u001b[39m shape_output = \u001b[38;5;28mtuple\u001b[39m(\u001b[38;5;28mreversed\u001b[39m(shape_output))\n\u001b[32m 104\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mshape_output\u001b[38;5;132;01m=}\u001b[39;00m\u001b[33m\"\u001b[39m)\n\u001b[32m--> \u001b[39m\u001b[32m106\u001b[39m coordinates_input = \u001b[38;5;28;43mtuple\u001b[39;49m\u001b[43m(\u001b[49m\n\u001b[32m 107\u001b[39m \u001b[43m \u001b[49m\u001b[43mnp\u001b[49m\u001b[43m.\u001b[49m\u001b[43mbroadcast_to\u001b[49m\u001b[43m(\u001b[49m\u001b[43mcoord\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mshape_input\u001b[49m\u001b[43m)\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43;01mfor\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mcoord\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;129;43;01min\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mcoordinates_input\u001b[49m\n\u001b[32m 108\u001b[39m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 109\u001b[39m coordinates_output = \u001b[38;5;28mtuple\u001b[39m(\n\u001b[32m 110\u001b[39m np.broadcast_to(coord, shape_output) \u001b[38;5;28;01mfor\u001b[39;00m coord \u001b[38;5;129;01min\u001b[39;00m coordinates_output\n\u001b[32m 111\u001b[39m )\n\u001b[32m 113\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m (\n\u001b[32m 114\u001b[39m coordinates_input,\n\u001b[32m 115\u001b[39m coordinates_output,\n\u001b[32m (...)\u001b[39m\u001b[32m 120\u001b[39m shape_orthogonal,\n\u001b[32m 121\u001b[39m )\n", - "\u001b[36mFile \u001b[39m\u001b[32m~\\Kankelborg-Group\\regridding\\regridding\\_util.py:107\u001b[39m, in \u001b[36m\u001b[39m\u001b[34m(.0)\u001b[39m\n\u001b[32m 102\u001b[39m shape_output = \u001b[38;5;28mtuple\u001b[39m(\u001b[38;5;28mreversed\u001b[39m(shape_output))\n\u001b[32m 104\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mshape_output\u001b[38;5;132;01m=}\u001b[39;00m\u001b[33m\"\u001b[39m)\n\u001b[32m 106\u001b[39m coordinates_input = \u001b[38;5;28mtuple\u001b[39m(\n\u001b[32m--> \u001b[39m\u001b[32m107\u001b[39m \u001b[43mnp\u001b[49m\u001b[43m.\u001b[49m\u001b[43mbroadcast_to\u001b[49m\u001b[43m(\u001b[49m\u001b[43mcoord\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mshape_input\u001b[49m\u001b[43m)\u001b[49m \u001b[38;5;28;01mfor\u001b[39;00m coord \u001b[38;5;129;01min\u001b[39;00m coordinates_input\n\u001b[32m 108\u001b[39m )\n\u001b[32m 109\u001b[39m coordinates_output = \u001b[38;5;28mtuple\u001b[39m(\n\u001b[32m 110\u001b[39m np.broadcast_to(coord, shape_output) \u001b[38;5;28;01mfor\u001b[39;00m coord \u001b[38;5;129;01min\u001b[39;00m coordinates_output\n\u001b[32m 111\u001b[39m )\n\u001b[32m 113\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m (\n\u001b[32m 114\u001b[39m coordinates_input,\n\u001b[32m 115\u001b[39m coordinates_output,\n\u001b[32m (...)\u001b[39m\u001b[32m 120\u001b[39m shape_orthogonal,\n\u001b[32m 121\u001b[39m )\n", - "\u001b[36mFile \u001b[39m\u001b[32m~\\AppData\\Local\\Programs\\Python\\Python313\\Lib\\site-packages\\astropy\\units\\quantity.py:1879\u001b[39m, in \u001b[36mQuantity.__array_function__\u001b[39m\u001b[34m(self, function, types, args, kwargs)\u001b[39m\n\u001b[32m 1866\u001b[39m \u001b[38;5;66;03m# A function should be in one of the following sets or dicts:\u001b[39;00m\n\u001b[32m 1867\u001b[39m \u001b[38;5;66;03m# 1. SUBCLASS_SAFE_FUNCTIONS (set), if the numpy implementation\u001b[39;00m\n\u001b[32m 1868\u001b[39m \u001b[38;5;66;03m# supports Quantity; we pass on to ndarray.__array_function__.\u001b[39;00m\n\u001b[32m (...)\u001b[39m\u001b[32m 1876\u001b[39m \u001b[38;5;66;03m# function is in none of the above, we simply call the numpy\u001b[39;00m\n\u001b[32m 1877\u001b[39m \u001b[38;5;66;03m# implementation.\u001b[39;00m\n\u001b[32m 1878\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m function \u001b[38;5;129;01min\u001b[39;00m SUBCLASS_SAFE_FUNCTIONS:\n\u001b[32m-> \u001b[39m\u001b[32m1879\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43msuper\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[43m.\u001b[49m\u001b[43m__array_function__\u001b[49m\u001b[43m(\u001b[49m\u001b[43mfunction\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mtypes\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 1881\u001b[39m \u001b[38;5;28;01melif\u001b[39;00m function \u001b[38;5;129;01min\u001b[39;00m FUNCTION_HELPERS:\n\u001b[32m 1882\u001b[39m function_helper = FUNCTION_HELPERS[function]\n", - "\u001b[36mFile \u001b[39m\u001b[32m~\\AppData\\Local\\Programs\\Python\\Python313\\Lib\\site-packages\\numpy\\lib\\_stride_tricks_impl.py:443\u001b[39m, in \u001b[36mbroadcast_to\u001b[39m\u001b[34m(array, shape, subok)\u001b[39m\n\u001b[32m 400\u001b[39m \u001b[38;5;129m@array_function_dispatch\u001b[39m(_broadcast_to_dispatcher, module=\u001b[33m'\u001b[39m\u001b[33mnumpy\u001b[39m\u001b[33m'\u001b[39m)\n\u001b[32m 401\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34mbroadcast_to\u001b[39m(array, shape, subok=\u001b[38;5;28;01mFalse\u001b[39;00m):\n\u001b[32m 402\u001b[39m \u001b[38;5;250m \u001b[39m\u001b[33;03m\"\"\"Broadcast an array to a new shape.\u001b[39;00m\n\u001b[32m 403\u001b[39m \n\u001b[32m 404\u001b[39m \u001b[33;03m Parameters\u001b[39;00m\n\u001b[32m (...)\u001b[39m\u001b[32m 441\u001b[39m \u001b[33;03m [1, 2, 3]])\u001b[39;00m\n\u001b[32m 442\u001b[39m \u001b[33;03m \"\"\"\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m443\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43m_broadcast_to\u001b[49m\u001b[43m(\u001b[49m\u001b[43marray\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mshape\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43msubok\u001b[49m\u001b[43m=\u001b[49m\u001b[43msubok\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mreadonly\u001b[49m\u001b[43m=\u001b[49m\u001b[38;5;28;43;01mTrue\u001b[39;49;00m\u001b[43m)\u001b[49m\n", - "\u001b[36mFile \u001b[39m\u001b[32m~\\AppData\\Local\\Programs\\Python\\Python313\\Lib\\site-packages\\numpy\\lib\\_stride_tricks_impl.py:382\u001b[39m, in \u001b[36m_broadcast_to\u001b[39m\u001b[34m(array, shape, subok, readonly)\u001b[39m\n\u001b[32m 379\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[33m'\u001b[39m\u001b[33mall elements of broadcast shape must be non-\u001b[39m\u001b[33m'\u001b[39m\n\u001b[32m 380\u001b[39m \u001b[33m'\u001b[39m\u001b[33mnegative\u001b[39m\u001b[33m'\u001b[39m)\n\u001b[32m 381\u001b[39m extras = []\n\u001b[32m--> \u001b[39m\u001b[32m382\u001b[39m it = \u001b[43mnp\u001b[49m\u001b[43m.\u001b[49m\u001b[43mnditer\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 383\u001b[39m \u001b[43m \u001b[49m\u001b[43m(\u001b[49m\u001b[43marray\u001b[49m\u001b[43m,\u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mflags\u001b[49m\u001b[43m=\u001b[49m\u001b[43m[\u001b[49m\u001b[33;43m'\u001b[39;49m\u001b[33;43mmulti_index\u001b[39;49m\u001b[33;43m'\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[33;43m'\u001b[39;49m\u001b[33;43mrefs_ok\u001b[39;49m\u001b[33;43m'\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[33;43m'\u001b[39;49m\u001b[33;43mzerosize_ok\u001b[39;49m\u001b[33;43m'\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m \u001b[49m\u001b[43m+\u001b[49m\u001b[43m \u001b[49m\u001b[43mextras\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 384\u001b[39m \u001b[43m \u001b[49m\u001b[43mop_flags\u001b[49m\u001b[43m=\u001b[49m\u001b[43m[\u001b[49m\u001b[33;43m'\u001b[39;49m\u001b[33;43mreadonly\u001b[39;49m\u001b[33;43m'\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mitershape\u001b[49m\u001b[43m=\u001b[49m\u001b[43mshape\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43morder\u001b[49m\u001b[43m=\u001b[49m\u001b[33;43m'\u001b[39;49m\u001b[33;43mC\u001b[39;49m\u001b[33;43m'\u001b[39;49m\u001b[43m)\u001b[49m\n\u001b[32m 385\u001b[39m \u001b[38;5;28;01mwith\u001b[39;00m it:\n\u001b[32m 386\u001b[39m \u001b[38;5;66;03m# never really has writebackifcopy semantics\u001b[39;00m\n\u001b[32m 387\u001b[39m broadcast = it.itviews[\u001b[32m0\u001b[39m]\n", - "\u001b[31mValueError\u001b[39m: operands could not be broadcast together with remapped shapes [original->remapped]: (21,128,128) and requested shape (128,21,128)" - ] - } - ], + "outputs": [], "source": [ "image = instrument.image(scene)" ] @@ -294,7 +242,26 @@ }, "outputs": [], "source": [ - "image.shape" + "with astropy.visualization.quantity_support():\n", + " fig, axs = plt.subplots(\n", + " ncols=2,\n", + " gridspec_kw=dict(width_ratios=[.9,.1]),\n", + " constrained_layout=True,\n", + " )\n", + " colorbar = na.plt.rgbmesh(\n", + " C=image,\n", + " axis_wavelength=\"wavelength\",\n", + " ax=axs[0],\n", + " vmin=0,\n", + " vmax=scene.outputs.max(),\n", + " )\n", + " na.plt.pcolormesh(\n", + " C=colorbar,\n", + " axis_rgb=\"wavelength\",\n", + " ax=axs[1],\n", + " )\n", + " axs[1].yaxis.tick_right()\n", + " axs[1].yaxis.set_label_position(\"right\")" ] }, { From 3b444d3bf5b96300e3a394393e50f8db2c82c910 Mon Sep 17 00:00:00 2001 From: Roy Smart Date: Mon, 6 Apr 2026 22:23:03 -0600 Subject: [PATCH 20/28] fixes --- ctis/instruments/_instruments.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/ctis/instruments/_instruments.py b/ctis/instruments/_instruments.py index 9349353..20bd7c1 100644 --- a/ctis/instruments/_instruments.py +++ b/ctis/instruments/_instruments.py @@ -120,7 +120,7 @@ def image( ) -> na.FunctionArray[na.SpectralPositionalVectorArray, na.AbstractScalar]: return na.FunctionArray( - inputs=self.coordinates_scene, + inputs=self.coordinates_sensor, outputs=na.regridding.regrid_from_weights( *self.weights, values_input=scene.outputs, @@ -133,7 +133,7 @@ def backproject( ) -> na.FunctionArray[na.SpectralPositionalVectorArray, na.AbstractScalar]: return na.FunctionArray( - inputs=self.coordinates_sensor, + inputs=self.coordinates_scene, outputs=na.regridding.regrid_from_weights( *self.weights_transpose, values_input=image.outputs, @@ -186,6 +186,11 @@ class IdealInstrument( A grid of wavelength and position coordinates on the detector plane. """ + axis_wavelength: str + """ + The logical axis corresponding to changing wavelength. + """ + axis_scene_xy: tuple[str, str] """ The logical axes in :attr:`coordinates_scene` corresponding to changing @@ -242,6 +247,9 @@ def weights(self) -> tuple[na.AbstractScalar, dict[str, int], dict[str, int]]: coordinates_input = self.distortion(self.coordinates_scene) coordinates_output = self.coordinates_sensor + coordinates_input = coordinates_input.cell_centers(self.axis_wavelength) + coordinates_output = coordinates_output.cell_centers(self.axis_wavelength) + return na.regridding.weights( coordinates_input=coordinates_input.position, coordinates_output=coordinates_output.position, From 3276055ad1e8e70441ab3106738856369020e84a Mon Sep 17 00:00:00 2001 From: Roy Smart Date: Mon, 6 Apr 2026 22:24:02 -0600 Subject: [PATCH 21/28] update named-arrays --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ebacb30..3e5a422 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,7 @@ classifiers = [ ] dependencies = [ "astropy", - "named-arrays~=1.0", + "named-arrays~=1.1", ] dynamic = ["version"] From 252ebdef9d4baa09dff265b7c1b94edbea539577 Mon Sep 17 00:00:00 2001 From: Roy Smart Date: Mon, 6 Apr 2026 22:24:31 -0600 Subject: [PATCH 22/28] improved tutorial --- docs/tutorials/ideal-instrument.ipynb | 257 +++++++++++++++++++++----- 1 file changed, 209 insertions(+), 48 deletions(-) diff --git a/docs/tutorials/ideal-instrument.ipynb b/docs/tutorials/ideal-instrument.ipynb index 88a7ca7..a416b49 100644 --- a/docs/tutorials/ideal-instrument.ipynb +++ b/docs/tutorials/ideal-instrument.ipynb @@ -22,20 +22,22 @@ { "cell_type": "code", "execution_count": null, - "id": "initial_id", - "metadata": { - "ExecuteTime": { - "end_time": "2026-04-03T22:06:30.387380200Z", - "start_time": "2026-04-03T22:06:25.022691200Z" - }, - "editable": true, - "slideshow": { - "slide_type": "" - }, - "tags": [] - }, + "id": "d7f05a2ae5d00984", + "metadata": {}, "outputs": [], "source": [ + "# %matplotlib widget" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b11a8acd771d3769", + "metadata": {}, + "outputs": [], + "source": [ + "import IPython.display\n", + "import numpy as np\n", "import matplotlib.pyplot as plt\n", "import astropy.units as u\n", "import astropy.visualization\n", @@ -44,19 +46,22 @@ ] }, { - "cell_type": "code", - "execution_count": null, + "cell_type": "raw", "id": "30daf494-91a5-4c14-9a7d-37e7ce757757", "metadata": { + "ExecuteTime": { + "end_time": "2026-04-07T03:07:00.732675600Z", + "start_time": "2026-04-07T03:07:00.680675400Z" + }, "editable": true, + "raw_mimetype": "text/x-rst", "slideshow": { "slide_type": "" }, "tags": [] }, - "outputs": [], "source": [ - "wavelength_center = 171 * u.AA" + "Start by defining a grid of wavelengths in Doppler units." ] }, { @@ -64,6 +69,10 @@ "execution_count": null, "id": "f8edf09f-ee49-4d66-ab58-df4d0e642fca", "metadata": { + "ExecuteTime": { + "end_time": "2026-04-07T03:07:00.777175800Z", + "start_time": "2026-04-07T03:07:00.736675200Z" + }, "editable": true, "slideshow": { "slide_type": "" @@ -75,11 +84,30 @@ "wavelength = na.linspace(-500, 500, axis=\"wavelength\", num=21) * u.km / u.s" ] }, + { + "cell_type": "raw", + "id": "f651badc-1e53-423d-9a7e-6dce1ec9dbb1", + "metadata": { + "editable": true, + "raw_mimetype": "text/x-rst", + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Next, define a grid of positions with which to sample the observed scene." + ] + }, { "cell_type": "code", "execution_count": null, "id": "76a24148-01a8-47ab-a423-dad8a6fc8550", "metadata": { + "ExecuteTime": { + "end_time": "2026-04-07T03:07:00.792175500Z", + "start_time": "2026-04-07T03:07:00.781177500Z" + }, "editable": true, "slideshow": { "slide_type": "" @@ -91,16 +119,35 @@ "position_scene = na.Cartesian2dVectorLinearSpace(\n", " start=-10 * u.arcsec,\n", " stop=10 * u.arcsec,\n", - " axis=na.Cartesian2dVectorArray(\"fx\", \"fy\"),\n", + " axis=na.Cartesian2dVectorArray(\"scene_x\", \"scene_y\"),\n", " num=128,\n", ")" ] }, + { + "cell_type": "raw", + "id": "29ac979d-b097-463c-9004-62c42f5bb122", + "metadata": { + "editable": true, + "raw_mimetype": "text/x-rst", + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Also define a grid of positions with which to sample the sensor surface." + ] + }, { "cell_type": "code", "execution_count": null, "id": "37b5250a-11a0-4eb2-b784-2c221f2c9e24", "metadata": { + "ExecuteTime": { + "end_time": "2026-04-07T03:07:01.281675300Z", + "start_time": "2026-04-07T03:07:00.795175600Z" + }, "editable": true, "slideshow": { "slide_type": "" @@ -110,16 +157,35 @@ "outputs": [], "source": [ "position_sensor = na.Cartesian2dVectorArray(\n", - " x=na.arange(0, 256, axis=\"sx\") * u.pix,\n", - " y=na.arange(0, 128, axis=\"sy\") * u.pix,\n", + " x=na.arange(0, 128, axis=\"sensor_x\") * u.pix,\n", + " y=na.arange(0, 128, axis=\"sensor_y\") * u.pix,\n", ")" ] }, + { + "cell_type": "raw", + "id": "4e3b47d8-d355-419a-9665-6f89995f2392", + "metadata": { + "editable": true, + "raw_mimetype": "text/x-rst", + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Combine the wavelengths and positions into a single spatial-spectral vector for the scene and sensor." + ] + }, { "cell_type": "code", "execution_count": null, "id": "b051dfcf-7a27-42af-8173-c318e06b2c25", "metadata": { + "ExecuteTime": { + "end_time": "2026-04-07T03:07:01.322675500Z", + "start_time": "2026-04-07T03:07:01.287174400Z" + }, "editable": true, "slideshow": { "slide_type": "" @@ -132,11 +198,31 @@ "coordinates_sensor = na.SpectralPositionalVectorArray(wavelength, position_sensor)" ] }, + { + "cell_type": "raw", + "id": "442c826e-689f-4bbc-9b0a-bb02d8afac15", + "metadata": { + "editable": true, + "raw_mimetype": "text/x-rst", + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Define an ideal CTIS instrument, an instrument characterized by a plate scale, dispersion magnitude/angle, and the coordinates of the scene/sensor.\n", + "In this case, we'll define an instrument with two channels: one with 5 km/s dispersion and another with no dispersion." + ] + }, { "cell_type": "code", "execution_count": null, "id": "7a531ee6fdccb5e9", "metadata": { + "ExecuteTime": { + "end_time": "2026-04-07T03:07:01.340177900Z", + "start_time": "2026-04-07T03:07:01.326676Z" + }, "editable": true, "slideshow": { "slide_type": "" @@ -147,23 +233,43 @@ "source": [ "instrument = ctis.instruments.IdealInstrument(\n", " response=1,\n", - " plate_scale=1 * u.arcsec / u.pix,\n", - " dispersion=10 * u.km / u.s / u.pix,\n", + " plate_scale=.1 * u.arcsec / u.pix,\n", + " dispersion=na.ScalarArray([np.inf, 5] * u.km / u.s / u.pix, axes=\"channel\"),\n", " angle=0*u.deg,\n", " wavelength_ref=0 * u.km / u.s,\n", - " position_ref=128 * u.pix,\n", + " position_ref=64 * u.pix,\n", " coordinates_scene=coordinates_scene,\n", " coordinates_sensor=coordinates_sensor,\n", - " axis_scene_xy=(\"fx\", \"fy\"),\n", + " axis_wavelength=\"wavelength\",\n", + " axis_scene_xy=(\"scene_x\", \"scene_y\"),\n", " axis_sensor_xy=(\"sx\", \"sy\"),\n", ")" ] }, + { + "cell_type": "raw", + "id": "32dab88f-3a09-4789-8781-19806c7daf66", + "metadata": { + "editable": true, + "raw_mimetype": "text/x-rst", + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "We'll have our ideal instrument observe a scene of randomly-placed Gaussians developed by Amy R. Winebarger." + ] + }, { "cell_type": "code", "execution_count": null, "id": "c9e75ff0-1d86-425b-a9c6-af33dfc888e4", "metadata": { + "ExecuteTime": { + "end_time": "2026-04-07T03:07:01.433675100Z", + "start_time": "2026-04-07T03:07:01.355675100Z" + }, "editable": true, "slideshow": { "slide_type": "" @@ -179,17 +285,35 @@ ] }, { - "cell_type": "code", - "execution_count": null, + "cell_type": "raw", + "id": "556877e4-70d4-40f9-8eaa-3b183bc1109b", + "metadata": { + "editable": true, + "raw_mimetype": "text/x-rst", + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Display the scene of random Gaussians as a false-color RGB image." + ] + }, + { + "cell_type": "raw", "id": "0812c400-02d6-4682-a21b-2c32780fbcba", "metadata": { + "ExecuteTime": { + "end_time": "2026-04-07T03:07:01.981675400Z", + "start_time": "2026-04-07T03:07:01.436175400Z" + }, "editable": true, + "raw_mimetype": "text/x-rst", "slideshow": { "slide_type": "" }, "tags": [] }, - "outputs": [], "source": [ "with astropy.visualization.quantity_support():\n", " fig, axs = plt.subplots(\n", @@ -213,11 +337,30 @@ " axs[1].yaxis.set_label_position(\"right\")" ] }, + { + "cell_type": "raw", + "id": "424a731f-f925-460e-bb2f-d5f2a733899f", + "metadata": { + "editable": true, + "raw_mimetype": "text/x-rst", + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Image the scene onto the sensors using the :meth:`~ctis.instruments.IdealInstrument.image` method." + ] + }, { "cell_type": "code", "execution_count": null, "id": "9cb5334a-042b-4fc3-a658-550d7bb7ab09", "metadata": { + "ExecuteTime": { + "end_time": "2026-04-07T03:07:07.370674500Z", + "start_time": "2026-04-07T03:07:02.058176Z" + }, "editable": true, "slideshow": { "slide_type": "" @@ -229,10 +372,25 @@ "image = instrument.image(scene)" ] }, + { + "cell_type": "raw", + "id": "ac7a254c-ac74-4852-a60c-139ca0c952b7", + "metadata": { + "editable": true, + "raw_mimetype": "text/x-rst", + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Blink between the undispersed and dispersed channels to visualize the behavior of the instrument." + ] + }, { "cell_type": "code", "execution_count": null, - "id": "31089bac-3668-4ddd-9109-75f6cacca3a6", + "id": "97721140fbb1cebd", "metadata": { "editable": true, "slideshow": { @@ -248,35 +406,38 @@ " gridspec_kw=dict(width_ratios=[.9,.1]),\n", " constrained_layout=True,\n", " )\n", - " colorbar = na.plt.rgbmesh(\n", - " C=image,\n", + " ax, cax = axs\n", + " ani, colorbar = na.plt.rgbmovie(\n", + " instrument.dispersion,\n", + " image.inputs.wavelength,\n", + " image.inputs.position.x,\n", + " image.inputs.position.y,\n", + " C=image.outputs,\n", + " axis_time=\"channel\",\n", " axis_wavelength=\"wavelength\",\n", - " ax=axs[0],\n", + " ax=ax,\n", " vmin=0,\n", - " vmax=scene.outputs.max(),\n", + " vmax=image.outputs.max(),\n", " )\n", " na.plt.pcolormesh(\n", " C=colorbar,\n", " axis_rgb=\"wavelength\",\n", - " ax=axs[1],\n", + " ax=cax,\n", " )\n", - " axs[1].yaxis.tick_right()\n", - " axs[1].yaxis.set_label_position(\"right\")" + " ax.set_xlabel(f\"detector $x$ ({image.inputs.position.x.unit})\")\n", + " ax.set_ylabel(f\"detector $y$ ({image.inputs.position.y.unit})\")\n", + " cax.xaxis.set_ticks_position(\"top\")\n", + " cax.xaxis.set_label_position(\"top\")\n", + " cax.yaxis.tick_right()\n", + " cax.yaxis.set_label_position(\"right\")\n", + "\n", + "result = ani.to_jshtml(fps=.5)\n", + "result = IPython.display.HTML(result)\n", + "\n", + "plt.close(ani._fig)\n", + "\n", + "result" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8b06f7c7-848a-4f77-8f03-afde1b50bb47", - "metadata": { - "editable": true, - "slideshow": { - "slide_type": "" - }, - "tags": [] - }, - "outputs": [], - "source": [] } ], "metadata": { From cd486a5a532ab74f6396c044d160c02eb9156518 Mon Sep 17 00:00:00 2001 From: Roy Smart Date: Mon, 6 Apr 2026 22:50:34 -0600 Subject: [PATCH 23/28] messing with the tutorial --- docs/tutorials/ideal-instrument.ipynb | 43 ++++++++++++++++++++------- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/docs/tutorials/ideal-instrument.ipynb b/docs/tutorials/ideal-instrument.ipynb index a416b49..9e702df 100644 --- a/docs/tutorials/ideal-instrument.ipynb +++ b/docs/tutorials/ideal-instrument.ipynb @@ -33,7 +33,13 @@ "cell_type": "code", "execution_count": null, "id": "b11a8acd771d3769", - "metadata": {}, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, "outputs": [], "source": [ "import IPython.display\n", @@ -120,7 +126,7 @@ " start=-10 * u.arcsec,\n", " stop=10 * u.arcsec,\n", " axis=na.Cartesian2dVectorArray(\"scene_x\", \"scene_y\"),\n", - " num=128,\n", + " num=64,\n", ")" ] }, @@ -157,8 +163,8 @@ "outputs": [], "source": [ "position_sensor = na.Cartesian2dVectorArray(\n", - " x=na.arange(0, 128, axis=\"sensor_x\") * u.pix,\n", - " y=na.arange(0, 128, axis=\"sensor_y\") * u.pix,\n", + " x=na.arange(0, 64, axis=\"sensor_x\") * u.pix,\n", + " y=na.arange(0, 64, axis=\"sensor_y\") * u.pix,\n", ")" ] }, @@ -233,16 +239,17 @@ "source": [ "instrument = ctis.instruments.IdealInstrument(\n", " response=1,\n", - " plate_scale=.1 * u.arcsec / u.pix,\n", - " dispersion=na.ScalarArray([np.inf, 5] * u.km / u.s / u.pix, axes=\"channel\"),\n", - " angle=0*u.deg,\n", + " plate_scale=.4 * u.arcsec / u.pix,\n", + " # dispersion=na.ScalarArray([np.inf, 5] * u.km / u.s / u.pix, axes=\"channel\"),\n", + " dispersion=10 * u.km / u.s / u.pix,\n", + " angle=na.linspace(0, 360, num=51, axis=\"channel\") * u.deg,\n", " wavelength_ref=0 * u.km / u.s,\n", - " position_ref=64 * u.pix,\n", + " position_ref=32 * u.pix,\n", " coordinates_scene=coordinates_scene,\n", " coordinates_sensor=coordinates_sensor,\n", " axis_wavelength=\"wavelength\",\n", " axis_scene_xy=(\"scene_x\", \"scene_y\"),\n", - " axis_sensor_xy=(\"sx\", \"sy\"),\n", + " axis_sensor_xy=(\"sensor_x\", \"sensor_y\"),\n", ")" ] }, @@ -408,7 +415,7 @@ " )\n", " ax, cax = axs\n", " ani, colorbar = na.plt.rgbmovie(\n", - " instrument.dispersion,\n", + " instrument.angle,\n", " image.inputs.wavelength,\n", " image.inputs.position.x,\n", " image.inputs.position.y,\n", @@ -431,13 +438,27 @@ " cax.yaxis.tick_right()\n", " cax.yaxis.set_label_position(\"right\")\n", "\n", - "result = ani.to_jshtml(fps=.5)\n", + "result = ani.to_jshtml(fps=5)\n", "result = IPython.display.HTML(result)\n", "\n", "plt.close(ani._fig)\n", "\n", "result" ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e20d297f-45a9-4841-8623-afe932cd3c58", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [] } ], "metadata": { From 8725f6773cc0584f6bb79e79512c4515a6c72a12 Mon Sep 17 00:00:00 2001 From: Roy Smart Date: Tue, 7 Apr 2026 08:09:20 -0600 Subject: [PATCH 24/28] tutorial tweaks --- docs/tutorials/ideal-instrument.ipynb | 28 +++------------------------ 1 file changed, 3 insertions(+), 25 deletions(-) diff --git a/docs/tutorials/ideal-instrument.ipynb b/docs/tutorials/ideal-instrument.ipynb index 9e702df..9a90ca7 100644 --- a/docs/tutorials/ideal-instrument.ipynb +++ b/docs/tutorials/ideal-instrument.ipynb @@ -19,16 +19,6 @@ "This instrument has no point-spread function, distortion, or vignetting." ] }, - { - "cell_type": "code", - "execution_count": null, - "id": "d7f05a2ae5d00984", - "metadata": {}, - "outputs": [], - "source": [ - "# %matplotlib widget" - ] - }, { "cell_type": "code", "execution_count": null, @@ -307,7 +297,8 @@ ] }, { - "cell_type": "raw", + "cell_type": "code", + "execution_count": null, "id": "0812c400-02d6-4682-a21b-2c32780fbcba", "metadata": { "ExecuteTime": { @@ -321,6 +312,7 @@ }, "tags": [] }, + "outputs": [], "source": [ "with astropy.visualization.quantity_support():\n", " fig, axs = plt.subplots(\n", @@ -445,20 +437,6 @@ "\n", "result" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e20d297f-45a9-4841-8623-afe932cd3c58", - "metadata": { - "editable": true, - "slideshow": { - "slide_type": "" - }, - "tags": [] - }, - "outputs": [], - "source": [] } ], "metadata": { From 244ff8957fb20c3d45ca2ab689ce6fdddda9476c Mon Sep 17 00:00:00 2001 From: Roy Smart Date: Tue, 7 Apr 2026 08:54:58 -0600 Subject: [PATCH 25/28] added backprojection to the tutorial --- ctis/instruments/_instruments.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ctis/instruments/_instruments.py b/ctis/instruments/_instruments.py index 20bd7c1..47b772b 100644 --- a/ctis/instruments/_instruments.py +++ b/ctis/instruments/_instruments.py @@ -258,5 +258,6 @@ def weights(self) -> tuple[na.AbstractScalar, dict[str, int], dict[str, int]]: method="conservative", ) + @functools.cached_property def weights_transpose(self): - raise NotImplementedError + return na.regridding.transpose_weights(self.weights) From 39e940692874ab1e37d81478031415238644d2bf Mon Sep 17 00:00:00 2001 From: Roy Smart Date: Tue, 7 Apr 2026 09:03:36 -0600 Subject: [PATCH 26/28] tweak fps --- docs/tutorials/ideal-instrument.ipynb | 117 +++++++++++++++++++++++++- 1 file changed, 115 insertions(+), 2 deletions(-) diff --git a/docs/tutorials/ideal-instrument.ipynb b/docs/tutorials/ideal-instrument.ipynb index 9a90ca7..a00136f 100644 --- a/docs/tutorials/ideal-instrument.ipynb +++ b/docs/tutorials/ideal-instrument.ipynb @@ -232,7 +232,7 @@ " plate_scale=.4 * u.arcsec / u.pix,\n", " # dispersion=na.ScalarArray([np.inf, 5] * u.km / u.s / u.pix, axes=\"channel\"),\n", " dispersion=10 * u.km / u.s / u.pix,\n", - " angle=na.linspace(0, 360, num=51, axis=\"channel\") * u.deg,\n", + " angle=na.linspace(0, 360, num=36, axis=\"channel\", centers=True) * u.deg,\n", " wavelength_ref=0 * u.km / u.s,\n", " position_ref=32 * u.pix,\n", " coordinates_scene=coordinates_scene,\n", @@ -430,13 +430,126 @@ " cax.yaxis.tick_right()\n", " cax.yaxis.set_label_position(\"right\")\n", "\n", - "result = ani.to_jshtml(fps=5)\n", + "result = ani.to_jshtml(fps=10)\n", "result = IPython.display.HTML(result)\n", "\n", "plt.close(ani._fig)\n", "\n", "result" ] + }, + { + "cell_type": "raw", + "id": "a64272a9-a6be-4a5a-ac0c-bb501c93f952", + "metadata": { + "editable": true, + "raw_mimetype": "text/x-rst", + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Average along the wavelength axis to compute the grayscale image that's actually observed" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bc62448e-9542-4107-af5c-97337aa1fb50", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "image = image.mean(\"wavelength\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3f8d7b1e-8338-43f1-82e1-7fa0ff7a5500", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "backprojected = instrument.backproject(image)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4e4584a2-dd82-4899-8c45-52069afc9bc6", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "with astropy.visualization.quantity_support():\n", + " fig, axs = plt.subplots(\n", + " ncols=2,\n", + " gridspec_kw=dict(width_ratios=[.9,.1]),\n", + " constrained_layout=True,\n", + " )\n", + " ax, cax = axs\n", + " ani, colorbar = na.plt.rgbmovie(\n", + " instrument.angle,\n", + " backprojected.inputs.wavelength,\n", + " backprojected.inputs.position.x,\n", + " backprojected.inputs.position.y,\n", + " C=backprojected.outputs,\n", + " axis_time=\"channel\",\n", + " axis_wavelength=\"wavelength\",\n", + " ax=ax,\n", + " vmin=0,\n", + " vmax=backprojected.outputs.max(),\n", + " )\n", + " na.plt.pcolormesh(\n", + " C=colorbar,\n", + " axis_rgb=\"wavelength\",\n", + " ax=cax,\n", + " )\n", + " ax.set_xlabel(f\"detector $x$ ({image.inputs.position.x.unit})\")\n", + " ax.set_ylabel(f\"detector $y$ ({image.inputs.position.y.unit})\")\n", + " cax.xaxis.set_ticks_position(\"top\")\n", + " cax.xaxis.set_label_position(\"top\")\n", + " cax.yaxis.tick_right()\n", + " cax.yaxis.set_label_position(\"right\")\n", + "\n", + "result = ani.to_jshtml(fps=10)\n", + "result = IPython.display.HTML(result)\n", + "\n", + "plt.close(ani._fig)\n", + "\n", + "result" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "392c1557-b05a-4473-a510-5d30134e8d69", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [] } ], "metadata": { From 527e96a98fb3ebb869883ebb2951b9c857b57eea Mon Sep 17 00:00:00 2001 From: Roy Smart Date: Tue, 7 Apr 2026 14:28:56 -0600 Subject: [PATCH 27/28] fixed tests --- ctis/instruments/_instruments.py | 2 +- ctis/instruments/_instruments_test.py | 80 ++++++++++++++++++++++----- 2 files changed, 67 insertions(+), 15 deletions(-) diff --git a/ctis/instruments/_instruments.py b/ctis/instruments/_instruments.py index 47b772b..f817ef5 100644 --- a/ctis/instruments/_instruments.py +++ b/ctis/instruments/_instruments.py @@ -108,7 +108,7 @@ def weights(self) -> tuple[na.AbstractScalar, dict[str, int], dict[str, int]]: @property @abc.abstractmethod - def weights_transpose(self): + def weights_transpose(self) -> tuple[na.AbstractScalar, dict[str, int], dict[str, int]]: """ The contribution of each pixel on the detector to each voxel on the skyplane. diff --git a/ctis/instruments/_instruments_test.py b/ctis/instruments/_instruments_test.py index 7c6480d..998f154 100644 --- a/ctis/instruments/_instruments_test.py +++ b/ctis/instruments/_instruments_test.py @@ -1,30 +1,82 @@ import pytest import abc +import numpy as np +import astropy.units as u +import named_arrays as na import ctis +wavelength = na.linspace(-500, 500, axis="wavelength", num=21) * u.km / u.s + +position_scene = na.Cartesian2dVectorLinearSpace( + start=-10 * u.arcsec, + stop=10 * u.arcsec, + axis=na.Cartesian2dVectorArray("scene_x", "scene_y"), + num=64, +) + +position_sensor = na.Cartesian2dVectorArray( + x=na.arange(0, 64, axis="sensor_x") * u.pix, + y=na.arange(0, 64, axis="sensor_y") * u.pix, +) + +coordinates_scene = na.SpectralPositionalVectorArray(wavelength, position_scene) +coordinates_sensor = na.SpectralPositionalVectorArray(wavelength, position_sensor) + +gaussians = ctis.scenes.gaussians( + inputs=coordinates_scene, + width=na.SpectralPositionalVectorArray(30 * u.km / u.s, 1 * u.arcsec), +) + +instrument_ideal = ctis.instruments.IdealInstrument( + response=1, + plate_scale=.4 * u.arcsec / u.pix, + dispersion=10 * u.km / u.s / u.pix, + angle=0 * u.deg, + wavelength_ref=0 * u.km / u.s, + position_ref=32 * u.pix, + coordinates_scene=coordinates_scene, + coordinates_sensor=coordinates_sensor, + axis_wavelength="wavelength", + axis_scene_xy=("scene_x", "scene_y"), + axis_sensor_xy=("sensor_x", "sensor_y"), +) class AbstractTestAbstractInstrument( abc.ABC, ): - def test_project(self, a: ctis.instruments.AbstractInstrument): - result = a.project - assert hasattr(result, "__call__") - def test_deproject(self, a: ctis.instruments.AbstractInstrument): - result = a.deproject - assert hasattr(result, "__call__") + @pytest.mark.parametrize("scene", [gaussians]) + def test_image( + self, + a: ctis.instruments.AbstractInstrument, + scene: na.FunctionArray[na.SpectralPositionalVectorArray, na.AbstractScalar], + ): + result = a.image(scene) + assert np.all(result.inputs == coordinates_sensor) + assert result.outputs.sum() > 0 + + @pytest.mark.parametrize("image", [instrument_ideal.image(gaussians)]) + def test_backproject( + self, + a: ctis.instruments.AbstractInstrument, + image: na.FunctionArray[na.SpectralPositionalVectorArray, na.AbstractScalar], + ): + result = a.backproject(image) + assert np.all(result.inputs == coordinates_scene) + assert result.outputs.sum() > 0 + + +class AbstractTestAbstractLinearInstrument( + AbstractTestAbstractInstrument, +): + pass @pytest.mark.parametrize( argnames="a", - argvalues=[ - ctis.instruments.Instrument( - project=lambda x: x, - deproject=lambda x: x, - ) - ], + argvalues=[instrument_ideal] ) -class TestInstrument( - AbstractTestAbstractInstrument, +class TestIdealInstrument( + AbstractTestAbstractLinearInstrument, ): pass From 90cb9af2b74605fe1ecf5657e4c2e3206f017ece Mon Sep 17 00:00:00 2001 From: Roy Smart Date: Tue, 7 Apr 2026 14:44:39 -0600 Subject: [PATCH 28/28] ruff --- docs/tutorials/ideal-instrument.ipynb | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/tutorials/ideal-instrument.ipynb b/docs/tutorials/ideal-instrument.ipynb index a00136f..6e55d1d 100644 --- a/docs/tutorials/ideal-instrument.ipynb +++ b/docs/tutorials/ideal-instrument.ipynb @@ -33,7 +33,6 @@ "outputs": [], "source": [ "import IPython.display\n", - "import numpy as np\n", "import matplotlib.pyplot as plt\n", "import astropy.units as u\n", "import astropy.visualization\n", @@ -230,7 +229,6 @@ "instrument = ctis.instruments.IdealInstrument(\n", " response=1,\n", " plate_scale=.4 * u.arcsec / u.pix,\n", - " # dispersion=na.ScalarArray([np.inf, 5] * u.km / u.s / u.pix, axes=\"channel\"),\n", " dispersion=10 * u.km / u.s / u.pix,\n", " angle=na.linspace(0, 360, num=36, axis=\"channel\", centers=True) * u.deg,\n", " wavelength_ref=0 * u.km / u.s,\n",