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/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..bcc5400 --- /dev/null +++ b/ctis/instruments/__init__.py @@ -0,0 +1,15 @@ +""" +Models of CTIS instruments used during inversions. +""" + +from ._instruments import ( + AbstractInstrument, + AbstractLinearInstrument, + IdealInstrument, +) + +__all__ = [ + "AbstractInstrument", + "AbstractLinearInstrument", + "IdealInstrument", +] diff --git a/ctis/instruments/_instruments.py b/ctis/instruments/_instruments.py new file mode 100644 index 0000000..2256131 --- /dev/null +++ b/ctis/instruments/_instruments.py @@ -0,0 +1,338 @@ +from typing import Callable +import abc +import functools +import dataclasses +import astropy.units as u +import named_arrays as na + +__all__ = [ + "AbstractInstrument", + "AbstractLinearInstrument", + "IdealInstrument", +] + + +ProjectionCallable = Callable[ + [na.FunctionArray[na.SpectralPositionalVectorArray, na.ScalarArray]], + na.FunctionArray[na.SpectralPositionalVectorArray, na.ScalarArray], +] + + +@dataclasses.dataclass +class AbstractInstrument( + abc.ABC, +): + """ + An interface describing a general CTIS 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. + + 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 + def image( + self, + scene: na.AbstractScalar, + ) -> na.FunctionArray[na.SpectralPositionalVectorArray, na.AbstractScalar]: + r""" + The forward model of this CTIS instrument, which maps spectral radiance + on the skyplane to photons measured by the instrument's sensor. + + This method does `not` sum over wavelength, the result still has a + wavelength axis so that this method is directly invertible. + + Parameters + ---------- + scene + The spectral radiance of an observed scene, + evaluated on :attr:`coordinates_scene`, + in units equivalent to + :math:`\text{erg} \, \text{cm}^{-2} \, \text{sr}^{-1} \, \AA^{-1} \, \text{s}^{-1}`. + """ + + @abc.abstractmethod + def backproject( + self, + image: na.AbstractScalar, + ) -> na.FunctionArray[na.SpectralPositionalVectorArray, na.AbstractScalar]: + """ + The inverse of the forward model of this CTIS instrument. + Since the forward model, :meth:`image`, does not sum over wavelength, + :meth:`backproject` can find the true inverse. + + Parameters + ---------- + image + A series of images captured by a CTIS instrument, + evaluated on :attr:`coordinates_sensor`, + in units of photons. + """ + + @property + @abc.abstractmethod + def coordinates_scene(self) -> 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. + """ + + @property + @abc.abstractmethod + def coordinates_sensor(self) -> na.AbstractSpectralPositionalVectorArray: + """ + A grid of wavelength and position coordinates on the detector plane. + """ + + @property + @abc.abstractmethod + def axis_wavelength(self) -> str: + """ + The logical axis of :attr:`coordinates_scene` and :attr:`coordinates_sensor` + corresponding to changing wavelength coordinate. + """ + + @property + @abc.abstractmethod + def axis_scene_xy(self) -> tuple[str, str]: + """ + The logical axes of :attr:`coordinates_scene` corresponding to + changing position coordinate. + """ + + @property + @abc.abstractmethod + def axis_sensor_xy(self) -> tuple[str, str]: + """ + The logical axes of :attr:`coordinates_sensor` corresponding to + changing position coordinate. + """ + + +@dataclasses.dataclass +class AbstractLinearInstrument( + AbstractInstrument, +): + """ + 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]]: + """ + The contribution of each voxel on the skyplane to each pixel on the + detector. + """ + + @property + @abc.abstractmethod + 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. + """ + + @property + def _volume_scene(self) -> na.AbstractScalar: + """ + The volume of each voxel in :attr:`coordinates_scene`. + """ + coords = self.coordinates_scene + + dw = coords.wavelength.volume_cell(self.axis_wavelength) + + dA = coords.position.volume_cell(self.axis_scene_xy) + dA = na.as_named_array(dA) + dA = dA.cell_centers(self.axis_wavelength) + + dV = dw * dA + + return dV + + def image( + self, + scene: na.AbstractScalar, + ) -> na.FunctionArray[na.SpectralPositionalVectorArray, na.AbstractScalar]: + + values_input = scene * self._volume_scene + + return na.FunctionArray( + inputs=self.coordinates_sensor, + outputs=na.regridding.regrid_from_weights( + *self.weights, + values_input=values_input, + ), + ) + + def backproject( + self, + image: na.AbstractScalar, + ) -> na.FunctionArray[na.SpectralPositionalVectorArray, na.AbstractScalar]: + + values_input = image / self._volume_scene + + return na.FunctionArray( + inputs=self.coordinates_scene, + outputs=na.regridding.regrid_from_weights( + *self.weights_transpose, + values_input=values_input, + ), + ) + + +@dataclasses.dataclass +class IdealInstrument( + AbstractLinearInstrument, +): + """ + An idealized CTIS instrument which has a perfect point-spread function + and no noise. + """ + + area_effective: u.Quantity | na.AbstractScalar + r""" + The effective area of the instrument aperture in units equivalent to + :math:`\text{cm}^2`. + """ + + timedelta_exposure: u.Quantity | na.AbstractScalar + r""" + The exposure time of the instrument in units equivalent to :math:`\text{s}. + """ + + 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`""" + + 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 = dataclasses.MISSING + """ + 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 = dataclasses.MISSING + """ + A grid of wavelength and position coordinates on the detector plane. + """ + + axis_wavelength: str = dataclasses.MISSING + """ + The logical axis of :attr:`coordinates_scene` and :attr:`coordinates_sensor` + corresponding to changing wavelength coordinate. + """ + + axis_scene_xy: tuple[str, str] = dataclasses.MISSING + """ + The logical axes of :attr:`coordinates_scene` corresponding to + changing position coordinate. + """ + + axis_sensor_xy: tuple[str, str] = dataclasses.MISSING + """ + The logical axes of :attr:`coordinates_sensor` corresponding to + changing position coordinate. + """ + + def distortion(self, coordinates: na.SpectralPositionalVectorArray): + unit_wavelength = coordinates.wavelength.unit + 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 * unit_wavelength / u.arcsec, + y=0 * unit_wavelength / u.arcsec, + ), + ), + position=na.Cartesian2dMatrixArray( + x=na.SpectralPositionalVectorArray( + wavelength=1 / self.dispersion, + position=na.Cartesian2dVectorArray( + x=1 / self.plate_scale, + y=0 * u.pix / u.arcsec, + ), + ), + y=na.SpectralPositionalVectorArray( + wavelength=0 * u.pix / unit_wavelength, + position=na.Cartesian2dVectorArray( + x=0 * u.pix / u.arcsec, + y=1 / self.plate_scale, + ), + ), + ), + ) + projected_grid = disperse @ rot_grid + return 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]]: + + 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, + axis_input=self.axis_scene_xy, + axis_output=self.axis_sensor_xy, + method="conservative", + ) + + @functools.cached_property + def weights_transpose(self): + return na.regridding.transpose_weights(self.weights) + + def image( + self, + scene: na.AbstractScalar, + ) -> na.FunctionArray[na.SpectralPositionalVectorArray, na.AbstractScalar]: + + scene = scene * self.area_effective * self.timedelta_exposure + + return super().image(scene) + + def backproject( + self, + image: na.AbstractScalar, + ) -> na.FunctionArray[na.SpectralPositionalVectorArray, na.AbstractScalar]: + + image = image / (self.area_effective * self.timedelta_exposure) + + return super().backproject(image) diff --git a/ctis/instruments/_instruments_test.py b/ctis/instruments/_instruments_test.py new file mode 100644 index 0000000..bb6247b --- /dev/null +++ b/ctis/instruments/_instruments_test.py @@ -0,0 +1,93 @@ +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=na.Cartesian2dVectorArray(64, 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( + area_effective=1 * u.cm**2, + timedelta_exposure=10 * u.s, + 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, +): + + @pytest.mark.parametrize( + argnames="scene", + argvalues=[ + gaussians.outputs, + ], + ) + def test_image( + self, + a: ctis.instruments.AbstractInstrument, + scene: na.AbstractScalar, + ): + result = a.image(scene) + assert np.all(result.inputs == coordinates_sensor) + assert result.outputs.sum() > 0 + + @pytest.mark.parametrize( + argnames="image", + argvalues=[ + instrument_ideal.image(gaussians.outputs).outputs, + ], + ) + def test_backproject( + self, + a: ctis.instruments.AbstractInstrument, + image: 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=[instrument_ideal] +) +class TestIdealInstrument( + AbstractTestAbstractLinearInstrument, +): + pass 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/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..31d5449 --- /dev/null +++ b/docs/tutorials/ideal-instrument.ipynb @@ -0,0 +1,40781 @@ +{ + "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": "b11a8acd771d3769", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "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", + "import named_arrays as na\n", + "import ctis" + ] + }, + { + "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": [] + }, + "source": [ + "Start by defining a grid of Doppler velocities." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "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": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "velocity = na.linspace(-300, 300, axis=\"wavelength\", num=21) * u.km / u.s" + ] + }, + { + "cell_type": "raw", + "id": "d9237822-d2cc-48ef-9b1a-89ea1a1cfc67", + "metadata": { + "editable": true, + "raw_mimetype": "text/x-rst", + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Now, define the rest wavelength of a spectral line." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "16ed9891-0524-4511-8ce2-8a6e6fc8d06f", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "wavelength_rest = 171 * u.AA" + ] + }, + { + "cell_type": "raw", + "id": "36de3a7c-9afa-4fba-a6b0-3b233c0cc5bf", + "metadata": { + "editable": true, + "raw_mimetype": "text/x-rst", + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Use this rest wavelength to define an equivalency between wavelength and Doppler velocity." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "8e717cb3-38a3-47c3-8143-ea710bd8b473", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "doppler = u.doppler_optical(wavelength_rest)" + ] + }, + { + "cell_type": "raw", + "id": "29fbe931-e83c-4551-8884-c088b03ae608", + "metadata": { + "editable": true, + "raw_mimetype": "text/x-rst", + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "This equivalency can then be used to convert the grid of Doppler velocities into wavelength units." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "6bc737e1-fb5d-4348-9a0b-61aa6b7be09a", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "wavelength = velocity.to(u.AA, equivalencies=doppler)" + ] + }, + { + "cell_type": "raw", + "id": "f651badc-1e53-423d-9a7e-6dce1ec9dbb1", + "metadata": { + "editable": true, + "raw_mimetype": "text/x-rst", + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Next, we will define a grid of positions with which to sample the observed scene." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "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": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "position_scene = na.Cartesian2dVectorLinearSpace(\n", + " start=-10 * u.arcsec,\n", + " stop=10 * u.arcsec,\n", + " axis=na.Cartesian2dVectorArray(\"scene_x\", \"scene_y\"),\n", + " num=na.Cartesian2dVectorArray(64, 64),\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": 7, + "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": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "position_sensor = na.Cartesian2dVectorArray(\n", + " x=na.arange(0, 64, axis=\"sensor_x\") * u.pix,\n", + " y=na.arange(0, 64, 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": 8, + "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": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "coordinates_scene = na.SpectralPositionalVectorArray(velocity, position_scene)\n", + "coordinates_sensor = na.SpectralPositionalVectorArray(velocity, 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": 9, + "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": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "instrument = ctis.instruments.IdealInstrument(\n", + " area_effective=1 * u.cm**2,\n", + " timedelta_exposure=10 * u.s,\n", + " plate_scale=.4 * u.arcsec / u.pix,\n", + " dispersion=10 * u.km / u.s / u.pix,\n", + " angle=na.linspace(0, 360, num=36, axis=\"channel\", endpoint=False) * u.deg,\n", + " wavelength_ref=0 * u.km / u.s,\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=(\"sensor_x\", \"sensor_y\"),\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 three Gaussians:\n", + "one at the center of the field of view (FOV) which is at rest,\n", + "another at the top of the FOV which is redshifted,\n", + "and a blueshifted one on the bottom of the FOV." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "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": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "center = na.SpectralPositionalVectorArray(\n", + " wavelength=na.ScalarArray([-100, 0, 100] * u.km / u.s, axes=\"g\"),\n", + " position=na.Cartesian2dVectorArray(\n", + " x=0 * u.arcsec,\n", + " y=na.ScalarArray([-7, 0, 7] * u.arcsec, axes=\"g\"),\n", + " )\n", + ")\n", + "width = na.SpectralPositionalVectorArray(\n", + " wavelength=na.ScalarArray([50, 100, 75] * u.km / u.s, axes=\"g\"),\n", + " position=na.ScalarArray([1, 1, 1] * u.arcsec, axes=\"g\"),\n", + ")\n", + "amplitude = 1 / np.sqrt(2 * np.pi) / width\n", + "radiance = amplitude * np.exp(-np.square(((coordinates_scene - center) / width)) / 2)\n", + "radiance = radiance.wavelength * radiance.position.x * radiance.position.y\n", + "radiance = radiance * 1000 * u.erg / u.s / u.cm**2\n", + "radiance = radiance.cell_centers((\"wavelength\", \"scene_x\", \"scene_y\"))\n", + "radiance = radiance.sum(\"g\")\n", + "\n", + "scene = na.FunctionArray(\n", + " inputs=coordinates_scene,\n", + " outputs=radiance,\n", + ")" + ] + }, + { + "cell_type": "raw", + "id": "556877e4-70d4-40f9-8eaa-3b183bc1109b", + "metadata": { + "editable": true, + "raw_mimetype": "text/x-rst", + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Display this scene as a false-color RGB image." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "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": [ + { + "data": { + "image/png": "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DmKx1dFWQR2l1KtXL1P25ptvSjAYlN69eydkuwAAQGo4nLgkGDww0C3UH1JjDB3gUlpaGu7HqHFJp06dzCXo0DJLliwxA3dDdBktTyYpM8BFm74/+eQT86hpcvS5TqGciDo8XYNCzXP0r3/9SxYtWiRTpkyR0aNHh1sBteVRm463bNliXuuvgHPPPVduuOEGM++f//ynjBkzRq688spqj4QGAABHlvfee8/0VdQ4RC9Ba0OTpsA57rjjwoHeVVddZQa36GVqHWj7/PPPy0MPPVSpG9stt9xisrDcd999ZoS09qP88MMPTSySVLwUMXz48KhDCt96663wMhs2bPDOO+88Lzc312vWrJk3fvx4r7S0NDxfl9U6X375Zbhs165d3tChQ7369et7DRs29K699lrv+++/j2nbioqKvGnTppnHVJdO+6LYn+TG/iSvdNoXxf6k1vsl+7atWrXKO+ecc7wmTZp42dnZ3rHHHuvddNNN3ubNmyst969//cs744wzzDJHHXWUd9dddx20rhdeeME74YQTvKysLO8HP/iB9+qrr3rJJuXyLAIAAKD2pMxlaAAAANQ+gkUAAABYESwCAADAimARAAAAVgSLAAAAsCJYrAZN9q03B8/Ly5NGjRpFXUbzPw4ePNgso0nB9RZAejcYF71TzNVXX20Sh+t6NRdTKG9kbVm6dKm5LVG06YMPPrDW69ev30HL33TTTZIMjj322IO27a677nLWKSoqMjk5mzZtKvXr15fLL7/8oKz6dWHDhg3mc6H3F83NzTU5vPSepiUllvs7J+H5eeyxx8w5ycnJMcnuq95pqar58+ebfKi6fPfu3WXhwoWSDPSe8D/84Q+lQYMG5t/4JZdcIuvWrXPWmTt37kHnQferrmkut6rbdajbqibrebH9m9dJ/02nwnl5++235cILLzT5fXVbXnrppUrzNWnJ1KlTpXXr1uZ7YMCAAfL5558n/N9eTa+nto8bEodgsRr0D/OQIUNk1KhRUedrknANFHW55cuXy1NPPWW+jPQft4sGipqoU7O1670k9YN/4403Sm3SIHjbtm2Vpuuvv94EJ6eccoqzriYzj6x39913S7KYMWNGpW27+eabncvfeuut8re//c38QVy2bJls3bpVLrvsMqlrmqRV7wrwhz/8wXxWHnjgAZk9e7bcdttth6ybDOdHk9BqAloNcD/66CM58cQTZdCgQQfdZjNE//1oYlsNkD/++GMTkOm0evVqqWv6udDgQ2/lpf9m9a4MejOAvXv3Ouvpj8HI86AJfJPBD37wg0rb9c4771iXTebzovSHbeS+6PlR+r2dCudFP0P6b0ODsmj03+7DDz9s/u1rMuh69eqZf0f6IzdR//Zqej11cdyQQHWd6DGVzJkzx8vPzz+ofOHChZ7f7/e2b98eLps1a5ZJ8l1cXBx1Xf/+979NgvAPPvggXPbaa695Pp/P27Jli1dXSkpKvObNm3szZsxwLnf22Wd7t9xyi5eM2rVr5z3wwAPVXr6goMDLzMz05s+fHy777LPPzPlZsWKFl2zuvvtur3379ilxfk499VRv9OjR4dfl5eVemzZtvJkzZ0Zd/n/+53+8wYMHVyrr3bu397Of/cxLNl9//bX5jCxbtizm74y6pgmMTzzxxGovn0rnReln/7jjjvOCwWBKnReln6kFCxaEX+s+tGrVyrvnnnsqfWdpkuc///nPCfu3V9Prqe3jhsSiZTEBVqxYYS7LtGzZMlymv7x2795tWoNsdfTSc2TrnV5a0Ju16y/HuvLXv/5Vdu3aJddee+0hl3322WfNDdS7desmkydPln379kmy0MvOekm5Z8+ecs899zi7BOi9wbWVSI9/iF5uO+aYY8x5SjaFhYXSpEmTpD8/2tKuxzbyuOrnW1/bjquWRy4f+reUrOdBHepcaNeSdu3aSdu2beXiiy+2fifUNr2MqZfvOnToYK5yaFcam1Q6L/q5e+aZZ+S6664zlyZT7bxU9eWXX8r27dsrHf/8/HxzOdh2/OP5t1eT60Hqy6jrDUgH+g85MlBUodc6z1ZH+z1FysjIMH94bHVqw5/+9CfzR+Doo492Lqf3vNQvWv1js2rVKpk4caLpv/Xiiy9KXfvFL34hJ598sjmWevlMAyW9zHT//fdHXV6Pt96/s2p/VD2HdXkuolm/fr088sgjcu+99yb9+fnmm29MF41o/zb08nos/5aS7Txo14CxY8fK6aefboJxm06dOsmTTz4pPXr0MMGlnjft+qGByaH+jdUkDTS0q4xun/7bmD59upx55pnmsrL2yUzV86K031pBQYGMGDEi5c5LNKFjHMvxj+ffXk2uB6nviA0WJ02aJL///e+dy3z22WeH7PSdTvu3efNmWbRokbzwwguHXH9k30ptVdWO1/3795cvvvjCDMKoy/2JvEm7/jHQQPBnP/uZGaCQnZ0tqXp+tmzZIueee67ph6X9EZPp/BxptO+iBlaufn6qT58+ZgrRgKRLly6mD+qdd94pdeW8886r9G9Eg0f9caH/9rVfYirTH7y6f/pDKdXOC5Csjthgcfz48c5fnkovz1RHq1atDhodFhpJq/Nsdap2ENZLpTpC2lanpvdvzpw55tLtRRddFPP76R+bUMtXTQQjh3O+dNv02OrIYm1RqEqPt15u0daIyNZFPYeJOBeJ2B8dcHPOOeeYP2pPPPFE0p2faPQSeCAQOGhUueu4anksy9eFMWPGhAekxdoKlZmZabpG6HlIJvq5P+GEE6zblQrnRekglTfeeCPmFvRkPS8qdIz1eOuPvhB9fdJJJyXs315Nrgep74gNFps3b26mRNBfqJpeR4O/0KVlHY2no+26du1qraPBifYH6dWrlyl78803zeWt0B/22tw/7R+sweKwYcPMF2esPvnkE/MY+WWWLOdLt0372VS97B+ix1/3ecmSJSZljtJLttqHK7L1oa72R1sUNVDU7dRzpPuSbOcnGm3R1W3W46ojZ5V+vvW1BlzR6PHW+XqJN0T/LdXUeYiF/hvRUfULFiwwKac0Y0Cs9JLep59+Kueff74kE+2/p63O11xzTcqdl0j670P/nWt2inQ4L0o/ZxqY6fEPBYfaH177ttsydMTzb68m14M0kOABM2npq6++8j7++GNv+vTpXv369c1znb7//nszv6yszOvWrZs3cOBA75NPPvFef/11M6J48uTJ4XW89957XqdOnbzNmzeHy84991yvZ8+eZt4777zjHX/88d7QoUPrZB/feOMNM5pMRwFXpdus267bqdavX29GS3/44Yfel19+6b388stehw4dvLPOOsura8uXLzcjofU8fPHFF94zzzxjzsWwYcOs+6Nuuukm75hjjvHefPNNs199+vQxU13Tbe3YsaPXv39/83zbtm3hKRXOz3PPPWdGbc6dO9dkALjxxhu9Ro0ahTMHXHPNNd6kSZPCy//zn//0MjIyvHvvvdd8FnXUro5U//TTT726NmrUKDOCdunSpZXOw759+8LLVN0f/c5YtGiR+SyuXLnSu/LKK72cnBxvzZo1Xl0aP3682Q/9fOgxHzBggNesWTMzwjvVzkvkKF39Nzxx4sSD5iX7edG/JaG/K/o9fP/995vn+rdH3XXXXebfjf5bXrVqlXfxxRebjAj79+8Pr+NHP/qR98gjj1T73151JWo9dXHckDgEi9UwfPhw80GsOr311lvhZTZs2OCdd955Xm5urvnS1S/j0tLS8HxdVuvol3PIrl27THCoAaim2bn22mvDAWht0+3o27dv1Hm6zZH7u3HjRhN4NGnSxHyJaDAzYcIEr7Cw0Ktr+sWvKT30j7p++Xfp0sX73e9+5xUVFVn3R+mX7s9//nOvcePGXl5ennfppZdWCsjqiqb4iPbZi/ydl+znR/+A6R/xrKwsk4bj3XffrZTiR/99RXrhhRe8E044wSz/gx/8wHv11Ve9ZGA7D3qObPszduzY8L63bNnSO//8872PPvrIq2tXXHGF17p1a7NdRx11lHmtPzJS8byEaPCn52PdunUHzUv28xL6+1B1Cm2zps+5/fbbzbbqv2n98Vh1PzVlmAbx1f23F4tErae2jxsSx6f/q+vWTQAAACQn8iwCAADAimARAAAAVgSLAAAAsCJYBAAAgBXBIgAAAKwIFgEAAGBFsAgAAAArgkUAAABYHbH3hgZQt6ZMmSItW7aUHTt2mEe97zIAIPnQsgigTixYsEDOPvvs8COA5HTppZdK48aN5Sc/+YkcKTZt2iT9+vWTrl27So8ePWT+/PlyJG8XwSKAWrd161Zp1qxZeNIvPQDJ6ZZbbpGnn35ajiQZGRny4IMPyr///W/5+9//LmPHjpW9e/cesdtFsAig1i1evFjGjBkTfgRwaNqipMFBXbxvgwYN5EjSunVrOemkk8zzVq1amR+133777RG7XQSLAGrdl19+aS5thR4BIFmtXLlSysvLpW3btnKkbhcDXADUujvuuKPSI3CkKSkpkaysrLreDNNKVVZWdlC5XuJs06ZNWu9/dfZdW+2GDRsm//u//3tkb5cHAAAOS3l5ufe73/3OO/bYY72cnByvR48e3vz588Pzzz77bG/06NHeLbfc4jVt2tTr16+fKd+9e7d31VVXeXl5eV6rVq28+++/3yyry1VVtfyVV17xGjZs6D3zzDPh+WPGjDHLNGrUyGvRooX3xBNPeHv27PFGjBjh1a9f3zvuuOO8hQsXxrx/b731lnf55ZdHnffaa695p59+upefn+81adLEGzx4sLd+/fqDtj3a/utx+/3vf2+2Kysry2vbtq33m9/8xszT49etWzdzPHW9/fv3N/tS3WPuWnd1FBUVeWeeeab39NNPW5dxvUdNnY/qbFeicRkaAIDDNHPmTDMIZPbs2bJmzRq59dZb5ac//aksW7YsvMxTTz1lWtP++c9/muXUuHHjzOu//vWvpg/vP/7xD/noo48O+X7z5s2ToUOHyrPPPitXX311pffQfmzvv/++SUc1atQoGTJkiPTt29esd+DAgXLNNdfIvn37ErbvOsBC9+PDDz+UJUuWiN/vN91LgsFgpeWi7f/kyZPlrrvukttvv90M2tD90lRa27ZtM/t33XXXyWeffSZLly6Vyy67TBu4qn3MbeuuDn2fESNGyI9+9CNzvGwO9R6JPh/V3a6Eq7WwFACANKQtPdoyuHz58krlI0eO9IYOHRpuZerZs2el+dqqmJmZWak1rKCgwKzL1bL46KOPmla8pUuXHjT/jDPOCL8uKyvz6tWr511zzTXhsm3btmm05a1YsaLa+6ctes2aNfNyc3O9o4466qD9rGrnzp3mPT799NNK2xZt/7Ozs73//d//PWgdK1euNOvYsGFDXMfcte7q+Mc//uH5fD7vxBNPDE+rVq2q9vbX1PmoznbVBPosAgBwGNavX29ahn784x8f1C+vZ8+e4de9evWqNP8///mPlJaWyqmnnhouy8/Pl06dOlnf6y9/+Yt8/fXXpnXuhz/84UHzI9NQBQIBadq0qXTv3j1cFmr10nVU1xtvvOGc//nnn8vUqVPlvffek2+++Sbcorhx40bp1q2bdf+1xbC4uFj69+9/0DpPPPFEU67bPmjQINMCp3keNd9jdY65a93VccYZZxzUMlpVdd4j0eejOttVEwgWAQA4DHv27DGPr776qhx11FGV5mVnZ4ef16tX77DfSwMhvXz55JNPyimnnCI+n6/S/MzMzEqvdX5kWWj5RAYcF154obRr184MttABGLpuDRI1cItUdf9zc3Ot69TASi/LL1++3AzseOSRR+TXv/61CUjbt29/yGNeUFAgNS3Xsf11eT5qAn0WAQA4DHo3DQ1QtCWtY8eOlSZXWpMOHTqYwOGDDz4IlxUWFsr/+3//z1rnuOOOk7feektefvnlpLhF5q5du2TdunXm9p3awtalSxf57rvvqlX3+OOPNwGX9nOMRgOp008/XaZPny4ff/yx6e+od3yqzjE/1LoT4fhaeI9kQcsiAACHQRNW//KXvzQDLLSFSC8VatCnl4obNmwow4cPt9bTeRMmTJAmTZpIixYtZNq0aWaASNUWw0gnnHCCCRg1WXbojh51RS8L66XVJ554wiSM1uBt0qRJ1aqbk5MjEydOlF/96lcmENTAcOfOnWawirZMahCml5/1uGiLos7TYLS6x9y27pEjRyZk33Mc25+o90gWBIsAABymO++8U5o3b25G6GpfxEaNGsnJJ58st912m7Pe/fffLzfddJNccMEFJsjRwEPv/6uBiIv2a3zzzTdNwKiXbO+77z6pCxrYPvfcc/KLX/zCBHi6XQ8//LDZrurQUcQa8GqfR70NqAacejz0WLz99tsmEN69e7e5zK37eN5551X7mNvWnUi318J7JAOfjnKp640AAAAH0tBoHzwNjNKtdQqpi5ZFAADqiPbFW7t2rRkRrZdRZ8yYYcovvvjiut40IIxgEQCAOnTvvfeaQSLa703Ty2hibk3kDCQLLkMDAADAitQ5AAAAsCJYBAAAgBXBIgAAAKwIFgEAAGBFsAgAAAArgkUAAABYESwCAADAimARAAAAVgSLAAAAsCJYBAAAgBXBIgAAAKwIFgEAACA2/x9k7ch4q5IB0QAAAABJRU5ErkJggg==", + "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": "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": 12, + "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": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "image = instrument.image(scene.outputs)" + ] + }, + { + "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": 13, + "id": "97721140fbb1cebd", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
\n", + " \n", + "
\n", + " \n", + "
\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
\n", + "
\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
\n", + "
\n", + "
\n", + "\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "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", + " cax_twin = cax.twinx()\n", + " ani, colorbar = na.plt.rgbmovie(\n", + " instrument.angle,\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=ax,\n", + " vmin=0,\n", + " vmax=image.outputs.max(),\n", + " )\n", + " na.plt.pcolormesh(\n", + " C=colorbar,\n", + " axis_rgb=\"wavelength\",\n", + " ax=cax,\n", + " )\n", + " na.plt.pcolormesh(\n", + " colorbar.inputs.x,\n", + " colorbar.inputs.y.to(u.AA, equivalencies=doppler),\n", + " C=colorbar.outputs,\n", + " axis_rgb=\"wavelength\",\n", + " ax=cax_twin,\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": "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": 14, + "id": "bc62448e-9542-4107-af5c-97337aa1fb50", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "image_avg = image.mean(\"wavelength\")" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "3f8d7b1e-8338-43f1-82e1-7fa0ff7a5500", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "backprojected = instrument.backproject(image_avg.outputs)" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "4e4584a2-dd82-4899-8c45-52069afc9bc6", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
\n", + " \n", + "
\n", + " \n", + "
\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
\n", + "
\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
\n", + "
\n", + "
\n", + "\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "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" + ] + } + ], + "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 +} diff --git a/pyproject.toml b/pyproject.toml index bc4253a..3e5a422 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,7 @@ classifiers = [ ] dependencies = [ "astropy", - "named-arrays==0.21.0", + "named-arrays~=1.1", ] dynamic = ["version"] @@ -34,6 +34,7 @@ doc = [ "pydata-sphinx-theme", "ipykernel", "jupyter-sphinx", + "nbsphinx", "sphinx-codeautolink", "sphinx-favicon", ]