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..92ad102 --- /dev/null +++ b/ctis/instruments/_instruments.py @@ -0,0 +1,107 @@ +from typing import Callable, Sequence +import abc +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 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 + (which maps detector counts to the spectral radiance of a physical scene). + """ + + @abc.abstractmethod + def image( + self, + scene: na.FunctionArray[na.SpectralPositionalVectorArray, na.AbstractScalar], + ) -> na.FunctionArray[na.SpectralPositionalVectorArray, na.AbstractScalar]: + 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 in units equivalent to + {(u.erg / (u.cm**2 * u.sr * u.AA * u.s)):latex_inline}. + """ + + @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. + """ + + +@dataclasses.dataclass +class AbstractLinearInstrument( + AbstractInstrument, +): + """ + An instrument that can be modeled using matrix multiplication. + """ + + @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 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], + ) -> na.FunctionArray[na.SpectralPositionalVectorArray, na.AbstractScalar]: + + pass + + +@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.""" diff --git a/ctis/instruments/_instruments_test.py b/ctis/instruments/_instruments_test.py new file mode 100644 index 0000000..7c6480d --- /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( + AbstractTestAbstractInstrument, +): + pass