Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: Docs

on: [push]

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install tox
run: |
python -m pip install --upgrade pip
pip install tox
# tox -e docs sets skip_install, so the documentation builds without atlas,
# without Qt and without a display. It also passes -W, so a broken cross
# reference or a page missing from the toctree fails the job.
- name: Build the documentation
run: |
tox -e docs
- name: Upload the built site
uses: actions/upload-artifact@v4
with:
name: docs-html
path: docs/_build/html
retention-days: 14
2 changes: 1 addition & 1 deletion .github/workflows/pylint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v3
with:
python-version: "3.14"
python-version: "3.12"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v3
with:
python-version: "3.14"
python-version: "3.12"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
Expand Down
166 changes: 39 additions & 127 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,19 @@

**atlas** is a Python GUI for viewing FITS images, in the spirit of SAOImage DS9.

It opens files into *frames*, shows them singly or tiled, and lets you read
individual pixel counts straight off the image. Everything beyond the image
itself is a tool you switch on, so a default launch stays a plain viewer rather
than a wall of panels.

## Installation

Requires Python 3.14 or higher.
Requires Python 3.12 or higher.

```sh
pip install -e . # atlas and its runtime dependencies
pip install -e ".[zmq]" # also the optional ZMQ tool
pip install -e ".[dev]" # also pylint, for the CI checks
pip install -e ".[dev]" # also pylint and pytest, for the CI checks
```

Dependencies are declared in `pyproject.toml`.
Expand All @@ -35,144 +40,51 @@ PYTHONPATH=src python -m atlas.main --profile viewer image.fits
Run `atlas --help` for the full command line, and `--list-profiles` for the
bundled profiles.

## Frames

atlas borrows DS9's **frame** concept: each loaded image lives in its own frame,
and the display mode decides how frames appear on screen.

| Mode | What you see | Shortcut |
| --- | --- | --- |
| `single` | one frame at a time | `Ctrl+1` |
| `tile` | every frame in a grid | `Ctrl+2` |

Move between frames with `Ctrl+]` and `Ctrl+[`, or click a tile. `Ctrl+W`
closes the current frame.

## Scales

**View → Scale** decides how pixel values map onto the brightness of the
display. It is a property of the frame, not of the window, so in `tile` mode
one image can be shown on a log scale beside another on a linear one.

| Scale | What it does | Shortcut |
| --- | --- | --- |
| `linear` | brightness proportional to pixel value | `Ctrl+3` |
| `log` | stretches the faint end, compresses the bright end | `Ctrl+4` |
## What it does

Both scales first map the frame's own minimum and maximum onto the full display
range, so changing scale never clips a pixel that was visible before, it only
redistributes contrast. Only the rendered pixmap changes; the raw data is left
alone, so switching back and forth is lossless.
| | |
| --- | --- |
| **Frames** | Each image in its own frame, shown singly or tiled |
| **Scales** | Linear or log, per frame rather than per window |
| **Hover readout** | Pixel index and raw count in the status bar, always on |
| **Header panel** | The current frame's FITS header, on by default |
| **Statistics** | Mean, median, standard deviation, min, max, pixel count |
| **Histograms** | Pixel-value distributions for every frame on screen |
| **Live streams** | Follow a detector over ImageStreamIO shared memory or ZMQ |
| **Tap subtraction** | COO detector signal and reset taps |

The histogram window has its own **Log count axis** checkbox, independent of the
frame's scale. A pixel histogram is usually dominated by a single sky or bias
peak, and a log count axis is what makes the faint tail visible.
Everything except the header panel is opt-in, from a profile, a configuration
file, or `--enable` on the command line.

## Hover readout
## Documentation

Resting the cursor on a pixel puts its index and its count on the right of the
status bar:
The full documentation lives in [`docs/`](docs/):

```
(341, 169) 27,922
```
- [Installation](docs/installation.md) and [Quickstart](docs/quickstart.md)
- [Frames and scales](docs/frames.md)
- [Inspecting pixels](docs/inspecting.md): hover readout, statistics, headers, histograms
- [Live streams](docs/live.md): shared memory and ZMQ
- [Detector tools](docs/detector.md): tap subtraction
- [Command line](docs/cli.md) and [Configuration](docs/configuration.md) reference
- [Development](docs/development.md)

The indices are 0-based, `x` across and `y` down, so the pair reads directly as
`data[y, x]` in whatever you are inspecting the frame with. This is numpy's
convention rather than DS9's 1-based one, and `y` counts down because atlas
draws the first row of the array at the top of the tile.

The count comes from the raw array, so it is a detector count and does not move
when the display scale changes. Blank pixels (NaN/inf) read as a dash rather
than as a number, and colour frames report one sample per channel.

The readout is always available: it needs no configuration, adds no panel, and
does no work at all until the cursor is over a frame. It sits beside the status
messages rather than replacing them, so neither overwrites the other.

A frame is usually shown smaller than it is, in which case several data pixels
share one screen pixel and the readout names one of them. It always names the
pixel whose count it shows.

While a live stream is running, the readout re-reads the hovered pixel as each
frame arrives, so resting the cursor on one pixel shows its counts changing.
When tiling, it reports whichever tile the cursor is over, prefixed with that
frame's name, which need not be the current frame.

