specterm1d 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- specterm1d-0.1.0/.github/workflows/ci.yml +67 -0
- specterm1d-0.1.0/.github/workflows/release.yml +63 -0
- specterm1d-0.1.0/.gitignore +13 -0
- specterm1d-0.1.0/.readthedocs.yaml +23 -0
- specterm1d-0.1.0/AGENTS.md +43 -0
- specterm1d-0.1.0/LICENSE +29 -0
- specterm1d-0.1.0/PKG-INFO +276 -0
- specterm1d-0.1.0/README.md +234 -0
- specterm1d-0.1.0/claude_docs/plans/2026-08-24-specterm1d.md +8436 -0
- specterm1d-0.1.0/claude_docs/plans/2026-08-25-specterm1d-gui-renderer.md +2279 -0
- specterm1d-0.1.0/claude_docs/plans/2026-08-31-documentation-and-ci.md +651 -0
- specterm1d-0.1.0/claude_docs/specs/2026-08-24-specterm1d-design.md +453 -0
- specterm1d-0.1.0/claude_docs/specs/2026-08-25-specterm1d-gui-renderer-design.md +455 -0
- specterm1d-0.1.0/claude_docs/specs/2026-08-31-documentation-and-ci-design.md +146 -0
- specterm1d-0.1.0/docs/batch.md +6 -0
- specterm1d-0.1.0/docs/conf.py +23 -0
- specterm1d-0.1.0/docs/differences.md +15 -0
- specterm1d-0.1.0/docs/index.md +16 -0
- specterm1d-0.1.0/docs/install.md +6 -0
- specterm1d-0.1.0/docs/keys.md +166 -0
- specterm1d-0.1.0/docs/measuring.md +6 -0
- specterm1d-0.1.0/docs/quickstart.md +6 -0
- specterm1d-0.1.0/docs/terminals.md +28 -0
- specterm1d-0.1.0/pyproject.toml +82 -0
- specterm1d-0.1.0/setup.cfg +4 -0
- specterm1d-0.1.0/specterm1d/__init__.py +0 -0
- specterm1d-0.1.0/specterm1d/cli.py +155 -0
- specterm1d-0.1.0/specterm1d/commands/__init__.py +9 -0
- specterm1d-0.1.0/specterm1d/commands/colon.py +174 -0
- specterm1d-0.1.0/specterm1d/commands/display.py +347 -0
- specterm1d-0.1.0/specterm1d/commands/measure.py +139 -0
- specterm1d-0.1.0/specterm1d/commands/transform.py +156 -0
- specterm1d-0.1.0/specterm1d/cursorscript.py +102 -0
- specterm1d-0.1.0/specterm1d/fitting.py +433 -0
- specterm1d-0.1.0/specterm1d/io/__init__.py +8 -0
- specterm1d-0.1.0/specterm1d/io/pypeit_io.py +179 -0
- specterm1d-0.1.0/specterm1d/io/registry.py +81 -0
- specterm1d-0.1.0/specterm1d/io/specutils_io.py +100 -0
- specterm1d-0.1.0/specterm1d/keymap.py +188 -0
- specterm1d-0.1.0/specterm1d/logfile.py +79 -0
- specterm1d-0.1.0/specterm1d/plot.py +475 -0
- specterm1d-0.1.0/specterm1d/session.py +702 -0
- specterm1d-0.1.0/specterm1d/spec.py +214 -0
- specterm1d-0.1.0/specterm1d/term/__init__.py +36 -0
- specterm1d-0.1.0/specterm1d/term/base.py +92 -0
- specterm1d-0.1.0/specterm1d/term/caps.py +229 -0
- specterm1d-0.1.0/specterm1d/term/chrome.py +213 -0
- specterm1d-0.1.0/specterm1d/term/gui.py +279 -0
- specterm1d-0.1.0/specterm1d/term/halfblock.py +128 -0
- specterm1d-0.1.0/specterm1d/term/input.py +220 -0
- specterm1d-0.1.0/specterm1d/term/iterm2.py +45 -0
- specterm1d-0.1.0/specterm1d/term/kitty.py +83 -0
- specterm1d-0.1.0/specterm1d/term/sixel.py +151 -0
- specterm1d-0.1.0/specterm1d/transcript.py +35 -0
- specterm1d-0.1.0/specterm1d/view.py +239 -0
- specterm1d-0.1.0/specterm1d.egg-info/PKG-INFO +276 -0
- specterm1d-0.1.0/specterm1d.egg-info/SOURCES.txt +91 -0
- specterm1d-0.1.0/specterm1d.egg-info/dependency_links.txt +1 -0
- specterm1d-0.1.0/specterm1d.egg-info/entry_points.txt +3 -0
- specterm1d-0.1.0/specterm1d.egg-info/requires.txt +21 -0
- specterm1d-0.1.0/specterm1d.egg-info/scm_file_list.json +87 -0
- specterm1d-0.1.0/specterm1d.egg-info/scm_version.json +8 -0
- specterm1d-0.1.0/specterm1d.egg-info/top_level.txt +1 -0
- specterm1d-0.1.0/tests/__init__.py +0 -0
- specterm1d-0.1.0/tests/conftest.py +89 -0
- specterm1d-0.1.0/tests/golden/tabular_default.png +0 -0
- specterm1d-0.1.0/tests/test_caps.py +375 -0
- specterm1d-0.1.0/tests/test_colon.py +130 -0
- specterm1d-0.1.0/tests/test_cursorscript.py +70 -0
- specterm1d-0.1.0/tests/test_display_commands.py +398 -0
- specterm1d-0.1.0/tests/test_docs.py +108 -0
- specterm1d-0.1.0/tests/test_end_to_end.py +110 -0
- specterm1d-0.1.0/tests/test_fitting.py +152 -0
- specterm1d-0.1.0/tests/test_graphics_backends.py +189 -0
- specterm1d-0.1.0/tests/test_gui.py +434 -0
- specterm1d-0.1.0/tests/test_gui_session.py +395 -0
- specterm1d-0.1.0/tests/test_halfblock.py +152 -0
- specterm1d-0.1.0/tests/test_input.py +94 -0
- specterm1d-0.1.0/tests/test_keymap.py +144 -0
- specterm1d-0.1.0/tests/test_logfile.py +89 -0
- specterm1d-0.1.0/tests/test_measure.py +143 -0
- specterm1d-0.1.0/tests/test_mouse.py +272 -0
- specterm1d-0.1.0/tests/test_plot.py +383 -0
- specterm1d-0.1.0/tests/test_profiles.py +246 -0
- specterm1d-0.1.0/tests/test_pypeit_io.py +129 -0
- specterm1d-0.1.0/tests/test_registry.py +100 -0
- specterm1d-0.1.0/tests/test_session.py +314 -0
- specterm1d-0.1.0/tests/test_spec.py +123 -0
- specterm1d-0.1.0/tests/test_specutils_io.py +48 -0
- specterm1d-0.1.0/tests/test_text_chrome.py +222 -0
- specterm1d-0.1.0/tests/test_transcript.py +65 -0
- specterm1d-0.1.0/tests/test_transform.py +139 -0
- specterm1d-0.1.0/tests/test_view.py +226 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
concurrency:
|
|
9
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
10
|
+
cancel-in-progress: true
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
check:
|
|
14
|
+
name: ${{ matrix.name }}
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
env:
|
|
17
|
+
# matplotlib must never reach for a display in CI.
|
|
18
|
+
MPLBACKEND: Agg
|
|
19
|
+
strategy:
|
|
20
|
+
fail-fast: false
|
|
21
|
+
matrix:
|
|
22
|
+
include:
|
|
23
|
+
- name: codestyle
|
|
24
|
+
python: "3.14"
|
|
25
|
+
extras: dev
|
|
26
|
+
command: ruff check .
|
|
27
|
+
- name: build_docs
|
|
28
|
+
python: "3.14"
|
|
29
|
+
extras: docs
|
|
30
|
+
command: sphinx-build -W --keep-going docs docs/_build/html
|
|
31
|
+
- name: test (3.14)
|
|
32
|
+
python: "3.14"
|
|
33
|
+
extras: dev
|
|
34
|
+
command: python -m pytest -q
|
|
35
|
+
- name: test (3.13)
|
|
36
|
+
python: "3.13"
|
|
37
|
+
extras: dev
|
|
38
|
+
command: python -m pytest -q
|
|
39
|
+
steps:
|
|
40
|
+
- uses: actions/checkout@v7
|
|
41
|
+
with:
|
|
42
|
+
# setuptools_scm derives the version from the tag history, and a
|
|
43
|
+
# shallow clone has none.
|
|
44
|
+
fetch-depth: 0
|
|
45
|
+
- uses: actions/setup-python@v7
|
|
46
|
+
with:
|
|
47
|
+
python-version: ${{ matrix.python }}
|
|
48
|
+
cache: pip
|
|
49
|
+
- run: pip install -e '.[${{ matrix.extras }}]'
|
|
50
|
+
- run: ${{ matrix.command }}
|
|
51
|
+
|
|
52
|
+
package:
|
|
53
|
+
name: package
|
|
54
|
+
runs-on: ubuntu-latest
|
|
55
|
+
steps:
|
|
56
|
+
- uses: actions/checkout@v7
|
|
57
|
+
with:
|
|
58
|
+
fetch-depth: 0
|
|
59
|
+
- uses: actions/setup-python@v7
|
|
60
|
+
with:
|
|
61
|
+
python-version: "3.14"
|
|
62
|
+
cache: pip
|
|
63
|
+
- run: pip install build twine
|
|
64
|
+
# Catches a broken long description or bad metadata on the pull request
|
|
65
|
+
# that introduces it. On release day the version number is already spent.
|
|
66
|
+
- run: python -m build
|
|
67
|
+
- run: twine check --strict dist/*
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
# Publishing to PyPI is irreversible: a version number, once used, can never
|
|
4
|
+
# be reused even after the file is deleted. So the artefact is built and
|
|
5
|
+
# checked once, then uploaded from that same artefact - and TestPyPI can be
|
|
6
|
+
# rehearsed by hand before anything reaches the real index.
|
|
7
|
+
on:
|
|
8
|
+
release:
|
|
9
|
+
types: [published]
|
|
10
|
+
workflow_dispatch:
|
|
11
|
+
inputs:
|
|
12
|
+
target:
|
|
13
|
+
description: Index to publish to
|
|
14
|
+
type: choice
|
|
15
|
+
options: [testpypi, pypi]
|
|
16
|
+
default: testpypi
|
|
17
|
+
|
|
18
|
+
jobs:
|
|
19
|
+
build:
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
steps:
|
|
22
|
+
- uses: actions/checkout@v7
|
|
23
|
+
with:
|
|
24
|
+
# setuptools_scm needs the tag to produce a release version rather
|
|
25
|
+
# than a development guess.
|
|
26
|
+
fetch-depth: 0
|
|
27
|
+
- uses: actions/setup-python@v7
|
|
28
|
+
with:
|
|
29
|
+
python-version: "3.14"
|
|
30
|
+
- run: pip install build twine
|
|
31
|
+
- run: python -m build
|
|
32
|
+
- run: twine check --strict dist/*
|
|
33
|
+
- name: Refuse to publish a development version
|
|
34
|
+
# A tagless build produces something like 0.1.dev48+gae58a27, whose
|
|
35
|
+
# local-version segment PyPI rejects outright. Failing here says why,
|
|
36
|
+
# rather than leaving it to a confusing upload error.
|
|
37
|
+
run: |
|
|
38
|
+
version=$(ls dist/*.tar.gz | sed -E 's/.*specterm1d-(.*)\.tar\.gz/\1/')
|
|
39
|
+
echo "built version: $version"
|
|
40
|
+
case "$version" in
|
|
41
|
+
*dev*|*+*) echo "::error::$version is not a release version; tag the commit first"; exit 1 ;;
|
|
42
|
+
esac
|
|
43
|
+
- uses: actions/upload-artifact@v7
|
|
44
|
+
with:
|
|
45
|
+
name: dist
|
|
46
|
+
path: dist/
|
|
47
|
+
|
|
48
|
+
publish:
|
|
49
|
+
needs: build
|
|
50
|
+
runs-on: ubuntu-latest
|
|
51
|
+
# Trusted publishing: PyPI verifies this workflow's OIDC identity, so
|
|
52
|
+
# there is no API token to store or leak.
|
|
53
|
+
environment: ${{ github.event.inputs.target || 'pypi' }}
|
|
54
|
+
permissions:
|
|
55
|
+
id-token: write
|
|
56
|
+
steps:
|
|
57
|
+
- uses: actions/download-artifact@v8
|
|
58
|
+
with:
|
|
59
|
+
name: dist
|
|
60
|
+
path: dist/
|
|
61
|
+
- uses: pypa/gh-action-pypi-publish@v1.14.2
|
|
62
|
+
with:
|
|
63
|
+
repository-url: ${{ github.event.inputs.target == 'testpypi' && 'https://test.pypi.org/legacy/' || 'https://upload.pypi.org/legacy/' }}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
version: 2
|
|
2
|
+
|
|
3
|
+
build:
|
|
4
|
+
os: ubuntu-24.04
|
|
5
|
+
tools:
|
|
6
|
+
python: "3.14"
|
|
7
|
+
jobs:
|
|
8
|
+
post_checkout:
|
|
9
|
+
# setuptools_scm reads the version from the git tag, and Read the Docs
|
|
10
|
+
# clones shallow with no tags. Without this the version reads as a bare
|
|
11
|
+
# development guess.
|
|
12
|
+
- git fetch --unshallow --tags || git fetch --tags
|
|
13
|
+
|
|
14
|
+
sphinx:
|
|
15
|
+
configuration: docs/conf.py
|
|
16
|
+
fail_on_warning: true
|
|
17
|
+
|
|
18
|
+
python:
|
|
19
|
+
install:
|
|
20
|
+
- method: pip
|
|
21
|
+
path: .
|
|
22
|
+
extra_requirements:
|
|
23
|
+
- docs
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Repository Guidelines
|
|
2
|
+
|
|
3
|
+
## Project Structure & Module Organization
|
|
4
|
+
|
|
5
|
+
The `specterm1d/` package contains the application code. Core spectrum and session state live in
|
|
6
|
+
`spec.py` and `session.py`; terminal interaction is split across `cli.py`, `view.py`, `plot.py`,
|
|
7
|
+
`keymap.py`, and `cursorscript.py`. Keep new behavior close to the module that owns it rather than
|
|
8
|
+
expanding the CLI entry point. Tests live in `tests/` and generally mirror user-facing features,
|
|
9
|
+
while shared fixtures are defined in `tests/conftest.py`. Keybinding documentation belongs in
|
|
10
|
+
`docs/keys.md`. Packaging and tool configuration are centralized in `pyproject.toml`.
|
|
11
|
+
|
|
12
|
+
## Build, Test, and Development Commands
|
|
13
|
+
|
|
14
|
+
- `python -m pip install -e '.[dev]'` installs the package in editable mode with pytest, coverage,
|
|
15
|
+
and Ruff.
|
|
16
|
+
- `pytest` runs the complete default test suite under `tests/`.
|
|
17
|
+
- `pytest tests/test_fitting.py -q` runs one focused test module during development.
|
|
18
|
+
- `pytest --cov=specterm1d` reports package coverage; the project does not enforce a numeric floor.
|
|
19
|
+
- `ruff check .` checks formatting-adjacent rules, imports, common bugs, and Python 3.13 compatibility.
|
|
20
|
+
- `specterm1d --dump frame.png spectrum.fits` performs a non-interactive rendering smoke test.
|
|
21
|
+
|
|
22
|
+
## Coding Style & Naming Conventions
|
|
23
|
+
|
|
24
|
+
Use four-space indentation, a 100-character line limit, and Python 3.13 syntax. Ruff enables the
|
|
25
|
+
`E`, `F`, `W`, `I`, `B`, `C4`, `SIM`, and `RUF` rule families; fix warnings instead of adding broad
|
|
26
|
+
ignores. Use `snake_case` for modules, functions, variables, and fixtures; `PascalCase` for classes;
|
|
27
|
+
and `UPPER_CASE` for constants. Keep imports sorted and add concise docstrings where behavior or
|
|
28
|
+
terminal cleanup constraints are not obvious.
|
|
29
|
+
|
|
30
|
+
## Testing Guidelines
|
|
31
|
+
|
|
32
|
+
Write pytest tests as `tests/test_<feature>.py` with functions named `test_<behavior>`. Prefer
|
|
33
|
+
temporary paths and existing fixtures so tests never write logs or generated spectra into the
|
|
34
|
+
repository. Mark tests needing local PypeIt development-suite data with `@pytest.mark.devsuite`,
|
|
35
|
+
and skip optional dependencies with `pytest.importorskip`. Add regression coverage for every bug
|
|
36
|
+
fix, especially around renderer fallback, input handling, and numerical measurements.
|
|
37
|
+
|
|
38
|
+
## Commit & Pull Request Guidelines
|
|
39
|
+
|
|
40
|
+
Recent history uses Conventional Commit-style subjects such as `feat:`, `fix:`, and `docs:`. Keep
|
|
41
|
+
subjects imperative and focused on one logical change. Pull requests should explain the user-visible
|
|
42
|
+
effect, list verification commands, and link relevant issues. Include screenshots or dumped frames
|
|
43
|
+
for rendering changes, and call out optional-backend or terminal-specific behavior explicitly.
|
specterm1d-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, T. E. Pickering
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
and/or other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
17
|
+
contributors may be used to endorse or promote products derived from
|
|
18
|
+
this software without specific prior written permission.
|
|
19
|
+
|
|
20
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
21
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
22
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
23
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
24
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
25
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
26
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
27
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
28
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
29
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: specterm1d
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Terminal-based 1D spectrum viewer with IRAF splot keybindings
|
|
5
|
+
Author-email: "T. E. Pickering" <te.pickering@gmail.com>
|
|
6
|
+
License-Expression: BSD-3-Clause
|
|
7
|
+
Project-URL: Homepage, https://github.com/tepickering/specterm1d
|
|
8
|
+
Project-URL: Documentation, https://specterm1d.readthedocs.io
|
|
9
|
+
Project-URL: Repository, https://github.com/tepickering/specterm1d
|
|
10
|
+
Project-URL: Issues, https://github.com/tepickering/specterm1d/issues
|
|
11
|
+
Keywords: spectroscopy,astronomy,terminal,splot,fits
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: Operating System :: MacOS
|
|
16
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Astronomy
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
21
|
+
Requires-Python: >=3.13
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: numpy>=2.5
|
|
25
|
+
Requires-Dist: scipy>=1.10
|
|
26
|
+
Requires-Dist: matplotlib>=3.7
|
|
27
|
+
Requires-Dist: astropy>=6.0
|
|
28
|
+
Requires-Dist: specutils>=2.0
|
|
29
|
+
Provides-Extra: pypeit
|
|
30
|
+
Requires-Dist: pypeit>=1.14; extra == "pypeit"
|
|
31
|
+
Provides-Extra: sixel
|
|
32
|
+
Requires-Dist: libsixel-python>=0.5; extra == "sixel"
|
|
33
|
+
Provides-Extra: docs
|
|
34
|
+
Requires-Dist: sphinx>=9.1; extra == "docs"
|
|
35
|
+
Requires-Dist: myst-parser>=5.1; extra == "docs"
|
|
36
|
+
Requires-Dist: furo>=2025.12.19; extra == "docs"
|
|
37
|
+
Provides-Extra: dev
|
|
38
|
+
Requires-Dist: pytest>=7.4; extra == "dev"
|
|
39
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
40
|
+
Requires-Dist: ruff; extra == "dev"
|
|
41
|
+
Dynamic: license-file
|
|
42
|
+
|
|
43
|
+
# specterm1d
|
|
44
|
+
|
|
45
|
+
[](https://github.com/tepickering/specterm1d/actions/workflows/ci.yml)
|
|
46
|
+
|
|
47
|
+
A terminal-based viewer for 1D spectra, with IRAF `splot`'s keybindings.
|
|
48
|
+
|
|
49
|
+
Opens anything `specutils` can read — IRAF multispec, `tabular-fits`,
|
|
50
|
+
`wcs1d-fits`, SDSS, HST/COS, HST/STIS, JWST, APOGEE and more — plus pypeit's
|
|
51
|
+
`OneSpec` and `spec1d` products, including echelle files with their orders
|
|
52
|
+
grouped by object.
|
|
53
|
+
|
|
54
|
+
The point is to keep the `splot` muscle memory intact while drawing a real
|
|
55
|
+
matplotlib figure in the terminal, rather than an ASCII approximation of one.
|
|
56
|
+
|
|
57
|
+
## Install
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install specterm1d # general FITS spectra
|
|
61
|
+
pip install 'specterm1d[pypeit]' # adds OneSpec and spec1d support
|
|
62
|
+
pip install 'specterm1d[sixel]' # optional libsixel encoder
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
pypeit is deliberately optional. Nothing imports it at module scope, so the
|
|
66
|
+
tool works as a general FITS viewer without it, and files it cannot open are
|
|
67
|
+
reported clearly rather than crashing on an import.
|
|
68
|
+
|
|
69
|
+
Requires Python 3.13+ and numpy 2.5+.
|
|
70
|
+
|
|
71
|
+
## Quick start
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
specterm1d spec1d_J0935+0924.fits # or the short alias: st1d
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Arrow keys move a crosshair, `?` pages the full keymap, `q` quits.
|
|
78
|
+
|
|
79
|
+
| Flag | Effect |
|
|
80
|
+
|------|--------|
|
|
81
|
+
| `--renderer kitty\|iterm2\|sixel\|gui\|halfblock` | force a backend instead of probing |
|
|
82
|
+
| `--units nm` | start in other dispersion units (`um`, `GHz`, anything astropy knows) |
|
|
83
|
+
| `--mouse` / `--no-mouse` | override click/drag positioning (on for inline graphics) |
|
|
84
|
+
| `--format NAME` | force a loader instead of sniffing the file |
|
|
85
|
+
| `--log FILE` | measurement log path (default `splot.log`) |
|
|
86
|
+
| `--cursor FILE` | replay a keystroke script instead of reading the keyboard |
|
|
87
|
+
| `--dump OUT.png` | render one frame to a PNG and exit; needs no terminal |
|
|
88
|
+
| `--dump-size WxH` | pixel size for `--dump` (default `1200x700`) |
|
|
89
|
+
| `--debug` | show full tracebacks instead of one-line errors |
|
|
90
|
+
|
|
91
|
+
## Terminal support
|
|
92
|
+
|
|
93
|
+
One matplotlib figure is rendered to an RGBA buffer, and five interchangeable
|
|
94
|
+
backends put those pixels on screen. Axes, tick labels, error bands and fit
|
|
95
|
+
overlays therefore look the same everywhere; only the fidelity changes.
|
|
96
|
+
|
|
97
|
+
| Terminal | Backend | Notes |
|
|
98
|
+
|----------|---------|-------|
|
|
99
|
+
| kitty, Ghostty, WezTerm | kitty graphics | pixel-exact; PNG transport |
|
|
100
|
+
| iTerm2 | **graphics window** | its inline image path leaks; see below |
|
|
101
|
+
| Windows Terminal 1.22+, foot, xterm, Konsole, mlterm, contour | sixel | detected via Primary Device Attributes |
|
|
102
|
+
| **stock macOS Terminal, GNOME Terminal, Alacritty** | **graphics window** | no graphics protocol exists; see below |
|
|
103
|
+
| ssh with no display, tmux over ssh | halfblock | always available |
|
|
104
|
+
|
|
105
|
+
The halfblock backend is first-class, not a stub - it is what runs wherever
|
|
106
|
+
neither an inline protocol nor a window is available. Each cell is `▀` with the top source pixel as foreground and the
|
|
107
|
+
bottom as background, giving `cols x 2*rows` effective pixels; frames are
|
|
108
|
+
diffed so a redraw costs only the cells that changed. Terminal.app never
|
|
109
|
+
gained 24-bit colour, so there is an xterm-256 path as well as truecolor.
|
|
110
|
+
|
|
111
|
+
Because that gives a 116x43 window a 116x82 pixel figure, halfblock does not
|
|
112
|
+
let matplotlib draw the axis decoration: a 4pt tick label is 5.6 px tall
|
|
113
|
+
there, which is a smear across three cells at any font size. Instead the
|
|
114
|
+
figure is rendered full bleed with nothing but data, and the terminal paints
|
|
115
|
+
the spines, tick marks, labels, title and legend as its own glyphs at your
|
|
116
|
+
font size. The curve ends up with more pixels than it had when matplotlib was
|
|
117
|
+
spending margins on labels nobody could read.
|
|
118
|
+
|
|
119
|
+
Under tmux the kitty protocol is never probed — its passthrough is unreliable
|
|
120
|
+
— so tmux users get sixel where tmux was built with `--enable-sixel`, and
|
|
121
|
+
halfblock otherwise.
|
|
122
|
+
|
|
123
|
+
### Two-window mode
|
|
124
|
+
|
|
125
|
+
Terminals with no inline-graphics protocol get a real matplotlib window
|
|
126
|
+
instead of half-block cells. This is what IRAF `splot` did on a
|
|
127
|
+
Tektronix-emulating terminal like `xgterm`: **you point at a feature in the
|
|
128
|
+
graphics window and press a key**, while prompts and measurement results
|
|
129
|
+
scroll past in the text terminal.
|
|
130
|
+
|
|
131
|
+
The terminal is a plain scrolling transcript in this mode — no full-screen
|
|
132
|
+
layout, no raw mode, no pinned status line. The live `x`/`y`/`pix` readout
|
|
133
|
+
moves to the window title, where your eye already is. `?` and `:show` scroll
|
|
134
|
+
past rather than paging.
|
|
135
|
+
|
|
136
|
+
Every binding means the same thing in both modes; that is the point.
|
|
137
|
+
|
|
138
|
+
| terminal | renderer |
|
|
139
|
+
|---|---|
|
|
140
|
+
| kitty, Ghostty, WezTerm | kitty protocol, inline |
|
|
141
|
+
| iTerm2 | graphics window (`--renderer iterm2` to force inline) |
|
|
142
|
+
| xterm with sixel | sixel, inline |
|
|
143
|
+
| Terminal.app, GNOME Terminal, Alacritty | graphics window |
|
|
144
|
+
| xterm on Linux with X11 | graphics window |
|
|
145
|
+
| ssh with no display, tmux over ssh | half-block |
|
|
146
|
+
|
|
147
|
+
Inline graphics still win where the terminal supports them — one window beats
|
|
148
|
+
two — with iTerm2 the one exception. Half-block is the last resort: correct
|
|
149
|
+
everywhere, comfortable nowhere.
|
|
150
|
+
|
|
151
|
+
### Why iTerm2 gets a window
|
|
152
|
+
|
|
153
|
+
iTerm2 never frees an inline image. Every distinct frame costs it about a
|
|
154
|
+
decoded bitmap of resident memory for the life of the session, so panning a
|
|
155
|
+
spectrum grows the terminal process by roughly 1.7 MB per keystroke — measured
|
|
156
|
+
over 100 cursor moves on iTerm2 3.6.11, against 0.05 MB/frame for the same
|
|
157
|
+
loop drawing text. kitty's protocol replaces a placement in situ through a
|
|
158
|
+
stable image id and does not do this; OSC 1337 has neither an id nor a delete
|
|
159
|
+
verb, and nothing the application can send collects the images. Its sixel path
|
|
160
|
+
leaks too, at 4 MB/frame, so both inline backends step aside where a window is
|
|
161
|
+
available. `--renderer iterm2` still forces the inline path.
|
|
162
|
+
|
|
163
|
+
This is not specific to specterm1d. It has been reported upstream twice —
|
|
164
|
+
[#3943](https://gitlab.com/gnachman/iterm2/-/issues/3943) in 2015 and
|
|
165
|
+
[#10420](https://gitlab.com/gnachman/iterm2/-/issues/10420) in 2022, the
|
|
166
|
+
latter reaching about 20 GB and surviving a scrollback clear and a session
|
|
167
|
+
close — and closed both times. The behaviour is still present in 3.6.11, and
|
|
168
|
+
has driven a machine into the OOM killer at 138 GB. There is no open upstream
|
|
169
|
+
issue to wait on, so the window is where iTerm2 stays.
|
|
170
|
+
|
|
171
|
+
To force either mode:
|
|
172
|
+
|
|
173
|
+
```
|
|
174
|
+
specterm1d --gui spec1d.fits # or --renderer gui
|
|
175
|
+
specterm1d --renderer halfblock spec1d.fits
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
The window opens at 1200x800 and is then yours to resize; resizing re-renders
|
|
179
|
+
at the new size. If no window can be opened — no `DISPLAY`, no usable
|
|
180
|
+
toolkit — specterm1d prints one line to stderr and falls back to half-block
|
|
181
|
+
rather than refusing to start.
|
|
182
|
+
|
|
183
|
+
## Keys
|
|
184
|
+
|
|
185
|
+
The complete reference is in [the key and command reference](https://specterm1d.readthedocs.io/en/latest/keys.html). The most-used:
|
|
186
|
+
|
|
187
|
+
| Key | Action |
|
|
188
|
+
|-----|--------|
|
|
189
|
+
| `<space>` | report the cursor position and nearest pixel |
|
|
190
|
+
| `a` | expand between two marks; the same point twice autoscales everything |
|
|
191
|
+
| `c` / `r` | clear all windowing / redraw keeping it |
|
|
192
|
+
| `z` `,` `.` | zoom by two about the cursor; pan left; pan right |
|
|
193
|
+
| `(` `)` `#` | previous / next spectrum; go to one by index or name |
|
|
194
|
+
| `e` | equivalent width by summation |
|
|
195
|
+
| `m` | mean, RMS and S/N over a region |
|
|
196
|
+
| `k` + `g`/`l`/`v` | fit a gaussian, lorentzian or voigt profile |
|
|
197
|
+
| `h` + `a`/`b`/`c`/`l`/`r`/`k` | equivalent width from a measured width |
|
|
198
|
+
| `s` | boxcar smooth |
|
|
199
|
+
| `U` | undo the last transform |
|
|
200
|
+
| `w` | the gtools window submode |
|
|
201
|
+
| `:` | colon commands (`:units nm`, `:sigma`, `:sky`, `:mask`, …) |
|
|
202
|
+
| `q` | next input spectrum, then exit |
|
|
203
|
+
|
|
204
|
+
Multi-point commands are explicit: press the command key to arm it, then mark
|
|
205
|
+
each point with `<space>`. The crosshair's **y** matters — `e`, `k` and `h`
|
|
206
|
+
take their continuum from the cursor's y at each marked point, which is what
|
|
207
|
+
IRAF's `sumflux.x` does with `eqy1`/`eqy2`.
|
|
208
|
+
|
|
209
|
+
## Not implemented yet
|
|
210
|
+
|
|
211
|
+
These keys are **registered** and report "not implemented in v1" when pressed.
|
|
212
|
+
They are never silently absent and never rebound to something else, so muscle
|
|
213
|
+
memory cannot misfire:
|
|
214
|
+
|
|
215
|
+
`d` deblend · `t` ICFIT · `f` arithmetic · `i` write to file ·
|
|
216
|
+
`j` set pixel to cursor · `x` etch-a-sketch · `p` linear wavelength scale ·
|
|
217
|
+
`u` user coordinate scale · `y` standard-star overplot
|
|
218
|
+
|
|
219
|
+
## Differences from splot
|
|
220
|
+
|
|
221
|
+
Three, stated plainly:
|
|
222
|
+
|
|
223
|
+
- **The cursor is always keyboard-driven and optionally mouse-driven.** Arrow
|
|
224
|
+
keys move a 2D crosshair and shift moves further. Inline Kitty, sixel and
|
|
225
|
+
iTerm2 graphics enable click/drag positioning and draw the full crosshair by
|
|
226
|
+
default. Terminals that answer DECRQM for DECSET 1016 - kitty, ghostty and
|
|
227
|
+
the sixel terminals among them - report the pointer in pixels rather than
|
|
228
|
+
cells, so the cursor tracks it instead of snapping to the character grid.
|
|
229
|
+
Terminals that do not, and tmux, keep cell coordinates. Halfblock leaves
|
|
230
|
+
mouse reporting off, having no pixels to place. Use
|
|
231
|
+
`--no-mouse` or `:mouse no` when you want the terminal's normal text selection
|
|
232
|
+
instead.
|
|
233
|
+
- **`%` cycles the extraction/calibration variant** (`OPT/COUNTS`,
|
|
234
|
+
`BOX/COUNTS`, `OPT/FLAM`, …) rather than an image band, which is the useful
|
|
235
|
+
analogue for pypeit products.
|
|
236
|
+
- **`U` undoes a transform.** `splot` has no equivalent: there, `s` is
|
|
237
|
+
destructive with no recovery short of reloading the file.
|
|
238
|
+
|
|
239
|
+
Beyond that, four display features `splot` had no data for: a one-sigma error
|
|
240
|
+
band (`:sigma`), masked-pixel highlighting (`:mask`), sky/telluric/model
|
|
241
|
+
overlays (`:sky`, `:telluric`, `:model`), and inverse-variance weighting of
|
|
242
|
+
profile fits.
|
|
243
|
+
|
|
244
|
+
## Measurement log
|
|
245
|
+
|
|
246
|
+
Measurements append to `splot.log` in IRAF's own column formats, taken from
|
|
247
|
+
`anshdr.x`, `eqwidth.x`, `gfit.x` and `avgsnr.x` — including the detail that
|
|
248
|
+
the `m` key suppresses the column header. Existing log-parsing scripts keep
|
|
249
|
+
working:
|
|
250
|
+
|
|
251
|
+
```
|
|
252
|
+
center cont flux eqw core gfwhm lfwhm
|
|
253
|
+
5183.6 1.234 -0.456 0.37
|
|
254
|
+
avg: 1.5 rms: 0.25 snr: 6.00
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
`:nolog` stops writing, `:log` resumes, and `:# some text` adds a comment.
|
|
258
|
+
|
|
259
|
+
## Batch use
|
|
260
|
+
|
|
261
|
+
`--cursor` replays a keystroke script, reproducing `splot`'s `cursor`
|
|
262
|
+
parameter. Combined with `--dump` it runs with no terminal at all:
|
|
263
|
+
|
|
264
|
+
```bash
|
|
265
|
+
cat > measure.txt <<'EOF'
|
|
266
|
+
5200 1.0 e
|
|
267
|
+
5200 1.0 <space>
|
|
268
|
+
5400 1.0 <space>
|
|
269
|
+
EOF
|
|
270
|
+
|
|
271
|
+
specterm1d spec.fits --cursor measure.txt --log out.log --dump frame.png
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## Licence
|
|
275
|
+
|
|
276
|
+
BSD-3-Clause.
|