pyreduce-astro 0.7a4__cp314-cp314-win_amd64.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.
- pyreduce/__init__.py +67 -0
- pyreduce/__main__.py +322 -0
- pyreduce/cli.py +342 -0
- pyreduce/clib/Release/_slitfunc_2d.cp311-win_amd64.exp +0 -0
- pyreduce/clib/Release/_slitfunc_2d.cp311-win_amd64.lib +0 -0
- pyreduce/clib/Release/_slitfunc_2d.cp312-win_amd64.exp +0 -0
- pyreduce/clib/Release/_slitfunc_2d.cp312-win_amd64.lib +0 -0
- pyreduce/clib/Release/_slitfunc_2d.cp313-win_amd64.exp +0 -0
- pyreduce/clib/Release/_slitfunc_2d.cp313-win_amd64.lib +0 -0
- pyreduce/clib/Release/_slitfunc_2d.cp314-win_amd64.exp +0 -0
- pyreduce/clib/Release/_slitfunc_2d.cp314-win_amd64.lib +0 -0
- pyreduce/clib/Release/_slitfunc_2d.obj +0 -0
- pyreduce/clib/Release/_slitfunc_bd.cp311-win_amd64.exp +0 -0
- pyreduce/clib/Release/_slitfunc_bd.cp311-win_amd64.lib +0 -0
- pyreduce/clib/Release/_slitfunc_bd.cp312-win_amd64.exp +0 -0
- pyreduce/clib/Release/_slitfunc_bd.cp312-win_amd64.lib +0 -0
- pyreduce/clib/Release/_slitfunc_bd.cp313-win_amd64.exp +0 -0
- pyreduce/clib/Release/_slitfunc_bd.cp313-win_amd64.lib +0 -0
- pyreduce/clib/Release/_slitfunc_bd.cp314-win_amd64.exp +0 -0
- pyreduce/clib/Release/_slitfunc_bd.cp314-win_amd64.lib +0 -0
- pyreduce/clib/Release/_slitfunc_bd.obj +0 -0
- pyreduce/clib/__init__.py +0 -0
- pyreduce/clib/_slitfunc_2d.cp311-win_amd64.pyd +0 -0
- pyreduce/clib/_slitfunc_2d.cp312-win_amd64.pyd +0 -0
- pyreduce/clib/_slitfunc_2d.cp313-win_amd64.pyd +0 -0
- pyreduce/clib/_slitfunc_2d.cp314-win_amd64.pyd +0 -0
- pyreduce/clib/_slitfunc_bd.cp311-win_amd64.pyd +0 -0
- pyreduce/clib/_slitfunc_bd.cp312-win_amd64.pyd +0 -0
- pyreduce/clib/_slitfunc_bd.cp313-win_amd64.pyd +0 -0
- pyreduce/clib/_slitfunc_bd.cp314-win_amd64.pyd +0 -0
- pyreduce/clib/build_extract.py +75 -0
- pyreduce/clib/slit_func_2d_xi_zeta_bd.c +1313 -0
- pyreduce/clib/slit_func_2d_xi_zeta_bd.h +55 -0
- pyreduce/clib/slit_func_bd.c +362 -0
- pyreduce/clib/slit_func_bd.h +17 -0
- pyreduce/clipnflip.py +147 -0
- pyreduce/combine_frames.py +861 -0
- pyreduce/configuration.py +191 -0
- pyreduce/continuum_normalization.py +329 -0
- pyreduce/cwrappers.py +404 -0
- pyreduce/datasets.py +238 -0
- pyreduce/echelle.py +413 -0
- pyreduce/estimate_background_scatter.py +130 -0
- pyreduce/extract.py +1362 -0
- pyreduce/extraction_width.py +77 -0
- pyreduce/instruments/__init__.py +0 -0
- pyreduce/instruments/aj.py +9 -0
- pyreduce/instruments/aj.yaml +51 -0
- pyreduce/instruments/andes.py +102 -0
- pyreduce/instruments/andes.yaml +72 -0
- pyreduce/instruments/common.py +711 -0
- pyreduce/instruments/common.yaml +57 -0
- pyreduce/instruments/crires_plus.py +103 -0
- pyreduce/instruments/crires_plus.yaml +101 -0
- pyreduce/instruments/filters.py +195 -0
- pyreduce/instruments/harpn.py +203 -0
- pyreduce/instruments/harpn.yaml +140 -0
- pyreduce/instruments/harps.py +312 -0
- pyreduce/instruments/harps.yaml +144 -0
- pyreduce/instruments/instrument_info.py +140 -0
- pyreduce/instruments/jwst_miri.py +29 -0
- pyreduce/instruments/jwst_miri.yaml +53 -0
- pyreduce/instruments/jwst_niriss.py +98 -0
- pyreduce/instruments/jwst_niriss.yaml +60 -0
- pyreduce/instruments/lick_apf.py +35 -0
- pyreduce/instruments/lick_apf.yaml +60 -0
- pyreduce/instruments/mcdonald.py +123 -0
- pyreduce/instruments/mcdonald.yaml +56 -0
- pyreduce/instruments/metis_ifu.py +45 -0
- pyreduce/instruments/metis_ifu.yaml +62 -0
- pyreduce/instruments/metis_lss.py +45 -0
- pyreduce/instruments/metis_lss.yaml +62 -0
- pyreduce/instruments/micado.py +45 -0
- pyreduce/instruments/micado.yaml +62 -0
- pyreduce/instruments/models.py +257 -0
- pyreduce/instruments/neid.py +156 -0
- pyreduce/instruments/neid.yaml +61 -0
- pyreduce/instruments/nirspec.py +215 -0
- pyreduce/instruments/nirspec.yaml +63 -0
- pyreduce/instruments/nte.py +42 -0
- pyreduce/instruments/nte.yaml +55 -0
- pyreduce/instruments/uves.py +46 -0
- pyreduce/instruments/uves.yaml +65 -0
- pyreduce/instruments/xshooter.py +39 -0
- pyreduce/instruments/xshooter.yaml +63 -0
- pyreduce/make_shear.py +607 -0
- pyreduce/masks/mask_crires_plus_det1.fits.gz +0 -0
- pyreduce/masks/mask_crires_plus_det2.fits.gz +0 -0
- pyreduce/masks/mask_crires_plus_det3.fits.gz +0 -0
- pyreduce/masks/mask_ctio_chiron.fits.gz +0 -0
- pyreduce/masks/mask_elodie.fits.gz +0 -0
- pyreduce/masks/mask_feros3.fits.gz +0 -0
- pyreduce/masks/mask_flames_giraffe.fits.gz +0 -0
- pyreduce/masks/mask_harps_blue.fits.gz +0 -0
- pyreduce/masks/mask_harps_red.fits.gz +0 -0
- pyreduce/masks/mask_hds_blue.fits.gz +0 -0
- pyreduce/masks/mask_hds_red.fits.gz +0 -0
- pyreduce/masks/mask_het_hrs_2x5.fits.gz +0 -0
- pyreduce/masks/mask_jwst_miri_lrs_slitless.fits.gz +0 -0
- pyreduce/masks/mask_jwst_niriss_gr700xd.fits.gz +0 -0
- pyreduce/masks/mask_lick_apf_.fits.gz +0 -0
- pyreduce/masks/mask_mcdonald.fits.gz +0 -0
- pyreduce/masks/mask_nes.fits.gz +0 -0
- pyreduce/masks/mask_nirspec_nirspec.fits.gz +0 -0
- pyreduce/masks/mask_sarg.fits.gz +0 -0
- pyreduce/masks/mask_sarg_2x2a.fits.gz +0 -0
- pyreduce/masks/mask_sarg_2x2b.fits.gz +0 -0
- pyreduce/masks/mask_subaru_hds_red.fits.gz +0 -0
- pyreduce/masks/mask_uves_blue.fits.gz +0 -0
- pyreduce/masks/mask_uves_blue_binned_2_2.fits.gz +0 -0
- pyreduce/masks/mask_uves_middle.fits.gz +0 -0
- pyreduce/masks/mask_uves_middle_2x2_split.fits.gz +0 -0
- pyreduce/masks/mask_uves_middle_binned_2_2.fits.gz +0 -0
- pyreduce/masks/mask_uves_red.fits.gz +0 -0
- pyreduce/masks/mask_uves_red_2x2.fits.gz +0 -0
- pyreduce/masks/mask_uves_red_2x2_split.fits.gz +0 -0
- pyreduce/masks/mask_uves_red_binned_2_2.fits.gz +0 -0
- pyreduce/masks/mask_xshooter_nir.fits.gz +0 -0
- pyreduce/pipeline.py +619 -0
- pyreduce/rectify.py +138 -0
- pyreduce/reduce.py +2065 -0
- pyreduce/settings/settings_AJ.json +19 -0
- pyreduce/settings/settings_ANDES.json +89 -0
- pyreduce/settings/settings_CRIRES_PLUS.json +89 -0
- pyreduce/settings/settings_HARPN.json +73 -0
- pyreduce/settings/settings_HARPS.json +69 -0
- pyreduce/settings/settings_JWST_MIRI.json +55 -0
- pyreduce/settings/settings_JWST_NIRISS.json +55 -0
- pyreduce/settings/settings_LICK_APF.json +62 -0
- pyreduce/settings/settings_MCDONALD.json +58 -0
- pyreduce/settings/settings_METIS_IFU.json +77 -0
- pyreduce/settings/settings_METIS_LSS.json +77 -0
- pyreduce/settings/settings_MICADO.json +78 -0
- pyreduce/settings/settings_NEID.json +73 -0
- pyreduce/settings/settings_NIRSPEC.json +58 -0
- pyreduce/settings/settings_NTE.json +60 -0
- pyreduce/settings/settings_UVES.json +54 -0
- pyreduce/settings/settings_XSHOOTER.json +78 -0
- pyreduce/settings/settings_pyreduce.json +184 -0
- pyreduce/settings/settings_schema.json +850 -0
- pyreduce/tools/__init__.py +0 -0
- pyreduce/tools/combine.py +117 -0
- pyreduce/trace.py +979 -0
- pyreduce/util.py +1366 -0
- pyreduce/wavecal/MICADO_HK_3arcsec_chip5.npz +0 -0
- pyreduce/wavecal/atlas/thar.fits +4946 -13
- pyreduce/wavecal/atlas/thar_list.txt +4172 -0
- pyreduce/wavecal/atlas/une.fits +0 -0
- pyreduce/wavecal/convert.py +38 -0
- pyreduce/wavecal/crires_plus_J1228_Open_det1.npz +0 -0
- pyreduce/wavecal/crires_plus_J1228_Open_det2.npz +0 -0
- pyreduce/wavecal/crires_plus_J1228_Open_det3.npz +0 -0
- pyreduce/wavecal/harpn_harpn_2D.npz +0 -0
- pyreduce/wavecal/harps_blue_2D.npz +0 -0
- pyreduce/wavecal/harps_blue_pol_2D.npz +0 -0
- pyreduce/wavecal/harps_red_2D.npz +0 -0
- pyreduce/wavecal/harps_red_pol_2D.npz +0 -0
- pyreduce/wavecal/mcdonald.npz +0 -0
- pyreduce/wavecal/metis_lss_l_2D.npz +0 -0
- pyreduce/wavecal/metis_lss_m_2D.npz +0 -0
- pyreduce/wavecal/nirspec_K2.npz +0 -0
- pyreduce/wavecal/uves_blue_360nm_2D.npz +0 -0
- pyreduce/wavecal/uves_blue_390nm_2D.npz +0 -0
- pyreduce/wavecal/uves_blue_437nm_2D.npz +0 -0
- pyreduce/wavecal/uves_middle_2x2_2D.npz +0 -0
- pyreduce/wavecal/uves_middle_565nm_2D.npz +0 -0
- pyreduce/wavecal/uves_middle_580nm_2D.npz +0 -0
- pyreduce/wavecal/uves_middle_600nm_2D.npz +0 -0
- pyreduce/wavecal/uves_middle_665nm_2D.npz +0 -0
- pyreduce/wavecal/uves_middle_860nm_2D.npz +0 -0
- pyreduce/wavecal/uves_red_580nm_2D.npz +0 -0
- pyreduce/wavecal/uves_red_600nm_2D.npz +0 -0
- pyreduce/wavecal/uves_red_665nm_2D.npz +0 -0
- pyreduce/wavecal/uves_red_760nm_2D.npz +0 -0
- pyreduce/wavecal/uves_red_860nm_2D.npz +0 -0
- pyreduce/wavecal/xshooter_nir.npz +0 -0
- pyreduce/wavelength_calibration.py +1871 -0
- pyreduce_astro-0.7a4.dist-info/METADATA +106 -0
- pyreduce_astro-0.7a4.dist-info/RECORD +182 -0
- pyreduce_astro-0.7a4.dist-info/WHEEL +4 -0
- pyreduce_astro-0.7a4.dist-info/entry_points.txt +2 -0
- pyreduce_astro-0.7a4.dist-info/licenses/LICENSE +674 -0
pyreduce/pipeline.py
ADDED
|
@@ -0,0 +1,619 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Fluent Pipeline API for PyReduce.
|
|
3
|
+
|
|
4
|
+
Provides a cleaner interface for building and running reduction pipelines.
|
|
5
|
+
Wraps the existing Step classes internally for backward compatibility.
|
|
6
|
+
|
|
7
|
+
Example usage:
|
|
8
|
+
from pyreduce.pipeline import Pipeline
|
|
9
|
+
|
|
10
|
+
# Simple: auto-discover files for an instrument
|
|
11
|
+
result = Pipeline.from_instrument(
|
|
12
|
+
instrument="UVES",
|
|
13
|
+
target="HD132205",
|
|
14
|
+
night="2010-04-01",
|
|
15
|
+
arm="middle",
|
|
16
|
+
base_dir="/data",
|
|
17
|
+
).run()
|
|
18
|
+
|
|
19
|
+
# Or build manually with explicit files:
|
|
20
|
+
result = (
|
|
21
|
+
Pipeline("UVES", output_dir, config=settings)
|
|
22
|
+
.bias(bias_files)
|
|
23
|
+
.flat(flat_files)
|
|
24
|
+
.trace_orders(order_files)
|
|
25
|
+
.extract(science_files)
|
|
26
|
+
.run()
|
|
27
|
+
)
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import logging
|
|
33
|
+
import os
|
|
34
|
+
from os.path import join
|
|
35
|
+
from typing import TYPE_CHECKING
|
|
36
|
+
|
|
37
|
+
from . import util
|
|
38
|
+
from .configuration import load_config
|
|
39
|
+
from .instruments.instrument_info import load_instrument
|
|
40
|
+
from .reduce import (
|
|
41
|
+
BackgroundScatter,
|
|
42
|
+
Bias,
|
|
43
|
+
ContinuumNormalization,
|
|
44
|
+
Finalize,
|
|
45
|
+
Flat,
|
|
46
|
+
LaserFrequencyCombFinalize,
|
|
47
|
+
LaserFrequencyCombMaster,
|
|
48
|
+
Mask,
|
|
49
|
+
NormalizeFlatField,
|
|
50
|
+
OrderTracing,
|
|
51
|
+
RectifyImage,
|
|
52
|
+
ScienceExtraction,
|
|
53
|
+
SlitCurvatureDetermination,
|
|
54
|
+
WavelengthCalibrationFinalize,
|
|
55
|
+
WavelengthCalibrationInitialize,
|
|
56
|
+
WavelengthCalibrationMaster,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
if TYPE_CHECKING:
|
|
60
|
+
from .instruments.common import Instrument
|
|
61
|
+
|
|
62
|
+
logger = logging.getLogger(__name__)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class Pipeline:
|
|
66
|
+
"""Fluent API for building reduction pipelines."""
|
|
67
|
+
|
|
68
|
+
STEP_CLASSES = {
|
|
69
|
+
"mask": Mask,
|
|
70
|
+
"bias": Bias,
|
|
71
|
+
"flat": Flat,
|
|
72
|
+
"orders": OrderTracing,
|
|
73
|
+
"scatter": BackgroundScatter,
|
|
74
|
+
"norm_flat": NormalizeFlatField,
|
|
75
|
+
"wavecal_master": WavelengthCalibrationMaster,
|
|
76
|
+
"wavecal_init": WavelengthCalibrationInitialize,
|
|
77
|
+
"wavecal": WavelengthCalibrationFinalize,
|
|
78
|
+
"freq_comb_master": LaserFrequencyCombMaster,
|
|
79
|
+
"freq_comb": LaserFrequencyCombFinalize,
|
|
80
|
+
"curvature": SlitCurvatureDetermination,
|
|
81
|
+
"science": ScienceExtraction,
|
|
82
|
+
"continuum": ContinuumNormalization,
|
|
83
|
+
"finalize": Finalize,
|
|
84
|
+
"rectify": RectifyImage,
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
STEP_ORDER = {
|
|
88
|
+
"mask": 5,
|
|
89
|
+
"bias": 10,
|
|
90
|
+
"flat": 20,
|
|
91
|
+
"orders": 30,
|
|
92
|
+
"curvature": 40,
|
|
93
|
+
"scatter": 45,
|
|
94
|
+
"norm_flat": 50,
|
|
95
|
+
"wavecal_master": 60,
|
|
96
|
+
"wavecal_init": 64,
|
|
97
|
+
"wavecal": 67,
|
|
98
|
+
"freq_comb_master": 70,
|
|
99
|
+
"freq_comb": 72,
|
|
100
|
+
"rectify": 75,
|
|
101
|
+
"science": 80,
|
|
102
|
+
"continuum": 90,
|
|
103
|
+
"finalize": 100,
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
def __init__(
|
|
107
|
+
self,
|
|
108
|
+
instrument: Instrument | str,
|
|
109
|
+
output_dir: str,
|
|
110
|
+
target: str = "",
|
|
111
|
+
arm: str = "",
|
|
112
|
+
night: str = "",
|
|
113
|
+
config: dict | None = None,
|
|
114
|
+
order_range: tuple[int, int] | None = None,
|
|
115
|
+
plot: int = 0,
|
|
116
|
+
plot_dir: str | None = None,
|
|
117
|
+
):
|
|
118
|
+
"""Initialize a reduction pipeline.
|
|
119
|
+
|
|
120
|
+
Parameters
|
|
121
|
+
----------
|
|
122
|
+
instrument : Instrument or str
|
|
123
|
+
Instrument instance or name to load
|
|
124
|
+
output_dir : str
|
|
125
|
+
Directory for output files
|
|
126
|
+
target : str, optional
|
|
127
|
+
Target name for output file naming
|
|
128
|
+
arm : str, optional
|
|
129
|
+
Instrument arm (e.g., "RED", "BLUE")
|
|
130
|
+
night : str, optional
|
|
131
|
+
Observation night string
|
|
132
|
+
config : dict, optional
|
|
133
|
+
Configuration dict with step-specific settings
|
|
134
|
+
order_range : tuple, optional
|
|
135
|
+
(first, last+1) orders to process
|
|
136
|
+
plot : int, optional
|
|
137
|
+
Plot level (0=off, 1=basic, 2=detailed). Default 0.
|
|
138
|
+
plot_dir : str, optional
|
|
139
|
+
Directory to save plots as PNG files. If None, plots are shown interactively.
|
|
140
|
+
"""
|
|
141
|
+
if isinstance(instrument, str):
|
|
142
|
+
instrument = load_instrument(instrument)
|
|
143
|
+
|
|
144
|
+
self.instrument = instrument
|
|
145
|
+
self.output_dir = output_dir.format(
|
|
146
|
+
instrument=instrument.name.upper(),
|
|
147
|
+
target=target,
|
|
148
|
+
night=night,
|
|
149
|
+
arm=arm,
|
|
150
|
+
)
|
|
151
|
+
self.target = target
|
|
152
|
+
self.arm = arm
|
|
153
|
+
self.night = night
|
|
154
|
+
self.config = config or {}
|
|
155
|
+
self.order_range = order_range
|
|
156
|
+
self.plot = plot
|
|
157
|
+
self.plot_dir = plot_dir
|
|
158
|
+
|
|
159
|
+
# Set global plot directory for util.show_or_save()
|
|
160
|
+
util.set_plot_dir(plot_dir)
|
|
161
|
+
|
|
162
|
+
self._steps: list[tuple[str, list | None]] = []
|
|
163
|
+
self._data: dict = {}
|
|
164
|
+
self._files: dict = {}
|
|
165
|
+
|
|
166
|
+
def _add_step(self, name: str, files: list | None = None) -> Pipeline:
|
|
167
|
+
"""Add a step to the pipeline."""
|
|
168
|
+
self._steps.append((name, files))
|
|
169
|
+
if files is not None:
|
|
170
|
+
self._files[name] = files
|
|
171
|
+
return self
|
|
172
|
+
|
|
173
|
+
# Step methods - fluent API
|
|
174
|
+
|
|
175
|
+
def mask(self) -> Pipeline:
|
|
176
|
+
"""Load or create bad pixel mask."""
|
|
177
|
+
return self._add_step("mask")
|
|
178
|
+
|
|
179
|
+
def bias(self, files: list[str]) -> Pipeline:
|
|
180
|
+
"""Combine bias frames into master bias."""
|
|
181
|
+
return self._add_step("bias", files)
|
|
182
|
+
|
|
183
|
+
def flat(self, files: list[str]) -> Pipeline:
|
|
184
|
+
"""Combine flat frames into master flat."""
|
|
185
|
+
return self._add_step("flat", files)
|
|
186
|
+
|
|
187
|
+
def trace_orders(self, files: list[str] | None = None) -> Pipeline:
|
|
188
|
+
"""Trace echelle orders on flat field.
|
|
189
|
+
|
|
190
|
+
If files not provided, uses flat from previous step.
|
|
191
|
+
"""
|
|
192
|
+
return self._add_step("orders", files)
|
|
193
|
+
|
|
194
|
+
def curvature(self, files: list[str] | None = None) -> Pipeline:
|
|
195
|
+
"""Determine slit curvature (tilt/shear)."""
|
|
196
|
+
return self._add_step("curvature", files)
|
|
197
|
+
|
|
198
|
+
def scatter(self, files: list[str] | None = None) -> Pipeline:
|
|
199
|
+
"""Fit background scatter model."""
|
|
200
|
+
return self._add_step("scatter", files)
|
|
201
|
+
|
|
202
|
+
def normalize_flat(self) -> Pipeline:
|
|
203
|
+
"""Normalize flat field, extract blaze function."""
|
|
204
|
+
return self._add_step("norm_flat")
|
|
205
|
+
|
|
206
|
+
def wavecal_master(self, files: list[str]) -> Pipeline:
|
|
207
|
+
"""Extract wavelength calibration spectrum."""
|
|
208
|
+
return self._add_step("wavecal_master", files)
|
|
209
|
+
|
|
210
|
+
def wavecal_init(self) -> Pipeline:
|
|
211
|
+
"""Initialize wavelength solution from line atlas."""
|
|
212
|
+
return self._add_step("wavecal_init")
|
|
213
|
+
|
|
214
|
+
def wavecal(self) -> Pipeline:
|
|
215
|
+
"""Finalize wavelength calibration."""
|
|
216
|
+
return self._add_step("wavecal")
|
|
217
|
+
|
|
218
|
+
def wavelength_calibration(self, files: list[str]) -> Pipeline:
|
|
219
|
+
"""Full wavelength calibration (master + init + finalize)."""
|
|
220
|
+
return self.wavecal_master(files).wavecal_init().wavecal()
|
|
221
|
+
|
|
222
|
+
def freq_comb_master(self, files: list[str]) -> Pipeline:
|
|
223
|
+
"""Extract laser frequency comb spectrum."""
|
|
224
|
+
return self._add_step("freq_comb_master", files)
|
|
225
|
+
|
|
226
|
+
def freq_comb(self) -> Pipeline:
|
|
227
|
+
"""Finalize frequency comb calibration."""
|
|
228
|
+
return self._add_step("freq_comb")
|
|
229
|
+
|
|
230
|
+
def extract(self, files: list[str]) -> Pipeline:
|
|
231
|
+
"""Extract science spectra."""
|
|
232
|
+
return self._add_step("science", files)
|
|
233
|
+
|
|
234
|
+
def continuum(self) -> Pipeline:
|
|
235
|
+
"""Normalize continuum."""
|
|
236
|
+
return self._add_step("continuum")
|
|
237
|
+
|
|
238
|
+
def finalize(self) -> Pipeline:
|
|
239
|
+
"""Write final output files."""
|
|
240
|
+
return self._add_step("finalize")
|
|
241
|
+
|
|
242
|
+
def rectify(self) -> Pipeline:
|
|
243
|
+
"""Rectify 2D image."""
|
|
244
|
+
return self._add_step("rectify")
|
|
245
|
+
|
|
246
|
+
# Loading intermediate results
|
|
247
|
+
|
|
248
|
+
def load(self, step: str, data=None) -> Pipeline:
|
|
249
|
+
"""Load intermediate result instead of computing.
|
|
250
|
+
|
|
251
|
+
Parameters
|
|
252
|
+
----------
|
|
253
|
+
step : str
|
|
254
|
+
Name of step whose output to load
|
|
255
|
+
data : any, optional
|
|
256
|
+
Data to use directly instead of loading from disk
|
|
257
|
+
"""
|
|
258
|
+
if data is not None:
|
|
259
|
+
self._data[step] = data
|
|
260
|
+
else:
|
|
261
|
+
# Will be loaded during run()
|
|
262
|
+
self._data[step] = None # Marker to load
|
|
263
|
+
return self
|
|
264
|
+
|
|
265
|
+
# Execution
|
|
266
|
+
|
|
267
|
+
def _get_step_inputs(self) -> tuple:
|
|
268
|
+
"""Get the standard inputs for Step classes."""
|
|
269
|
+
return (
|
|
270
|
+
self.instrument,
|
|
271
|
+
self.arm,
|
|
272
|
+
self.target,
|
|
273
|
+
self.night,
|
|
274
|
+
self.output_dir,
|
|
275
|
+
self.order_range,
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
def _run_step(self, name: str, files: list | None, load_only: bool = False):
|
|
279
|
+
"""Run or load a single step."""
|
|
280
|
+
step_class = self.STEP_CLASSES[name]
|
|
281
|
+
step_config = self.config.get(name, {}).copy()
|
|
282
|
+
step_config["plot"] = self.plot # Runtime plot setting
|
|
283
|
+
step = step_class(*self._get_step_inputs(), **step_config)
|
|
284
|
+
|
|
285
|
+
# Get dependencies
|
|
286
|
+
deps = step.loadDependsOn if load_only else step.dependsOn
|
|
287
|
+
for dep in deps:
|
|
288
|
+
if dep not in self._data:
|
|
289
|
+
self._ensure_dependency(dep)
|
|
290
|
+
dep_args = {d: self._data[d] for d in deps}
|
|
291
|
+
|
|
292
|
+
if load_only:
|
|
293
|
+
try:
|
|
294
|
+
logger.info("Loading data from step '%s'", name)
|
|
295
|
+
return step.load(**dep_args)
|
|
296
|
+
except FileNotFoundError:
|
|
297
|
+
logger.warning(
|
|
298
|
+
"Intermediate files for step '%s' not found, running instead.",
|
|
299
|
+
name,
|
|
300
|
+
)
|
|
301
|
+
return self._run_step(name, files, load_only=False)
|
|
302
|
+
|
|
303
|
+
logger.info("Running step '%s'", name)
|
|
304
|
+
if files is not None:
|
|
305
|
+
dep_args["files"] = files
|
|
306
|
+
return step.run(**dep_args)
|
|
307
|
+
|
|
308
|
+
def _ensure_dependency(self, name: str):
|
|
309
|
+
"""Ensure a dependency is available (load if needed)."""
|
|
310
|
+
if name in self._data:
|
|
311
|
+
return
|
|
312
|
+
|
|
313
|
+
# 'config' is a special dependency - it's the full config dict, not a step
|
|
314
|
+
if name == "config":
|
|
315
|
+
self._data["config"] = self.config
|
|
316
|
+
return
|
|
317
|
+
|
|
318
|
+
files = self._files.get(name)
|
|
319
|
+
self._data[name] = self._run_step(name, files, load_only=True)
|
|
320
|
+
|
|
321
|
+
def run(self, skip_existing: bool = False) -> dict:
|
|
322
|
+
"""Execute all queued steps.
|
|
323
|
+
|
|
324
|
+
Parameters
|
|
325
|
+
----------
|
|
326
|
+
skip_existing : bool
|
|
327
|
+
If True, skip steps whose output files already exist
|
|
328
|
+
|
|
329
|
+
Returns
|
|
330
|
+
-------
|
|
331
|
+
dict
|
|
332
|
+
Results keyed by step name
|
|
333
|
+
"""
|
|
334
|
+
# Create output directory
|
|
335
|
+
if not os.path.exists(self.output_dir):
|
|
336
|
+
os.makedirs(self.output_dir)
|
|
337
|
+
|
|
338
|
+
# Sort steps by execution order
|
|
339
|
+
sorted_steps = sorted(self._steps, key=lambda x: self.STEP_ORDER.get(x[0], 999))
|
|
340
|
+
|
|
341
|
+
for name, files in sorted_steps:
|
|
342
|
+
# Check if already computed
|
|
343
|
+
if name in self._data and self._data[name] is not None:
|
|
344
|
+
continue
|
|
345
|
+
|
|
346
|
+
result = self._run_step(name, files)
|
|
347
|
+
self._data[name] = result
|
|
348
|
+
|
|
349
|
+
return self._data
|
|
350
|
+
|
|
351
|
+
@property
|
|
352
|
+
def results(self) -> dict:
|
|
353
|
+
"""Access results after run()."""
|
|
354
|
+
return self._data
|
|
355
|
+
|
|
356
|
+
@classmethod
|
|
357
|
+
def from_files(
|
|
358
|
+
cls,
|
|
359
|
+
files: dict,
|
|
360
|
+
output_dir: str,
|
|
361
|
+
target: str,
|
|
362
|
+
instrument,
|
|
363
|
+
arm: str,
|
|
364
|
+
night: str,
|
|
365
|
+
config: dict,
|
|
366
|
+
order_range=None,
|
|
367
|
+
steps="all",
|
|
368
|
+
plot: int = 0,
|
|
369
|
+
plot_dir: str | None = None,
|
|
370
|
+
) -> Pipeline:
|
|
371
|
+
"""Create pipeline from a files dict and run specified steps.
|
|
372
|
+
|
|
373
|
+
This provides a simpler interface similar to the legacy Reducer class.
|
|
374
|
+
|
|
375
|
+
Parameters
|
|
376
|
+
----------
|
|
377
|
+
files : dict
|
|
378
|
+
Files for each step (bias, flat, orders, wavecal, science, etc.)
|
|
379
|
+
output_dir : str
|
|
380
|
+
Output directory
|
|
381
|
+
target : str
|
|
382
|
+
Target name
|
|
383
|
+
instrument : Instrument or str
|
|
384
|
+
Instrument instance or name
|
|
385
|
+
arm : str
|
|
386
|
+
Instrument arm
|
|
387
|
+
night : str
|
|
388
|
+
Observation night
|
|
389
|
+
config : dict
|
|
390
|
+
Configuration dict
|
|
391
|
+
order_range : tuple, optional
|
|
392
|
+
Order range to process
|
|
393
|
+
steps : list or "all"
|
|
394
|
+
Steps to run
|
|
395
|
+
plot : int, optional
|
|
396
|
+
Plot level (0=off, 1=basic, 2=detailed). Default 0.
|
|
397
|
+
plot_dir : str, optional
|
|
398
|
+
Directory to save plots as PNG files. If None, plots are shown interactively.
|
|
399
|
+
|
|
400
|
+
Returns
|
|
401
|
+
-------
|
|
402
|
+
Pipeline
|
|
403
|
+
Configured pipeline ready to run
|
|
404
|
+
"""
|
|
405
|
+
pipe = cls(
|
|
406
|
+
instrument=instrument,
|
|
407
|
+
output_dir=output_dir,
|
|
408
|
+
target=target,
|
|
409
|
+
arm=arm,
|
|
410
|
+
night=night,
|
|
411
|
+
config=config,
|
|
412
|
+
order_range=order_range,
|
|
413
|
+
plot=plot,
|
|
414
|
+
plot_dir=plot_dir,
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
if steps == "all":
|
|
418
|
+
steps = list(cls.STEP_ORDER.keys())
|
|
419
|
+
|
|
420
|
+
# Register files for steps that may be needed as dependencies
|
|
421
|
+
# (even if the step itself isn't in the steps list)
|
|
422
|
+
for key in [
|
|
423
|
+
"bias",
|
|
424
|
+
"flat",
|
|
425
|
+
"orders",
|
|
426
|
+
"curvature",
|
|
427
|
+
"scatter",
|
|
428
|
+
"wavecal_master",
|
|
429
|
+
"freq_comb_master",
|
|
430
|
+
"science",
|
|
431
|
+
]:
|
|
432
|
+
if key in files and len(files.get(key, [])):
|
|
433
|
+
pipe._files[key] = files[key]
|
|
434
|
+
|
|
435
|
+
# Map step names to pipeline methods
|
|
436
|
+
# Use len() for truth checks since files can be numpy arrays
|
|
437
|
+
step_map = {
|
|
438
|
+
"bias": lambda: pipe.bias(files.get("bias", []))
|
|
439
|
+
if len(files.get("bias", []))
|
|
440
|
+
else pipe,
|
|
441
|
+
"flat": lambda: pipe.flat(files.get("flat", []))
|
|
442
|
+
if len(files.get("flat", []))
|
|
443
|
+
else pipe,
|
|
444
|
+
"orders": lambda: pipe.trace_orders(files.get("orders")),
|
|
445
|
+
"curvature": lambda: pipe.curvature(files.get("curvature")),
|
|
446
|
+
"scatter": lambda: pipe.scatter(files.get("scatter")),
|
|
447
|
+
"norm_flat": lambda: pipe.normalize_flat(),
|
|
448
|
+
"wavecal_master": lambda: pipe.wavecal_master(
|
|
449
|
+
files.get("wavecal_master", [])
|
|
450
|
+
)
|
|
451
|
+
if len(files.get("wavecal_master", []))
|
|
452
|
+
else pipe,
|
|
453
|
+
"wavecal_init": lambda: pipe.wavecal_init(),
|
|
454
|
+
"wavecal": lambda: pipe.wavecal(),
|
|
455
|
+
"freq_comb_master": lambda: pipe.freq_comb_master(
|
|
456
|
+
files.get("freq_comb_master", [])
|
|
457
|
+
)
|
|
458
|
+
if len(files.get("freq_comb_master", []))
|
|
459
|
+
else pipe,
|
|
460
|
+
"freq_comb": lambda: pipe.freq_comb(),
|
|
461
|
+
"rectify": lambda: pipe.rectify(),
|
|
462
|
+
"science": lambda: pipe.extract(files.get("science", []))
|
|
463
|
+
if len(files.get("science", []))
|
|
464
|
+
else pipe,
|
|
465
|
+
"continuum": lambda: pipe.continuum(),
|
|
466
|
+
"finalize": lambda: pipe.finalize(),
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
for step in steps:
|
|
470
|
+
if step in step_map:
|
|
471
|
+
step_map[step]()
|
|
472
|
+
|
|
473
|
+
return pipe
|
|
474
|
+
|
|
475
|
+
@classmethod
|
|
476
|
+
def from_instrument(
|
|
477
|
+
cls,
|
|
478
|
+
instrument: str,
|
|
479
|
+
target: str,
|
|
480
|
+
night: str | None = None,
|
|
481
|
+
arm: str | None = None,
|
|
482
|
+
steps: tuple | list | str = "all",
|
|
483
|
+
base_dir: str | None = None,
|
|
484
|
+
input_dir: str | None = None,
|
|
485
|
+
output_dir: str | None = None,
|
|
486
|
+
configuration: dict | None = None,
|
|
487
|
+
order_range: tuple[int, int] | None = None,
|
|
488
|
+
allow_calibration_only: bool = False,
|
|
489
|
+
plot: int = 0,
|
|
490
|
+
plot_dir: str | None = None,
|
|
491
|
+
) -> Pipeline:
|
|
492
|
+
"""Create pipeline from instrument name with automatic file discovery.
|
|
493
|
+
|
|
494
|
+
This is the recommended entry point for running reductions. It handles
|
|
495
|
+
loading the instrument, finding and sorting files, and setting up
|
|
496
|
+
the pipeline with the correct configuration.
|
|
497
|
+
|
|
498
|
+
Parameters
|
|
499
|
+
----------
|
|
500
|
+
instrument : str
|
|
501
|
+
Instrument name (e.g., "UVES", "HARPS", "XSHOOTER")
|
|
502
|
+
target : str
|
|
503
|
+
Target name or regex pattern to match in headers
|
|
504
|
+
night : str, optional
|
|
505
|
+
Observation night (YYYY-MM-DD format or regex)
|
|
506
|
+
arm : str, optional
|
|
507
|
+
Instrument arm (e.g., "RED", "BLUE", "middle"). If None,
|
|
508
|
+
uses all available arms for the instrument.
|
|
509
|
+
steps : tuple, list, or "all"
|
|
510
|
+
Steps to run. Default "all" runs all applicable steps.
|
|
511
|
+
base_dir : str, optional
|
|
512
|
+
Base directory for data. Default: $REDUCE_DATA or ~/REDUCE_DATA
|
|
513
|
+
input_dir : str, optional
|
|
514
|
+
Input directory relative to base_dir. Default: from config
|
|
515
|
+
output_dir : str, optional
|
|
516
|
+
Output directory relative to base_dir. Default: from config
|
|
517
|
+
configuration : dict, optional
|
|
518
|
+
Configuration overrides. Default: instrument defaults
|
|
519
|
+
order_range : tuple, optional
|
|
520
|
+
(first, last+1) orders to process
|
|
521
|
+
allow_calibration_only : bool
|
|
522
|
+
If True, allow running without science files
|
|
523
|
+
plot : int
|
|
524
|
+
Plot level (0=off, 1=basic, 2=detailed)
|
|
525
|
+
plot_dir : str, optional
|
|
526
|
+
Directory to save plots. If None, shows interactively.
|
|
527
|
+
|
|
528
|
+
Returns
|
|
529
|
+
-------
|
|
530
|
+
Pipeline
|
|
531
|
+
Configured pipeline ready to call .run()
|
|
532
|
+
|
|
533
|
+
Example
|
|
534
|
+
-------
|
|
535
|
+
>>> result = Pipeline.from_instrument(
|
|
536
|
+
... instrument="UVES",
|
|
537
|
+
... target="HD132205",
|
|
538
|
+
... night="2010-04-01",
|
|
539
|
+
... arm="middle",
|
|
540
|
+
... steps=("bias", "flat", "orders", "science"),
|
|
541
|
+
... ).run()
|
|
542
|
+
"""
|
|
543
|
+
# Environment variable overrides for plot
|
|
544
|
+
if "PYREDUCE_PLOT" in os.environ:
|
|
545
|
+
plot = int(os.environ["PYREDUCE_PLOT"])
|
|
546
|
+
if "PYREDUCE_PLOT_DIR" in os.environ:
|
|
547
|
+
plot_dir = os.environ["PYREDUCE_PLOT_DIR"]
|
|
548
|
+
|
|
549
|
+
# Set global plot directory
|
|
550
|
+
util.set_plot_dir(plot_dir)
|
|
551
|
+
|
|
552
|
+
# Load configuration
|
|
553
|
+
config = load_config(configuration, instrument, 0)
|
|
554
|
+
|
|
555
|
+
# Load instrument
|
|
556
|
+
inst = load_instrument(instrument)
|
|
557
|
+
info = inst.info
|
|
558
|
+
|
|
559
|
+
# Get directories from config if not specified
|
|
560
|
+
if base_dir is None:
|
|
561
|
+
base_dir = config["reduce"]["base_dir"]
|
|
562
|
+
if input_dir is None:
|
|
563
|
+
input_dir = config["reduce"]["input_dir"]
|
|
564
|
+
if output_dir is None:
|
|
565
|
+
output_dir = config["reduce"]["output_dir"]
|
|
566
|
+
|
|
567
|
+
full_input_dir = join(base_dir, input_dir)
|
|
568
|
+
full_output_dir = join(base_dir, output_dir)
|
|
569
|
+
|
|
570
|
+
# Get arms to process
|
|
571
|
+
if arm is None:
|
|
572
|
+
arms = info["arms"]
|
|
573
|
+
else:
|
|
574
|
+
arms = [arm] if isinstance(arm, str) else arm
|
|
575
|
+
|
|
576
|
+
# Find and sort files
|
|
577
|
+
files = inst.sort_files(
|
|
578
|
+
full_input_dir,
|
|
579
|
+
target,
|
|
580
|
+
night,
|
|
581
|
+
arm=arms[0] if len(arms) == 1 else arms[0],
|
|
582
|
+
**config["instrument"],
|
|
583
|
+
allow_calibration_only=allow_calibration_only,
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
if len(files) == 0:
|
|
587
|
+
logger.warning(
|
|
588
|
+
"No files found for instrument: %s, target: %s, night: %s, arm: %s",
|
|
589
|
+
instrument,
|
|
590
|
+
target,
|
|
591
|
+
night,
|
|
592
|
+
arm,
|
|
593
|
+
)
|
|
594
|
+
raise FileNotFoundError(
|
|
595
|
+
f"No files found for {instrument} / {target} / {night} / {arm}"
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
# Use the first file set (for single arm)
|
|
599
|
+
k, f = files[0]
|
|
600
|
+
logger.info("Pipeline settings:")
|
|
601
|
+
for key, value in k.items():
|
|
602
|
+
logger.info(" %s: %s", key, value)
|
|
603
|
+
|
|
604
|
+
# Create pipeline
|
|
605
|
+
pipe = cls.from_files(
|
|
606
|
+
files=f,
|
|
607
|
+
output_dir=full_output_dir,
|
|
608
|
+
target=k.get("target", target),
|
|
609
|
+
instrument=inst,
|
|
610
|
+
arm=arms[0],
|
|
611
|
+
night=k.get("night", night or ""),
|
|
612
|
+
config=config,
|
|
613
|
+
order_range=order_range,
|
|
614
|
+
steps=steps,
|
|
615
|
+
plot=plot,
|
|
616
|
+
plot_dir=plot_dir,
|
|
617
|
+
)
|
|
618
|
+
|
|
619
|
+
return pipe
|