pyredshift 1.9__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.
pyredshift-1.9/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Karl Glazebrook
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,167 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyredshift
3
+ Version: 1.9
4
+ Summary: Interactive redshifting of 1D astronomical spectra (Python successor of pdlredshift / redshift.f)
5
+ Home-page: https://github.com/karlglazebrook/pyredshift
6
+ Author: Karl Glazebrook
7
+ License: MIT
8
+ Keywords: astronomy spectroscopy redshift spectra interactive
9
+ Classifier: Development Status :: 5 - Production/Stable
10
+ Classifier: Intended Audience :: Science/Research
11
+ Classifier: Topic :: Scientific/Engineering :: Astronomy
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Environment :: MacOS X
15
+ Classifier: Environment :: X11 Applications
16
+ Requires-Python: >=3.9
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Requires-Dist: numpy
20
+ Requires-Dist: matplotlib
21
+ Requires-Dist: astropy
22
+ Requires-Dist: scipy
23
+ Dynamic: author
24
+ Dynamic: classifier
25
+ Dynamic: description
26
+ Dynamic: description-content-type
27
+ Dynamic: home-page
28
+ Dynamic: keywords
29
+ Dynamic: license
30
+ Dynamic: license-file
31
+ Dynamic: requires-dist
32
+ Dynamic: requires-python
33
+ Dynamic: summary
34
+
35
+ # pyredshift
36
+
37
+ Interactive redshifting of 1D astronomical spectra, by eye.
38
+
39
+ `pyredshift` plots a spectrum histogram-style with a full-window crosshair
40
+ cursor and overlays rest-frame line identifications that follow a trial
41
+ redshift. Mark a feature you recognise, and the line labels jump to that
42
+ redshift; refine, zoom, bin, smooth, fit a continuum and measure equivalent
43
+ widths — all from single keypresses. It is the Python descendant of the
44
+ author's venerable `redshift.f` (Fortran/Figaro, 1990s) via `pdlredshift`
45
+ (Perl/PDL/PGPLOT), and deliberately keeps that fast keyboard-driven feel.
46
+
47
+ ![pyredshift showing an SDSS z=2.5 quasar in --retro mode](https://raw.githubusercontent.com/karlglazebrook/pyredshift/main/screenshot.png)
48
+
49
+ *An SDSS quasar from [Example-Spectra](Example-Spectra/) at z = 2.48 and
50
+ counting, in `--retro` mode: Lyα, CIV and CIII] lined up; the crosshair
51
+ cursor mid-hunt.*
52
+
53
+ ## Install
54
+
55
+ ```
56
+ pip install git+https://github.com/karlglazebrook/pyredshift
57
+ ```
58
+
59
+ or from a checkout:
60
+
61
+ ```
62
+ pip install .
63
+ ```
64
+
65
+ This installs the `pyredshift` command into the active python's `bin/`
66
+ (e.g. `anaconda3/bin`) and the `pyredshift` package into `site-packages`.
67
+ Requires python ≥ 3.9 with numpy, matplotlib, astropy and scipy.
68
+
69
+ ## Usage
70
+
71
+ ```
72
+ pyredshift spectrum.fits # format auto-detected
73
+ pyredshift spectrum.fits -f sdss # force a format
74
+ pyredshift spectrum.fits -z 2.19 # start from a known redshift
75
+ pyredshift spectrum.fits --retro # retro PGPLOT-style black background
76
+ pyredshift spectrum.fits --microns # keep micron wavelengths (no A conversion)
77
+ ```
78
+
79
+ The file format is auto-detected by inspecting the FITS structure and
80
+ column names/units:
81
+
82
+ | Format | Description |
83
+ |------------|-------------------------------------------------------------------|
84
+ | `fits` | 1D (or 2D — row 1) FITS image with WCS; `CD1_1` overrides `CDELT1`, IRAF/SDSS log-λ flags handled |
85
+ | `sdss` | SDSS binary table (`LOGLAM`/`FLUX`, either case) |
86
+ | `table` | generic binary table with wave+flux columns; `TUNIT` (µm, nm, Å) converted automatically (aliases: `jwst`, `gabe`, `dja`) |
87
+ | `xs` | XSHOOTER 1D table spectrum (nm) |
88
+ | `xs2` | XSHOOTER as a FITS image in extension 1 |
89
+ | `outthere` | OutThere multi-extension grism spectra, stitched and flat-calibrated |
90
+ | `csv` | comma-separated, header rows skipped |
91
+ | `ascii` | 2-column text |
92
+
93
+ Wavelengths that look like microns are converted to Ångstroms; NaNs are
94
+ treated as bad pixels and plot as gaps, with orange markers along the
95
+ bottom of the frame.
96
+
97
+ Real spectra to practice on are included in
98
+ [Example-Spectra](Example-Spectra/) — the true redshifts are hidden in
99
+ their FITS headers.
100
+
101
+ ### From a Jupyter notebook
102
+
103
+ The interactive module also works straight from a notebook, given numpy
104
+ arrays:
105
+
106
+ ```python
107
+ from pyredshift import redshift as redshift_module
108
+ z = redshift_module.redshift(wave, flux)
109
+ ```
110
+
111
+ The interactive window opens *outside* the notebook (inline backends
112
+ cannot deliver events to it) and the cell blocks until you quit with
113
+ `q`; the final annotated view is then embedded in the cell output and
114
+ the redshift returned. Any non-interactive backend (e.g. `%matplotlib
115
+ inline`) is switched to a GUI backend for the session and restored
116
+ afterwards — including if you interrupt the kernel mid-session.
117
+
118
+ **Local kernels only**: the window opens on the machine where the
119
+ kernel runs, so remote kernels (JupyterHub, cloud, ssh without X
120
+ forwarding) cannot work — pyredshift detects this and says so rather
121
+ than hanging or crashing the kernel.
122
+
123
+ ## Interaction
124
+
125
+ Press `?` (or click the `?` button) for the full command reference — it
126
+ opens as a themed page in your browser, with the complete line list and a
127
+ short guide in separate tabs. The essentials:
128
+
129
+ - **Identify a line**: put the crosshair on a feature and press `ESC`
130
+ followed by a shortcut key (`a` = Hα, `b` = Hβ, `l` = Lyα, `o` = [OII],
131
+ `c` = CIV ...) — or right-click the feature and pick the line from the
132
+ popup menu, or `g` to type any rest wavelength. Press `ESC ESC` on a
133
+ sharp line to refine the redshift; `=` enters a redshift directly.
134
+ - **Navigate**: drag the mouse for a rubber-band zoom (a purely horizontal
135
+ drag zooms X only), `z`/`u` and `i`/`o` zoom about the cursor, `[` `]`
136
+ pan, `w` whole range, `a` autoscale, `h` home. The matplotlib toolbar
137
+ works too and stays in sync.
138
+ - **Analyse**: `b` bin, `s` smooth, `_` iterative continuum fit,
139
+ `m` equivalent width and line flux, `B` zap artefacts, `p` print to PDF.
140
+ - **Quit**: `q` — the final redshift is reported on the terminal.
141
+
142
+ Line identifications come from `pyredshift.lines` (CSV: wavelength, name,
143
+ label, matplotlib colour, comment — all vacuum wavelengths; a colour may
144
+ be a `light/dark` pair for the two backgrounds), installed
145
+ alongside the module and easy to customise. The window size and aspect
146
+ ratio are remembered between runs in `~/.pyredshift.json`. A template
147
+ spectrum can be overlaid with `t` (searched for in `$DATADIR`,
148
+ `$NEWMODELS/Spectra`, then `~/Templates/Spectra`).
149
+
150
+ ## Layout
151
+
152
+ ```
153
+ pyredshift the command-line script: format detection and reading
154
+ src/pyredshift/ the interactive module (redshift.py) + data files
155
+ ```
156
+
157
+ The script prefers a `src/pyredshift/` found next to it or under the
158
+ current directory, so a development checkout always runs its own code.
159
+
160
+ ## Credits
161
+
162
+ Karl Glazebrook, Swinburne University of Technology.
163
+
164
+ Lineage: `redshift.f` (FIGARO/Fortran, ~1992) → `pdlredshift` (Perl/PDL) →
165
+ `pyredshift`. Ported with the assistance of Claude (Anthropic).
166
+
167
+ MIT license — see [LICENSE](LICENSE).
@@ -0,0 +1,133 @@
1
+ # pyredshift
2
+
3
+ Interactive redshifting of 1D astronomical spectra, by eye.
4
+
5
+ `pyredshift` plots a spectrum histogram-style with a full-window crosshair
6
+ cursor and overlays rest-frame line identifications that follow a trial
7
+ redshift. Mark a feature you recognise, and the line labels jump to that
8
+ redshift; refine, zoom, bin, smooth, fit a continuum and measure equivalent
9
+ widths — all from single keypresses. It is the Python descendant of the
10
+ author's venerable `redshift.f` (Fortran/Figaro, 1990s) via `pdlredshift`
11
+ (Perl/PDL/PGPLOT), and deliberately keeps that fast keyboard-driven feel.
12
+
13
+ ![pyredshift showing an SDSS z=2.5 quasar in --retro mode](https://raw.githubusercontent.com/karlglazebrook/pyredshift/main/screenshot.png)
14
+
15
+ *An SDSS quasar from [Example-Spectra](Example-Spectra/) at z = 2.48 and
16
+ counting, in `--retro` mode: Lyα, CIV and CIII] lined up; the crosshair
17
+ cursor mid-hunt.*
18
+
19
+ ## Install
20
+
21
+ ```
22
+ pip install git+https://github.com/karlglazebrook/pyredshift
23
+ ```
24
+
25
+ or from a checkout:
26
+
27
+ ```
28
+ pip install .
29
+ ```
30
+
31
+ This installs the `pyredshift` command into the active python's `bin/`
32
+ (e.g. `anaconda3/bin`) and the `pyredshift` package into `site-packages`.
33
+ Requires python ≥ 3.9 with numpy, matplotlib, astropy and scipy.
34
+
35
+ ## Usage
36
+
37
+ ```
38
+ pyredshift spectrum.fits # format auto-detected
39
+ pyredshift spectrum.fits -f sdss # force a format
40
+ pyredshift spectrum.fits -z 2.19 # start from a known redshift
41
+ pyredshift spectrum.fits --retro # retro PGPLOT-style black background
42
+ pyredshift spectrum.fits --microns # keep micron wavelengths (no A conversion)
43
+ ```
44
+
45
+ The file format is auto-detected by inspecting the FITS structure and
46
+ column names/units:
47
+
48
+ | Format | Description |
49
+ |------------|-------------------------------------------------------------------|
50
+ | `fits` | 1D (or 2D — row 1) FITS image with WCS; `CD1_1` overrides `CDELT1`, IRAF/SDSS log-λ flags handled |
51
+ | `sdss` | SDSS binary table (`LOGLAM`/`FLUX`, either case) |
52
+ | `table` | generic binary table with wave+flux columns; `TUNIT` (µm, nm, Å) converted automatically (aliases: `jwst`, `gabe`, `dja`) |
53
+ | `xs` | XSHOOTER 1D table spectrum (nm) |
54
+ | `xs2` | XSHOOTER as a FITS image in extension 1 |
55
+ | `outthere` | OutThere multi-extension grism spectra, stitched and flat-calibrated |
56
+ | `csv` | comma-separated, header rows skipped |
57
+ | `ascii` | 2-column text |
58
+
59
+ Wavelengths that look like microns are converted to Ångstroms; NaNs are
60
+ treated as bad pixels and plot as gaps, with orange markers along the
61
+ bottom of the frame.
62
+
63
+ Real spectra to practice on are included in
64
+ [Example-Spectra](Example-Spectra/) — the true redshifts are hidden in
65
+ their FITS headers.
66
+
67
+ ### From a Jupyter notebook
68
+
69
+ The interactive module also works straight from a notebook, given numpy
70
+ arrays:
71
+
72
+ ```python
73
+ from pyredshift import redshift as redshift_module
74
+ z = redshift_module.redshift(wave, flux)
75
+ ```
76
+
77
+ The interactive window opens *outside* the notebook (inline backends
78
+ cannot deliver events to it) and the cell blocks until you quit with
79
+ `q`; the final annotated view is then embedded in the cell output and
80
+ the redshift returned. Any non-interactive backend (e.g. `%matplotlib
81
+ inline`) is switched to a GUI backend for the session and restored
82
+ afterwards — including if you interrupt the kernel mid-session.
83
+
84
+ **Local kernels only**: the window opens on the machine where the
85
+ kernel runs, so remote kernels (JupyterHub, cloud, ssh without X
86
+ forwarding) cannot work — pyredshift detects this and says so rather
87
+ than hanging or crashing the kernel.
88
+
89
+ ## Interaction
90
+
91
+ Press `?` (or click the `?` button) for the full command reference — it
92
+ opens as a themed page in your browser, with the complete line list and a
93
+ short guide in separate tabs. The essentials:
94
+
95
+ - **Identify a line**: put the crosshair on a feature and press `ESC`
96
+ followed by a shortcut key (`a` = Hα, `b` = Hβ, `l` = Lyα, `o` = [OII],
97
+ `c` = CIV ...) — or right-click the feature and pick the line from the
98
+ popup menu, or `g` to type any rest wavelength. Press `ESC ESC` on a
99
+ sharp line to refine the redshift; `=` enters a redshift directly.
100
+ - **Navigate**: drag the mouse for a rubber-band zoom (a purely horizontal
101
+ drag zooms X only), `z`/`u` and `i`/`o` zoom about the cursor, `[` `]`
102
+ pan, `w` whole range, `a` autoscale, `h` home. The matplotlib toolbar
103
+ works too and stays in sync.
104
+ - **Analyse**: `b` bin, `s` smooth, `_` iterative continuum fit,
105
+ `m` equivalent width and line flux, `B` zap artefacts, `p` print to PDF.
106
+ - **Quit**: `q` — the final redshift is reported on the terminal.
107
+
108
+ Line identifications come from `pyredshift.lines` (CSV: wavelength, name,
109
+ label, matplotlib colour, comment — all vacuum wavelengths; a colour may
110
+ be a `light/dark` pair for the two backgrounds), installed
111
+ alongside the module and easy to customise. The window size and aspect
112
+ ratio are remembered between runs in `~/.pyredshift.json`. A template
113
+ spectrum can be overlaid with `t` (searched for in `$DATADIR`,
114
+ `$NEWMODELS/Spectra`, then `~/Templates/Spectra`).
115
+
116
+ ## Layout
117
+
118
+ ```
119
+ pyredshift the command-line script: format detection and reading
120
+ src/pyredshift/ the interactive module (redshift.py) + data files
121
+ ```
122
+
123
+ The script prefers a `src/pyredshift/` found next to it or under the
124
+ current directory, so a development checkout always runs its own code.
125
+
126
+ ## Credits
127
+
128
+ Karl Glazebrook, Swinburne University of Technology.
129
+
130
+ Lineage: `redshift.f` (FIGARO/Fortran, ~1992) → `pdlredshift` (Perl/PDL) →
131
+ `pyredshift`. Ported with the assistance of Claude (Anthropic).
132
+
133
+ MIT license — see [LICENSE](LICENSE).
@@ -0,0 +1,6 @@
1
+ # Minimal build-system declaration; the real metadata is in setup.py
2
+ # (kept there because script-file installation with shebang rewriting
3
+ # is a setup.py feature).
4
+ [build-system]
5
+ requires = ["setuptools"]
6
+ build-backend = "setuptools.build_meta"
@@ -0,0 +1,297 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ pyredshift - read a 1D spectrum in any of the usual formats and
4
+ redshift it interactively. Python port of the Perl 'pdlredshift'.
5
+
6
+ Usage:
7
+ pyredshift spectrum.fits # format auto-detected
8
+ pyredshift spectrum.fits -f sdss # manual override
9
+ pyredshift spectrum.fits -z 2.19 # start at a known redshift
10
+
11
+ Formats are auto-detected where possible by inspecting the FITS HDUs
12
+ and column names/units, replacing the old mode zoo:
13
+ ascii 2 column ascii file
14
+ csv comma separated, header rows skipped (NIRSPEC ERO data)
15
+ fits 1D (or 2D - row 1 used) FITS image with WCS; handles
16
+ CD1_1 overriding CDELT1 and IRAF/SDSS log-lambda flags
17
+ sdss SDSS binary table (LOGLAM/FLUX, either case)
18
+ table generic binary table with wave+flux columns; TUNIT
19
+ (um, nm, Angstrom...) converted automatically.
20
+ (aliases: jwst, jwst2, gabe, dja)
21
+ xs XSHOOTER 1D table spectrum (WAVE in nm)
22
+ xs2 XSHOOTER as FITS image in extension 1 (Corentin's code)
23
+ outthere OutThere multi-extension grism spectra: F115W/F150W/F200W
24
+ extensions stitched in wavelength order, flux/flat
25
+ calibrated, overlaps trimmed at the filter transitions
26
+ """
27
+
28
+ import argparse
29
+ import os
30
+ import re
31
+ import sys
32
+
33
+ import numpy as np
34
+ from astropy.io import fits
35
+ from astropy import units as u
36
+
37
+ # Dev override: a src/pyredshift package next to this script, or under the
38
+ # current directory, is picked up in preference to the pip-installed one
39
+ for _p in (os.path.join(os.path.dirname(os.path.abspath(__file__)), "src"),
40
+ os.path.join(os.getcwd(), "src")):
41
+ if os.path.isdir(os.path.join(_p, "pyredshift")):
42
+ sys.path.insert(0, _p)
43
+ from pyredshift import redshift as redshift_module
44
+
45
+ WAVE_COLS = ("wave", "wavelength", "lam", "lambda", "awav", "wav")
46
+ FLUX_COLS = ("flux", "flam", "fnu", "spec", "counts")
47
+
48
+ # Old pdlredshift mode names that the generic table reader now covers
49
+ ALIASES = {"jwst": "table", "jwst2": "table", "gabe": "table", "dja": "table"}
50
+
51
+
52
+ def wave_factor(unit_str):
53
+ """Conversion factor from a TUNIT string to Angstroms, or None."""
54
+ if not unit_str:
55
+ return None
56
+ try:
57
+ return float((1 * u.Unit(unit_str)).to(u.AA).value)
58
+ except Exception:
59
+ return None
60
+
61
+
62
+ def read_ascii(fname):
63
+ print("Reading file %s as a 2 column ascii file" % fname)
64
+ wav, spec = np.loadtxt(fname, usecols=(0, 1), comments="#", unpack=True)
65
+ return wav, spec, True
66
+
67
+
68
+ def read_csv(fname):
69
+ print("Reading file %s as a CSV table spectrum" % fname)
70
+ # Exclude headers: rows beginning with # or letters
71
+ wav, spec = [], []
72
+ with open(fname) as fh:
73
+ for line in fh:
74
+ if not line.strip() or re.match(r"^#|^\s*[A-Za-z]", line):
75
+ continue
76
+ parts = line.split(",")
77
+ try:
78
+ wav.append(float(parts[0]))
79
+ spec.append(float(parts[1]))
80
+ except (IndexError, ValueError):
81
+ continue
82
+ return np.array(wav), np.array(spec), True
83
+
84
+
85
+ def first_table(hdul):
86
+ for hdu in hdul:
87
+ if getattr(hdu, "columns", None) is not None and hdu.data is not None:
88
+ return hdu
89
+ return None
90
+
91
+
92
+ def first_image(hdul):
93
+ for hdu in hdul:
94
+ if hdu.is_image and hdu.data is not None and hdu.data.size > 1:
95
+ return hdu
96
+ return None
97
+
98
+
99
+ def looks_outthere(hdul):
100
+ n = 0
101
+ for hdu in hdul:
102
+ if getattr(hdu, "columns", None) is None:
103
+ continue
104
+ extname = str(hdu.header.get("EXTNAME", ""))
105
+ if re.match(r"^F\d+W", extname) and "flat" in [c.lower() for c in hdu.columns.names]:
106
+ n += 1
107
+ return n >= 2
108
+
109
+
110
+ def read_table(hdu, hint=""):
111
+ """Generic FITS table spectrum: find the wave/flux columns whatever
112
+ their case, use TUNIT to fix the wavelength units."""
113
+ low = {c.lower(): c for c in hdu.columns.names}
114
+
115
+ if "loglam" in low: # The particular SDSS format FITS table
116
+ print("as a SDSS table spectrum")
117
+ wav = 10.0 ** np.asarray(hdu.data[low["loglam"]], float).ravel()
118
+ spec = np.asarray(hdu.data[low["flux"]], float).ravel()
119
+ return wav, spec
120
+
121
+ wcol = next((low[n] for n in WAVE_COLS if n in low), None)
122
+ fcol = next((low[n] for n in FLUX_COLS if n in low), None)
123
+ if wcol is None or fcol is None:
124
+ sys.exit("Cannot identify wavelength/flux columns among %s"
125
+ % list(hdu.columns.names))
126
+ print("as a FITS table spectrum (columns %s, %s)" % (wcol, fcol), end="")
127
+ wav = np.asarray(hdu.data[wcol], float).ravel()
128
+ spec = np.asarray(hdu.data[fcol], float).ravel()
129
+
130
+ fac = wave_factor(hdu.columns[wcol].unit)
131
+ if fac is not None and fac != 1.0:
132
+ print(" [%s -> Angstroms]" % hdu.columns[wcol].unit, end="")
133
+ wav = wav * fac
134
+ elif fac is None and "SHOOT" in hint.upper():
135
+ print(" [XSHOOTER nm -> Angstroms]", end="")
136
+ wav = wav * 10.0
137
+ print()
138
+ return wav, spec
139
+
140
+
141
+ def read_image(hdu):
142
+ print("as a FITS image spectrum", end="")
143
+ spec = np.asarray(hdu.data, float)
144
+ h = hdu.header
145
+ if spec.ndim == 2: # Handle 2D case
146
+ print(" found 2D file, assuming row 1", end="")
147
+ spec = spec[0].copy()
148
+ x = np.arange(spec.size, dtype=float)
149
+ wav = x.copy() # In case there is no WCS
150
+ has_wcs = "CRVAL1" in h
151
+ if has_wcs:
152
+ print(" found WCS", end="")
153
+ # See https://www.aanda.org/articles/aa/pdf/2002/45/aah3859.pdf
154
+ delt = h.get("CDELT1", 1.0)
155
+ if "CD1_1" in h:
156
+ delt = h["CD1_1"] # CD1_1 overrides!
157
+ wav = (x - h.get("CRPIX1", 1.0) + 1) * delt + h["CRVAL1"]
158
+ # First is IRAF, second is SDSS convention
159
+ if h.get("DC-FLAG") or h.get("LOGSCALE") == 1:
160
+ print(" log scale", end="")
161
+ wav = 10.0 ** wav
162
+ else:
163
+ print(" linear scale", end="")
164
+ print()
165
+ return wav, spec, has_wcs
166
+
167
+
168
+ def read_outthere(hdul):
169
+ """Read the F115W/F150W/F200W grism extensions and append them."""
170
+ print("as OutThere multi-extension grism spectra")
171
+ tabs = [hdu for hdu in hdul
172
+ if getattr(hdu, "columns", None) is not None
173
+ and str(hdu.header.get("EXTNAME", "")).startswith("F")]
174
+ tabs.sort(key=lambda hdu: hdu.header["EXTNAME"]) # EXTNAME order == wavelength order
175
+ wav = np.array([])
176
+ spec = np.array([])
177
+ for tab in tabs:
178
+ ext = tab.header["EXTNAME"]
179
+ wave = np.asarray(tab.data["wave"], float).ravel()
180
+ flux = np.asarray(tab.data["flux"], float).ravel()
181
+ flat = np.asarray(tab.data["flat"], float).ravel()
182
+ # Select wavelengths based on filter to avoid overlaps; these
183
+ # transitions are based on where one filter curve exceeds the other
184
+ keep = wave > 0 # Default select all
185
+ if ext == "F115W":
186
+ keep = wave <= 13040
187
+ if ext == "F150W":
188
+ keep = (wave > 13040) & (wave <= 17070)
189
+ if ext == "F200W":
190
+ keep = wave > 17070
191
+ with np.errstate(divide="ignore", invalid="ignore"):
192
+ wav = np.append(wav, wave[keep])
193
+ spec = np.append(spec, flux[keep] / flat[keep])
194
+ return wav, spec
195
+
196
+
197
+ def read_spectrum(fname, fmt=None):
198
+ if not os.path.exists(fname):
199
+ sys.exit("File %s not found" % fname)
200
+ fmt = (fmt or "").lower()
201
+ fmt = ALIASES.get(fmt, fmt)
202
+
203
+ if fmt == "ascii":
204
+ return read_ascii(fname)
205
+ if fmt == "csv":
206
+ return read_csv(fname)
207
+
208
+ try:
209
+ hdul = fits.open(fname, memmap=False)
210
+ except Exception:
211
+ hdul = None
212
+
213
+ if hdul is None: # Not FITS: csv or plain ascii
214
+ if fmt:
215
+ sys.exit("%s is not a FITS file, but format '%s' was requested"
216
+ % (fname, fmt))
217
+ with open(fname) as fh:
218
+ for line in fh:
219
+ if line.strip() and not line.startswith("#"):
220
+ if "," in line or fname.lower().endswith(".csv"):
221
+ return read_csv(fname)
222
+ return read_ascii(fname)
223
+ sys.exit("%s appears to be empty" % fname)
224
+
225
+ with hdul:
226
+ print("Reading file %s " % fname, end="")
227
+ instrume = str(hdul[0].header.get("INSTRUME", ""))
228
+ has_wcs = True # tables always carry real wavelengths
229
+
230
+ if fmt in ("table", "sdss"):
231
+ wav, spec = read_table(first_table(hdul), instrume)
232
+ elif fmt == "xs":
233
+ print("as an XSHOOTER 1D table spectrum")
234
+ tab = hdul[1]
235
+ wav = np.asarray(tab.data["WAVE"], float).ravel() * 10
236
+ spec = np.asarray(tab.data["FLUX"], float).ravel()
237
+ elif fmt == "xs2":
238
+ wav, spec, has_wcs = read_image(hdul[1])
239
+ wav = wav * 10000.0
240
+ elif fmt == "outthere":
241
+ wav, spec = read_outthere(hdul)
242
+ elif fmt == "fits":
243
+ wav, spec, has_wcs = read_image(first_image(hdul))
244
+ elif fmt == "": # Auto-detect from the HDU structure
245
+ if looks_outthere(hdul):
246
+ wav, spec = read_outthere(hdul)
247
+ else:
248
+ tab = first_table(hdul)
249
+ if tab is not None:
250
+ wav, spec = read_table(tab, instrume)
251
+ else:
252
+ img = first_image(hdul)
253
+ if img is None:
254
+ sys.exit("No spectrum found in FITS file")
255
+ wav, spec, has_wcs = read_image(img)
256
+ else:
257
+ sys.exit("Unknown format '%s' (see pyredshift -h)" % fmt)
258
+
259
+ spec = np.asarray(spec, float)
260
+ spec[~np.isfinite(spec)] = np.nan # NaNs plot as gaps downstream
261
+ return np.asarray(wav, float), spec, has_wcs
262
+
263
+
264
+ def main():
265
+ ap = argparse.ArgumentParser(
266
+ prog="pyredshift",
267
+ description="Interactive redshifting of 1D spectra (port of pdlredshift).",
268
+ epilog="Formats: ascii csv fits sdss table (=jwst,jwst2,gabe,dja) "
269
+ "xs xs2 outthere. Default is auto-detection.")
270
+ ap.add_argument("file", help="spectrum file (FITS, CSV or 2-column ascii)")
271
+ ap.add_argument("-f", "--format", default=None, metavar="FORMAT",
272
+ help="force the input format instead of auto-detecting")
273
+ ap.add_argument("-z", "--redshift", type=float, default=None,
274
+ help="initial redshift guess")
275
+ ap.add_argument("--retro", action="store_true",
276
+ help="retro PGPLOT-style black background")
277
+ ap.add_argument("--microns", action="store_true",
278
+ help="keep micron wavelengths (micron-mode display) "
279
+ "instead of converting to Angstroms")
280
+ ap.add_argument("--dark", dest="retro", action="store_true",
281
+ help=argparse.SUPPRESS) # deprecated alias for --retro
282
+ args = ap.parse_args()
283
+
284
+ wav, spec, has_wcs = read_spectrum(args.file, args.format)
285
+
286
+ if np.nanmax(wav) < 100 and has_wcs and not args.microns:
287
+ print(" Detected microns -> Angstroms (--microns to keep)")
288
+ wav = wav * 10000.0
289
+
290
+ z = redshift_module.redshift(wav, spec, args.redshift,
291
+ os.path.basename(args.file), dark=args.retro)
292
+ if z is not None:
293
+ print("Final redshift = %.6g" % z)
294
+
295
+
296
+ if __name__ == "__main__":
297
+ main()
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,40 @@
1
+ from setuptools import setup
2
+
3
+ # pip install . (from this directory)
4
+ #
5
+ # Installs:
6
+ # the pyredshift package (redshift.py + data files pyredshift.lines,
7
+ # pyredshift-help.html) -> site-packages/pyredshift/
8
+ # the pyredshift script -> the bin/ of whichever python runs the install
9
+ # (e.g. anaconda3/bin), with the shebang rewritten to that python.
10
+ #
11
+ # The package source lives in src/ (a file and a directory cannot share
12
+ # the name 'pyredshift' at the top level).
13
+
14
+ setup(
15
+ name="pyredshift",
16
+ version="1.9",
17
+ description="Interactive redshifting of 1D astronomical spectra "
18
+ "(Python successor of pdlredshift / redshift.f)",
19
+ long_description=open("README.md").read(),
20
+ long_description_content_type="text/markdown",
21
+ author="Karl Glazebrook",
22
+ url="https://github.com/karlglazebrook/pyredshift",
23
+ license="MIT",
24
+ classifiers=[
25
+ "Development Status :: 5 - Production/Stable",
26
+ "Intended Audience :: Science/Research",
27
+ "Topic :: Scientific/Engineering :: Astronomy",
28
+ "License :: OSI Approved :: MIT License",
29
+ "Programming Language :: Python :: 3",
30
+ "Environment :: MacOS X",
31
+ "Environment :: X11 Applications",
32
+ ],
33
+ keywords="astronomy spectroscopy redshift spectra interactive",
34
+ packages=["pyredshift"],
35
+ package_dir={"": "src"},
36
+ package_data={"pyredshift": ["pyredshift.lines", "pyredshift-help.html"]},
37
+ scripts=["pyredshift"],
38
+ install_requires=["numpy", "matplotlib", "astropy", "scipy"],
39
+ python_requires=">=3.9",
40
+ )
@@ -0,0 +1,5 @@
1
+ """pyredshift - interactive redshifting of 1D spectra.
2
+
3
+ The interactive tool lives in pyredshift.redshift; the pyredshift script
4
+ handles reading the many spectrum formats.
5
+ """