firepype 0.0.1__py3-none-any.whl
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.
- firepype/__init__.py +27 -0
- firepype/calibration.py +520 -0
- firepype/cli.py +296 -0
- firepype/coadd.py +105 -0
- firepype/config.py +55 -0
- firepype/detection.py +517 -0
- firepype/extraction.py +198 -0
- firepype/io.py +248 -0
- firepype/pipeline.py +339 -0
- firepype/plotting.py +234 -0
- firepype/telluric.py +1401 -0
- firepype/utils.py +344 -0
- firepype-0.0.1.dist-info/METADATA +153 -0
- firepype-0.0.1.dist-info/RECORD +18 -0
- firepype-0.0.1.dist-info/WHEEL +5 -0
- firepype-0.0.1.dist-info/entry_points.txt +3 -0
- firepype-0.0.1.dist-info/licenses/LICENSE +21 -0
- firepype-0.0.1.dist-info/top_level.txt +1 -0
firepype/cli.py
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
# firepype/cli.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from .config import PipelineConfig, RunSpec
|
|
8
|
+
from .pipeline import run_ab_pairs
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def main():
|
|
12
|
+
"""
|
|
13
|
+
Purpose:
|
|
14
|
+
Command-line entry point for the FIRE AB-pair NIR reduction pipeline.
|
|
15
|
+
Parses CLI arguments, constructs PipelineConfig (applying optional
|
|
16
|
+
overrides), and executes AB-pair processing to produce co-added
|
|
17
|
+
spectrum and QA outputs
|
|
18
|
+
Inputs:
|
|
19
|
+
None (arguments are read from sys.argv via argparse)
|
|
20
|
+
Returns:
|
|
21
|
+
None
|
|
22
|
+
Runs the pipeline and writes outputs to disk. Exits with argparse
|
|
23
|
+
error messages on invalid usage
|
|
24
|
+
CLI options:
|
|
25
|
+
--raw-dir Directory with raw FITS files (required)
|
|
26
|
+
--out-dir Output directory for products and QA (required)
|
|
27
|
+
--arc Path to ARC frame FITS (required)
|
|
28
|
+
--ref-list Path to reference line list file (required)
|
|
29
|
+
--spec Frame spec string for AB pairing (required)
|
|
30
|
+
--no-qa Disable QA plots (optional flag)
|
|
31
|
+
--ap-radius Extraction aperture half-width in pixels (optional)
|
|
32
|
+
--bg-in Inner background offset in pixels (optional)
|
|
33
|
+
--bg-out Outer background offset in pixels (optional)
|
|
34
|
+
--footprint-half Half-size (columns) for footprint median combine (optional)
|
|
35
|
+
--wl-min Wavelength range minimum in microns (optional)
|
|
36
|
+
--wl-max Wavelength range maximum in microns (optional)
|
|
37
|
+
--grid-step Wavelength grid step for coadd in microns (optional)
|
|
38
|
+
--verbose Enable verbose diagnostic prints (optional)
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
p = argparse.ArgumentParser(
|
|
42
|
+
prog="firepype",
|
|
43
|
+
description="FIRE AB-pair NIR reduction pipeline",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
p.add_argument(
|
|
47
|
+
"--raw-dir",
|
|
48
|
+
required=True,
|
|
49
|
+
help="Directory containing raw FITS files (e.g., /data/raw)",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
p.add_argument(
|
|
53
|
+
"--out-dir",
|
|
54
|
+
required=True,
|
|
55
|
+
help="Output directory for products and QA (e.g., ./out)",
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
p.add_argument(
|
|
59
|
+
"--arc",
|
|
60
|
+
required=True,
|
|
61
|
+
help="Path to the ARC frame FITS (e.g., /data/raw/fire_XXXX.fits)",
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
p.add_argument(
|
|
65
|
+
"--ref-list",
|
|
66
|
+
required=True,
|
|
67
|
+
help="Path to the line list file (e.g., /data/ref/line_list.lst)",
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
p.add_argument(
|
|
71
|
+
"--spec",
|
|
72
|
+
required=True,
|
|
73
|
+
help='Frame spec string for AB pairing, e.g. "XXXX-XXXX" or "XX-XX,XX-XX"',
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
p.add_argument(
|
|
77
|
+
"--no-qa",
|
|
78
|
+
action="store_true",
|
|
79
|
+
help="Disable QA plots",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# Optional default overrides
|
|
83
|
+
p.add_argument(
|
|
84
|
+
"--ap-radius", type=int, default=None, help="Extraction aperture half-width (px)"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
p.add_argument(
|
|
88
|
+
"--bg-in", type=int, default=None, help="Inner background offset (px)"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
p.add_argument(
|
|
92
|
+
"--bg-out", type=int, default=None, help="Outer background offset (px)"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
p.add_argument(
|
|
96
|
+
"--footprint-half",
|
|
97
|
+
type=int,
|
|
98
|
+
default=None,
|
|
99
|
+
help="Half-size (in columns) for footprint median combine",
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
p.add_argument(
|
|
103
|
+
"--wl-min", type=float, default=None, help="Wavelength range min (micron)"
|
|
104
|
+
)
|
|
105
|
+
p.add_argument(
|
|
106
|
+
"--wl-max", type=float, default=None, help="Wavelength range max (micron)"
|
|
107
|
+
)
|
|
108
|
+
p.add_argument(
|
|
109
|
+
"--grid-step", type=float, default=None, help="Grid step for coadd (micron)"
|
|
110
|
+
)
|
|
111
|
+
p.add_argument(
|
|
112
|
+
"--verbose", action="store_true", help="Verbose detection/diagnostic prints"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
args = p.parse_args()
|
|
116
|
+
|
|
117
|
+
# Build config with defaults, then override if provided
|
|
118
|
+
cfg = PipelineConfig(
|
|
119
|
+
run=RunSpec(
|
|
120
|
+
raw_dir=Path(args.raw_dir),
|
|
121
|
+
out_dir=Path(args.out_dir),
|
|
122
|
+
arc_path=Path(args.arc),
|
|
123
|
+
ref_list_path=Path(args.ref_list),
|
|
124
|
+
user_spec=args.spec,
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
cfg.qa.save_figs = not args.no_qa
|
|
129
|
+
cfg.qa.verbose = bool(args.verbose)
|
|
130
|
+
cfg.qa.out_dir = Path(args.out_dir)
|
|
131
|
+
cfg.qa.qa_dir = Path(args.out_dir) / "qa"
|
|
132
|
+
|
|
133
|
+
if args.ap_radius is not None:
|
|
134
|
+
cfg.extraction.ap_radius = int(args.ap_radius)
|
|
135
|
+
|
|
136
|
+
if args.bg_in is not None:
|
|
137
|
+
cfg.extraction.bg_in = int(args.bg_in)
|
|
138
|
+
|
|
139
|
+
if args.bg_out is not None:
|
|
140
|
+
cfg.extraction.bg_out = int(args.bg_out)
|
|
141
|
+
|
|
142
|
+
if args.footprint_half is not None:
|
|
143
|
+
cfg.extraction.footprint_half = int(args.footprint_half)
|
|
144
|
+
|
|
145
|
+
if args.wl_min is not None or args.wl_max is not None:
|
|
146
|
+
wl_lo = args.wl_min if args.wl_min is not None else cfg.wavecal.wl_range[0]
|
|
147
|
+
wl_hi = args.wl_max if args.wl_max is not None else cfg.wavecal.wl_range[1]
|
|
148
|
+
cfg.wavecal.wl_range = (float(wl_lo), float(wl_hi))
|
|
149
|
+
|
|
150
|
+
if args.grid_step is not None:
|
|
151
|
+
cfg.wavecal.grid_step = float(args.grid_step)
|
|
152
|
+
|
|
153
|
+
run_ab_pairs(cfg)
|
|
154
|
+
|
|
155
|
+
def main_telluric() -> None:
|
|
156
|
+
"""
|
|
157
|
+
Telluric/response CLI.
|
|
158
|
+
|
|
159
|
+
Minimal workflow:
|
|
160
|
+
- Read a standard-star exposure (or coadd) FITS.
|
|
161
|
+
- Optionally read a reference spectrum (A0V) to derive response.
|
|
162
|
+
- Fit/derive telluric transmission and instrument response.
|
|
163
|
+
- Write out telluric.fits and response.fits (or as configured).
|
|
164
|
+
"""
|
|
165
|
+
import argparse
|
|
166
|
+
import sys
|
|
167
|
+
from pathlib import Path
|
|
168
|
+
|
|
169
|
+
parser = argparse.ArgumentParser(
|
|
170
|
+
prog="firepype-telluric",
|
|
171
|
+
description="Telluric + instrument response from a standard star",
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
parser.add_argument(
|
|
175
|
+
"--standard",
|
|
176
|
+
required=True,
|
|
177
|
+
help="Path to standard star FITS (2D or extracted 1D).",
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
parser.add_argument(
|
|
181
|
+
"--out-dir",
|
|
182
|
+
required=True,
|
|
183
|
+
help="Output directory for telluric/response products.",
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
parser.add_argument(
|
|
187
|
+
"--ref-spectrum",
|
|
188
|
+
default=None,
|
|
189
|
+
help="Optional path to reference spectrum for the standard "
|
|
190
|
+
"(e.g., A0V template). If omitted, use built-in or empirical.",
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
parser.add_argument(
|
|
194
|
+
"--stype",
|
|
195
|
+
default="A0V",
|
|
196
|
+
help='Standard star spectral type (default: "A0V").',
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
parser.add_argument(
|
|
200
|
+
"--airmass",
|
|
201
|
+
type=float,
|
|
202
|
+
default=None,
|
|
203
|
+
help="Airmass to use for telluric modeling (override header if given).",
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
parser.add_argument(
|
|
207
|
+
"--wl-min",
|
|
208
|
+
type=float,
|
|
209
|
+
default=None,
|
|
210
|
+
help="Minimum wavelength (micron) to include.",
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
parser.add_argument(
|
|
214
|
+
"--wl-max",
|
|
215
|
+
type=float,
|
|
216
|
+
default=None,
|
|
217
|
+
help="Maximum wavelength (micron) to include.",
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
parser.add_argument(
|
|
221
|
+
"--plot",
|
|
222
|
+
action="store_true",
|
|
223
|
+
help="Save diagnostic plots to out-dir.",
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
parser.add_argument(
|
|
227
|
+
"--overwrite",
|
|
228
|
+
action="store_true",
|
|
229
|
+
help="Overwrite existing outputs.",
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
args = parser.parse_args()
|
|
233
|
+
|
|
234
|
+
std_path = Path(args.standard).expanduser().resolve()
|
|
235
|
+
out_dir = Path(args.out_dir).expanduser().resolve()
|
|
236
|
+
ref_path = (
|
|
237
|
+
None
|
|
238
|
+
if args.ref_spectrum is None
|
|
239
|
+
else Path(args.ref_spectrum).expanduser().resolve()
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
# Basic validations
|
|
243
|
+
if not std_path.exists():
|
|
244
|
+
print(f"error: standard file not found: {std_path}", file=sys.stderr)
|
|
245
|
+
sys.exit(2)
|
|
246
|
+
|
|
247
|
+
if ref_path is not None and not ref_path.exists():
|
|
248
|
+
print(f"error: reference spectrum not found: {ref_path}", file=sys.stderr)
|
|
249
|
+
sys.exit(2)
|
|
250
|
+
|
|
251
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
252
|
+
|
|
253
|
+
# Default output filenames
|
|
254
|
+
telluric_out = out_dir / "telluric.fits"
|
|
255
|
+
response_out = out_dir / "response.fits"
|
|
256
|
+
qa_dir = out_dir / "qa"
|
|
257
|
+
|
|
258
|
+
if not args.overwrite:
|
|
259
|
+
for p in (telluric_out, response_out):
|
|
260
|
+
if p.exists():
|
|
261
|
+
print(f"error: output exists (use --overwrite): {p}", file=sys.stderr)
|
|
262
|
+
sys.exit(2)
|
|
263
|
+
|
|
264
|
+
# Import and run the actual logic
|
|
265
|
+
try:
|
|
266
|
+
from .telluric import run_telluric
|
|
267
|
+
except Exception as e:
|
|
268
|
+
print(f"error: could not import telluric runner: {e}", file=sys.stderr)
|
|
269
|
+
sys.exit(1)
|
|
270
|
+
|
|
271
|
+
try:
|
|
272
|
+
run_telluric(
|
|
273
|
+
standard_fits=std_path,
|
|
274
|
+
stype=args.stype,
|
|
275
|
+
out_telluric=telluric_out,
|
|
276
|
+
out_response=response_out,
|
|
277
|
+
ref_spectrum=ref_path,
|
|
278
|
+
airmass=args.airmass,
|
|
279
|
+
wl_min=args.wl_min,
|
|
280
|
+
wl_max=args.wl_max,
|
|
281
|
+
save_plots=args.plot,
|
|
282
|
+
qa_dir=qa_dir,
|
|
283
|
+
overwrite=args.overwrite,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
except Exception as e:
|
|
287
|
+
print(f"error: telluric processing failed: {e}", file=sys.stderr)
|
|
288
|
+
sys.exit(1)
|
|
289
|
+
|
|
290
|
+
print(f"wrote: {telluric_out}")
|
|
291
|
+
print(f"wrote: {response_out}")
|
|
292
|
+
if args.plot:
|
|
293
|
+
print(f"qa: {qa_dir}")
|
|
294
|
+
|
|
295
|
+
if __name__ == "__main__":
|
|
296
|
+
main()
|
firepype/coadd.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# firepype/coadd.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class CoaddAccumulator:
|
|
8
|
+
"""
|
|
9
|
+
Purpose:
|
|
10
|
+
Maintain inverse-variance coaddition accumulator on a fixed wavelength
|
|
11
|
+
grid. Add multiple spectra (already interpolated to this grid) along
|
|
12
|
+
with their per-bin uncertainties and an optional validity mask; to
|
|
13
|
+
accumulate weighted sums and producecco-added spectrum and errors
|
|
14
|
+
Inputs:
|
|
15
|
+
grid_wl (at init): 1D array of target wavelength grid in microns (or
|
|
16
|
+
arbitrary units) onto which all spectra are interpolated
|
|
17
|
+
Returns:
|
|
18
|
+
CoaddAccumulator instance
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, grid_wl: np.ndarray):
|
|
22
|
+
"""
|
|
23
|
+
Purpose:
|
|
24
|
+
Initialise coaddition accumulator for a specified wavelength grid
|
|
25
|
+
Inputs:
|
|
26
|
+
grid_wl: 1D array of wavelengths defining the coadd grid
|
|
27
|
+
Returns:
|
|
28
|
+
None
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
self.grid = np.asarray(grid_wl, float)
|
|
32
|
+
self.flux_sum = np.zeros_like(self.grid, dtype=float)
|
|
33
|
+
self.weight_sum = np.zeros_like(self.grid, dtype=float)
|
|
34
|
+
|
|
35
|
+
def add_spectrum(
|
|
36
|
+
self,
|
|
37
|
+
flux: np.ndarray,
|
|
38
|
+
err: np.ndarray,
|
|
39
|
+
mask: np.ndarray | None = None,
|
|
40
|
+
) -> None:
|
|
41
|
+
"""
|
|
42
|
+
Purpose:
|
|
43
|
+
Add single spectrum to accumulator. Spectrum should already be
|
|
44
|
+
interpolated onto the accumulator's grid (self.grid). Accumulation is
|
|
45
|
+
performed using inverse-variance weights
|
|
46
|
+
Inputs:
|
|
47
|
+
flux: 1D array of flux values on self.grid (same shape as grid)
|
|
48
|
+
err: 1D array of 1-sigma uncertainties on self.grid (same shape)
|
|
49
|
+
mask: Optional boolean mask (same shape) where True indicates a bin
|
|
50
|
+
should be used. If provided, it is combined with finite/positive
|
|
51
|
+
error masking
|
|
52
|
+
Returns:
|
|
53
|
+
None
|
|
54
|
+
Raises:
|
|
55
|
+
ValueError: If flux/err (or mask, when provided) shapes do not match
|
|
56
|
+
the grid shape
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
f = np.asarray(flux, float)
|
|
60
|
+
e = np.asarray(err, float)
|
|
61
|
+
|
|
62
|
+
if f.shape != self.grid.shape or e.shape != self.grid.shape:
|
|
63
|
+
raise ValueError("flux/err must match grid shape")
|
|
64
|
+
|
|
65
|
+
m = np.isfinite(f) & np.isfinite(e) & (e > 0)
|
|
66
|
+
|
|
67
|
+
if mask is not None:
|
|
68
|
+
mask = np.asarray(mask, bool)
|
|
69
|
+
|
|
70
|
+
if mask.shape != self.grid.shape:
|
|
71
|
+
raise ValueError("mask must match grid shape")
|
|
72
|
+
|
|
73
|
+
m &= mask
|
|
74
|
+
|
|
75
|
+
if not np.any(m):
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
w = 1.0 / np.maximum(e[m] ** 2, 1e-20)
|
|
79
|
+
self.flux_sum[m] += w * f[m]
|
|
80
|
+
self.weight_sum[m] += w
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def finalize(self) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
84
|
+
"""
|
|
85
|
+
Purpose:
|
|
86
|
+
Compute final coadded flux and error arrays from accumulated
|
|
87
|
+
weighted sums
|
|
88
|
+
Inputs:
|
|
89
|
+
None
|
|
90
|
+
Returns:
|
|
91
|
+
tuple:
|
|
92
|
+
- flux (np.ndarray): Weighted-mean flux on self.grid; NaN where no
|
|
93
|
+
samples contributed (weight == 0)
|
|
94
|
+
- err (np.ndarray): 1-sigma uncertainty as sqrt(1/weight); NaN where
|
|
95
|
+
weight == 0
|
|
96
|
+
- used (np.ndarray): Boolean mask where weight > 0 (bins used)
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
used = self.weight_sum > 0
|
|
100
|
+
flux = np.full_like(self.grid, np.nan, dtype=float)
|
|
101
|
+
err = np.full_like(self.grid, np.nan, dtype=float)
|
|
102
|
+
flux[used] = self.flux_sum[used] / self.weight_sum[used]
|
|
103
|
+
err[used] = np.sqrt(1.0 / self.weight_sum[used])
|
|
104
|
+
|
|
105
|
+
return flux, err, used
|
firepype/config.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# firepype/config.py
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Sequence, Tuple, Optional, List
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class QASettings:
|
|
8
|
+
save_figs: bool = True
|
|
9
|
+
verbose: bool = True
|
|
10
|
+
out_dir: Path = Path("./output")
|
|
11
|
+
qa_dir: Path = field(default_factory=lambda: Path("./output/qa"))
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class WavecalSettings:
|
|
15
|
+
wl_range: Tuple[float, float] = (0.83, 2.45)
|
|
16
|
+
grid_step: float = 0.0008
|
|
17
|
+
band_anchors_global: Sequence[Tuple[int, float]] = (
|
|
18
|
+
(100, 0.83),
|
|
19
|
+
(1000, 1.15),
|
|
20
|
+
(1600, 1.79),
|
|
21
|
+
(1950, 2.43),
|
|
22
|
+
)
|
|
23
|
+
max_sep: float = 0.012
|
|
24
|
+
deg: int = 3
|
|
25
|
+
force_global_orient: str = "fwd"
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class ExtractionSettings:
|
|
29
|
+
ap_radius: int = 5
|
|
30
|
+
bg_in: int = 8
|
|
31
|
+
bg_out: int = 18
|
|
32
|
+
footprint_half: int = 1
|
|
33
|
+
row_fraction: Tuple[float, float] = (0.35, 0.85)
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class SlitDetectionSettings:
|
|
37
|
+
slit_x_hint: Tuple[int, int] = (900, 1300)
|
|
38
|
+
slit_hint_expand: int = 150
|
|
39
|
+
row_fraction: Tuple[float, float] = (0.35, 0.85)
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class RunSpec:
|
|
43
|
+
raw_dir: Path
|
|
44
|
+
out_dir: Path
|
|
45
|
+
arc_path: Path
|
|
46
|
+
ref_list_path: Path
|
|
47
|
+
user_spec: str # e.g., "XXXX-XXXX"
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class PipelineConfig:
|
|
51
|
+
run: RunSpec
|
|
52
|
+
qa: QASettings = field(default_factory=QASettings)
|
|
53
|
+
wavecal: WavecalSettings = field(default_factory=WavecalSettings)
|
|
54
|
+
extraction: ExtractionSettings = field(default_factory=ExtractionSettings)
|
|
55
|
+
slit: SlitDetectionSettings = field(default_factory=SlitDetectionSettings)
|