Skip to main content

ctis

tests codecov Black Ruff Documentation Status PyPI version DOI

ctis is a Python library for simulating and inverting observations from computed tomography imaging spectrographs (CTISs), particularly those designed to observe the Sun in the extreme ultraviolet.

A slit spectrograph records the spectrum of one strip of the scene at a time. A CTIS has no slit: each of its channels disperses the entire field of view onto a sensor, so position and wavelength are mixed together along the dispersion direction of every image. Observing the same scene with several dispersion directions at once turns the recovery of the spectral cube into a limited-angle tomography problem, which is how instruments such as the Multi-Order Solar EUV Spectrograph (MOSES) and the EUV Snapshot Imaging Spectrograph (ESIS) measure the intensity, Doppler shift, and width of a spectral line over a two-dimensional field of view in a single exposure.

ctis provides models of these instruments, which map a spectral scene to the images on the sensor, and inversion algorithms, which reconstruct the scene from those images. It is built on named-arrays, so every scene and image carries named axes and physical units, and an instrument model can be built from an optika model of the optical system.

Installation

This package is published on PyPI and can be installed using pip

pip install ctis

ctis requires Python 3.12 or newer. To upgrade an existing installation, along with the versions of named-arrays and numpy that it needs, use

pip install --upgrade ctis

Features

  • IdealInstrument, a CTIS defined by its effective area, plate scale, and the magnitude and angle of the dispersion of each channel, with photon shot noise and Gaussian read noise.
  • OptikaInstrument, a CTIS whose forward model is an optika linear system, which supplies the distortion, effective area, vignetting, and sensor response.
  • A forward model, image(), which maps the spectral radiance of a scene to the electrons measured by the sensor, optionally with the uncertainty of each pixel, and its transpose, backproject().
  • MartInverter, an implementation of the multiplicative algebraic reconstruction technique (MART), which iterates until the mean χ² of the predicted images stops improving.
  • Merit functions for judging an inversion: the mean χ², and the correlation between the predicted images and the residuals.
  • gaussians(), a synthetic test scene of Gaussian blobs with different Doppler shifts, and plot_moments(), which compares the radiance, Doppler shift, and line width of a reconstruction against the true scene.

Key concepts

Scenes and images are functions of named coordinates. A scene is a FunctionArray of spectral radiance evaluated on instrument.coordinates_scene, a grid of wavelengths and positions on the sky, and the images are a FunctionArray of electrons evaluated on instrument.coordinates_sensor, the vertices of the pixels on the sensor.

The channels are the elements of a named axis. The channels of a CTIS lie along the axis named by axis_channel, and every parameter of an instrument broadcasts against it, so a four-channel instrument is defined by giving the dispersion angle four values along that axis.

An instrument is a forward model and its transpose. image() maps radiance to electrons, and backproject() spreads the electrons in each pixel back over every voxel of the scene that could have contributed to them. Back-projection is not an inverse, but together the two operations are all that an iterative inversion needs. A linear instrument computes the sparse weights relating the scene and the sensor once, and reuses them every time either operation is applied.

An inversion returns a result. Calling an inverter on a set of images returns a result containing the reconstructed solution, a success flag and message, and the merit of every iteration, so the convergence of the inversion can be inspected afterward.

Example

Simulate the images captured by an ideal four-channel CTIS observing a test scene, and reconstruct the scene from those images using MART.

import matplotlib.pyplot as plt
import astropy.units as u
import astropy.visualization
import named_arrays as na
import ctis

# Define the rest wavelength of the observed spectral line
wavelength_rest = 171 * u.AA

# Define a grid of Doppler velocities
velocity = na.linspace(-500, 500, axis="wavelength", num=21) * u.km / u.s

# Define the grid of wavelengths and sky positions
# on which to reconstruct the scene
coordinates_scene = na.DopplerPositionalVectorArray.from_velocity(
    velocity=velocity,
    wavelength_rest=wavelength_rest,
    position=na.Cartesian2dVectorLinearSpace(
        start=-10 * u.arcsec,
        stop=10 * u.arcsec,
        axis=na.Cartesian2dVectorArray("scene_x", "scene_y"),
        num=65,
    ),
)

# Define the vertices of the pixels on the sensor
coordinates_sensor = na.DopplerPositionalVectorArray.from_velocity(
    velocity=velocity,
    wavelength_rest=wavelength_rest,
    position=na.Cartesian2dVectorArray(
        x=na.arange(0, 129, axis="sensor_x") * u.pix,
        y=na.arange(0, 65, axis="sensor_y") * u.pix,
    ),
)

# Define a test scene of Gaussian blobs with different Doppler shifts
scene = ctis.scenes.gaussians(coordinates_scene)
scene = scene + scene.max() / 100