## Statistics

The **statistics** tool adds a dock panel summarising the current frame's pixel
values: mean, median, standard deviation, min and max, plus the pixel count.
Enable it with `--enable statistics` or `tools.statistics` in a configuration.

The figures come from the raw array, not the rendered image, so they describe
detector counts and do not move when the display scale changes. Blank pixels
(NaN/inf) are excluded and reported separately, since a single NaN would
otherwise make every statistic NaN.

Recomputing is not free. The median alone costs about ten times the other
statistics put together, roughly 30 ms on a 2048x2048 frame. So while a live
stream is running the panel refreshes at `update_hz` (2 Hz by default) rather
than on every displayed frame. Switching frames by hand still recomputes
immediately, and a hidden panel does no work at all.

## Configuration

A configuration is resolved from, in increasing order of precedence: built-in
defaults, the `--profile`, the `--config` file, then command-line overrides.
Unknown keys are rejected with an error rather than ignored, so a typo cannot
silently leave a feature switched off.

```yaml
window:
title: atlas
width_fraction: 0.8 # of the screen
height_fraction: 0.8

display:
mode: single # single | tile
tile_columns: null # null picks a roughly square grid

tools:
header: true # FITS header panel
histogram: false # pixel-intensity histograms
tap_subtraction: false # COO detector signal/reset taps
zmq: false # load frames announced over ZMQ
statistics: false # pixel statistics panel
```
Build it with [tox](https://tox.wiki/):

Any tool taking options can be written either as a bare boolean or as a section:

```yaml
tools:
tap_subtraction:
enabled: true
tap_width: 128
num_taps: 32
zmq:
enabled: true
address: tcp://localhost:5555
socket_type: SUB # SUB | PULL
bind: false
statistics:
enabled: true
update_hz: 2.0 # recompute rate while a live stream is running
```sh
tox -e docs # writes docs/_build/html/index.html
tox -e serve # rebuilds on save at http://127.0.0.1:8000
```

Individual tools can be toggled from the command line without editing anything:
## Development

```sh
python src/main.py --profile minimal --enable histogram
python src/main.py --profile detector --disable zmq
tox # the test suite and pylint, as CI runs them
tox -e tests
tox -e lint
```

### Bundled profiles

| Profile | Purpose |
| --- | --- |
| `minimal` | Image display only — no panels, no tools. |
| `viewer` | General FITS viewing: headers and histograms. |
| `detector` | COO detector work: tiled frames, tap subtraction, ZMQ. |
See [Development](docs/development.md) for the full set of environments and how
to add a documentation page.

## Reporting Issues

Expand Down
20 changes: 20 additions & 0 deletions docs/_static/custom.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/* Keep the hover-readout and status-bar samples reading as fixed-width UI
text rather than as code the reader is meant to run. */
.sample-readout pre {
font-size: 0.95rem;
}

/* Shortcut keys appear in most tables on the site; give them a little air. */
kbd {
padding: 0.1em 0.4em;
border: 1px solid var(--sy-c-border, #d0d7de);
border-bottom-width: 2px;
border-radius: 4px;
font-size: 0.85em;
white-space: nowrap;
}

/* The landing page cards read better with even heights. */
.sd-card {
height: 100%;
}
70 changes: 70 additions & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Command line

```text
atlas [files ...] [-c FILE] [-p NAME] [-m MODE] [--tile-columns N]
[--enable TOOL] [--disable TOOL] [--list-profiles] [-h]
```

Run `atlas --help` for the same list from the program itself.

## Arguments

`files`
: Zero or more FITS files, each opened into its own frame. Shell globs work:
`atlas *.fits`.

## Options

`-c FILE`, `--config FILE`
: A YAML configuration file. See [](configuration).

`-p NAME`, `--profile NAME`
: One of the bundled profiles: `minimal`, `viewer`, `detector`.

`-m MODE`, `--mode MODE`
: Display mode, `single` or `tile`, overriding the configuration.

`--tile-columns N`
: Columns to use when tiling. Omit it to let atlas pick a roughly square grid.

`--enable TOOL`
: Turn a tool on. Repeatable. Valid names are `header`, `histogram`, `shm`,
`statistics`, `tap_subtraction`, `zmq`.

`--disable TOOL`
: Turn a tool off. Repeatable.

`--list-profiles`
: Print the bundled profile names and exit, without opening a window.

`-h`, `--help`
: Print usage and exit.

## Exit codes

| Code | Meaning |
| --- | --- |
| `0` | Normal exit, including `--list-profiles` |
| `2` | The configuration was rejected; the reason is printed to stderr |

A rejected configuration is caught before any widget is created, so atlas
either starts with the features you asked for or does not start at all. It
never opens a window with a feature silently missing.

## Examples

```sh
atlas # defaults: single frame, header panel
atlas image.fits # open a file straight away
atlas *.fits --mode tile # every file in its own tile
atlas --profile minimal # image display and nothing else
atlas --config observing.yaml # your own configuration
atlas --profile detector --disable zmq
atlas image.fits --enable histogram --enable statistics
```

Running from a checkout without installing:

```sh
PYTHONPATH=src python -m atlas.main --profile viewer image.fits
```
Loading
Loading