# Define an ideal CTIS with four channels,
# each dispersing the scene in a different direction
angle = na.linspace(0, 360, num=4, axis="channel", endpoint=False) * u.deg
instrument = ctis.instruments.IdealInstrument(
    area_effective=1 * u.cm**2,
    timedelta_exposure=20 * u.s,
    plate_scale=0.4 * u.arcsec / u.pix,
    dispersion=5.7 * u.mAA / u.pix,
    angle=angle + 5.64 * u.deg,
    wavelength_ref=wavelength_rest,
    position_ref=na.Cartesian2dVectorArray(64, 32) * u.pix,
    coordinates_scene=coordinates_scene,
    coordinates_sensor=coordinates_sensor,
    channel="dispersion angle = " + angle.to_string_array("%03d"),
    axis_channel="channel",
    axis_wavelength="wavelength",
    axis_scene_xy=("scene_x", "scene_y"),
    axis_sensor_xy=("sensor_x", "sensor_y"),
)

# Simulate the images captured by each channel,
# including photon shot noise
images = instrument.image(scene)

# Reconstruct the scene from the images using MART
inverter = ctis.inverters.MartInverter(instrument)
result = inverter(images)

# Plot the original and reconstructed scenes as false-color images
with astropy.visualization.quantity_support():
    fig, axs = plt.subplots(
        ncols=3,
        gridspec_kw=dict(width_ratios=[0.42, 0.42, 0.16]),
        constrained_layout=True,
        figsize=(9, 4),
    )
    for ax, s, title in zip(axs, [scene, result.solution], ["original", "reconstructed"]):
        colorbar = na.plt.rgbmesh(
            C=s,
            axis_wavelength="wavelength",
            ax=ax,
            vmin=0,
            vmax=scene.outputs.max(),
        )
        ax.set_aspect("equal")
        ax.set_title(title)
        ax.set_xlabel("$x$ (arcsec)")
        ax.set_ylabel("$y$ (arcsec)")
    na.plt.pcolormesh(
        C=colorbar,
        axis_rgb="wavelength",
        ax=axs[2],
    )
    axs[2].set_ylabel("wavelength (Å)")
    axs[2].yaxis.tick_right()
    axs[2].yaxis.set_label_position("right")

MART reconstruction of a test scene

The hue of each pixel represents the Doppler shift of the spectral line, and the brightness represents its intensity.

Documentation

The full documentation, including the API reference, tutorials, and discussions of the theory behind the inversions, is hosted at ctis.readthedocs.io.

Citation

If you use ctis in your research, please cite it. The citation metadata is kept in CITATION.cff, which the "Cite this repository" button on GitHub can export as BibTeX or APA.

Every release of ctis is archived on Zenodo with its own DOI. The concept DOI, 10.5281/zenodo.23086313, always resolves to the latest version, and the Zenodo page lists the DOI of every version. Please include the version of ctis that you used, which is given by importlib.metadata.version("ctis"). The BibTeX entry below uses the concept DOI. To cite a specific version instead, replace doi with the DOI of that version.

@software{ctis,
  author = {Smart, Roy T. and Parker, Jacob D. and Kankelborg, Charles C.},
  title = {ctis},
  version = {X.Y.Z},
  doi = {10.5281/zenodo.23086313},
  url = {https://github.com/sun-data/ctis},
}

Development

Install the package in editable mode along with its test dependencies, and run the test suite using pytest:

pip install -e .[test]
pytest

This project is formatted using black and linted using ruff, both of which are checked by continuous integration:

black .
ruff check .

To build the documentation locally:

pip install -e .[doc]
sphinx-build docs docs/_build/html

Metadata

Release files for ctis 0.4.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for ctis 0.4.0
File Size Uploaded
ctis-0.4.0.tar.gz 73.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for ctis 0.4.0
File Interpreter ABI Platform
ctis-0.4.0-py3-none-any.whl Python 3 none any Details

Total release size: 110.6 kB

Release files / ctis-0.4.0.tar.gz

Download URL ctis-0.4.0.tar.gz
Size 73.9 kB
Tags Source
SHA-256 checksum
How to use checksums
1b7630290dba1f70f2c8b10dbbfadaa4005298aa1d7195ed5f9e24bfaf29d0e8
BLAKE2b-256 checksum
How to use checksums
22ca4f1c899a9cd1e68bf596541866fe0dca5fb34c05c3bb0c6f2b2042d7cb22
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 1, 2026.

Transparency log

Release files / ctis-0.4.0-py3-none-any.whl

Download URL ctis-0.4.0-py3-none-any.whl
Size 36.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
1ddab741f3351842e1c4b4aaae60c699e6d07e26f722d2765a50e6b6f17c0c04
BLAKE2b-256 checksum
How to use checksums
ccd863a603d8512b8178e280605c53c78ff79bc7f28bdc64e3e65fdf4081c10c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 1, 2026.

Transparency log

Release history Release notifications | RSS feed

0.5.1

2 release files

0.5.0

2 release files

This release

0.4.0 This release

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.1

2 release files

0.1.0

2 release files

0.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page