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.
Files changed (182) hide show
  1. pyreduce/__init__.py +67 -0
  2. pyreduce/__main__.py +322 -0
  3. pyreduce/cli.py +342 -0
  4. pyreduce/clib/Release/_slitfunc_2d.cp311-win_amd64.exp +0 -0
  5. pyreduce/clib/Release/_slitfunc_2d.cp311-win_amd64.lib +0 -0
  6. pyreduce/clib/Release/_slitfunc_2d.cp312-win_amd64.exp +0 -0
  7. pyreduce/clib/Release/_slitfunc_2d.cp312-win_amd64.lib +0 -0
  8. pyreduce/clib/Release/_slitfunc_2d.cp313-win_amd64.exp +0 -0
  9. pyreduce/clib/Release/_slitfunc_2d.cp313-win_amd64.lib +0 -0
  10. pyreduce/clib/Release/_slitfunc_2d.cp314-win_amd64.exp +0 -0
  11. pyreduce/clib/Release/_slitfunc_2d.cp314-win_amd64.lib +0 -0
  12. pyreduce/clib/Release/_slitfunc_2d.obj +0 -0
  13. pyreduce/clib/Release/_slitfunc_bd.cp311-win_amd64.exp +0 -0
  14. pyreduce/clib/Release/_slitfunc_bd.cp311-win_amd64.lib +0 -0
  15. pyreduce/clib/Release/_slitfunc_bd.cp312-win_amd64.exp +0 -0
  16. pyreduce/clib/Release/_slitfunc_bd.cp312-win_amd64.lib +0 -0
  17. pyreduce/clib/Release/_slitfunc_bd.cp313-win_amd64.exp +0 -0
  18. pyreduce/clib/Release/_slitfunc_bd.cp313-win_amd64.lib +0 -0
  19. pyreduce/clib/Release/_slitfunc_bd.cp314-win_amd64.exp +0 -0
  20. pyreduce/clib/Release/_slitfunc_bd.cp314-win_amd64.lib +0 -0
  21. pyreduce/clib/Release/_slitfunc_bd.obj +0 -0
  22. pyreduce/clib/__init__.py +0 -0
  23. pyreduce/clib/_slitfunc_2d.cp311-win_amd64.pyd +0 -0
  24. pyreduce/clib/_slitfunc_2d.cp312-win_amd64.pyd +0 -0
  25. pyreduce/clib/_slitfunc_2d.cp313-win_amd64.pyd +0 -0
  26. pyreduce/clib/_slitfunc_2d.cp314-win_amd64.pyd +0 -0
  27. pyreduce/clib/_slitfunc_bd.cp311-win_amd64.pyd +0 -0
  28. pyreduce/clib/_slitfunc_bd.cp312-win_amd64.pyd +0 -0
  29. pyreduce/clib/_slitfunc_bd.cp313-win_amd64.pyd +0 -0
  30. pyreduce/clib/_slitfunc_bd.cp314-win_amd64.pyd +0 -0
  31. pyreduce/clib/build_extract.py +75 -0
  32. pyreduce/clib/slit_func_2d_xi_zeta_bd.c +1313 -0
  33. pyreduce/clib/slit_func_2d_xi_zeta_bd.h +55 -0
  34. pyreduce/clib/slit_func_bd.c +362 -0
  35. pyreduce/clib/slit_func_bd.h +17 -0
  36. pyreduce/clipnflip.py +147 -0
  37. pyreduce/combine_frames.py +861 -0
  38. pyreduce/configuration.py +191 -0
  39. pyreduce/continuum_normalization.py +329 -0
  40. pyreduce/cwrappers.py +404 -0
  41. pyreduce/datasets.py +238 -0
  42. pyreduce/echelle.py +413 -0
  43. pyreduce/estimate_background_scatter.py +130 -0
  44. pyreduce/extract.py +1362 -0
  45. pyreduce/extraction_width.py +77 -0
  46. pyreduce/instruments/__init__.py +0 -0
  47. pyreduce/instruments/aj.py +9 -0
  48. pyreduce/instruments/aj.yaml +51 -0
  49. pyreduce/instruments/andes.py +102 -0
  50. pyreduce/instruments/andes.yaml +72 -0
  51. pyreduce/instruments/common.py +711 -0
  52. pyreduce/instruments/common.yaml +57 -0
  53. pyreduce/instruments/crires_plus.py +103 -0
  54. pyreduce/instruments/crires_plus.yaml +101 -0
  55. pyreduce/instruments/filters.py +195 -0
  56. pyreduce/instruments/harpn.py +203 -0
  57. pyreduce/instruments/harpn.yaml +140 -0
  58. pyreduce/instruments/harps.py +312 -0
  59. pyreduce/instruments/harps.yaml +144 -0
  60. pyreduce/instruments/instrument_info.py +140 -0
  61. pyreduce/instruments/jwst_miri.py +29 -0
  62. pyreduce/instruments/jwst_miri.yaml +53 -0
  63. pyreduce/instruments/jwst_niriss.py +98 -0
  64. pyreduce/instruments/jwst_niriss.yaml +60 -0
  65. pyreduce/instruments/lick_apf.py +35 -0
  66. pyreduce/instruments/lick_apf.yaml +60 -0
  67. pyreduce/instruments/mcdonald.py +123 -0
  68. pyreduce/instruments/mcdonald.yaml +56 -0
  69. pyreduce/instruments/metis_ifu.py +45 -0
  70. pyreduce/instruments/metis_ifu.yaml +62 -0
  71. pyreduce/instruments/metis_lss.py +45 -0
  72. pyreduce/instruments/metis_lss.yaml +62 -0
  73. pyreduce/instruments/micado.py +45 -0
  74. pyreduce/instruments/micado.yaml +62 -0
  75. pyreduce/instruments/models.py +257 -0
  76. pyreduce/instruments/neid.py +156 -0
  77. pyreduce/instruments/neid.yaml +61 -0
  78. pyreduce/instruments/nirspec.py +215 -0
  79. pyreduce/instruments/nirspec.yaml +63 -0
  80. pyreduce/instruments/nte.py +42 -0
  81. pyreduce/instruments/nte.yaml +55 -0
  82. pyreduce/instruments/uves.py +46 -0
  83. pyreduce/instruments/uves.yaml +65 -0
  84. pyreduce/instruments/xshooter.py +39 -0
  85. pyreduce/instruments/xshooter.yaml +63 -0
  86. pyreduce/make_shear.py +607 -0
  87. pyreduce/masks/mask_crires_plus_det1.fits.gz +0 -0
  88. pyreduce/masks/mask_crires_plus_det2.fits.gz +0 -0
  89. pyreduce/masks/mask_crires_plus_det3.fits.gz +0 -0
  90. pyreduce/masks/mask_ctio_chiron.fits.gz +0 -0
  91. pyreduce/masks/mask_elodie.fits.gz +0 -0
  92. pyreduce/masks/mask_feros3.fits.gz +0 -0
  93. pyreduce/masks/mask_flames_giraffe.fits.gz +0 -0
  94. pyreduce/masks/mask_harps_blue.fits.gz +0 -0
  95. pyreduce/masks/mask_harps_red.fits.gz +0 -0
  96. pyreduce/masks/mask_hds_blue.fits.gz +0 -0
  97. pyreduce/masks/mask_hds_red.fits.gz +0 -0
  98. pyreduce/masks/mask_het_hrs_2x5.fits.gz +0 -0
  99. pyreduce/masks/mask_jwst_miri_lrs_slitless.fits.gz +0 -0
  100. pyreduce/masks/mask_jwst_niriss_gr700xd.fits.gz +0 -0
  101. pyreduce/masks/mask_lick_apf_.fits.gz +0 -0
  102. pyreduce/masks/mask_mcdonald.fits.gz +0 -0
  103. pyreduce/masks/mask_nes.fits.gz +0 -0
  104. pyreduce/masks/mask_nirspec_nirspec.fits.gz +0 -0
  105. pyreduce/masks/mask_sarg.fits.gz +0 -0
  106. pyreduce/masks/mask_sarg_2x2a.fits.gz +0 -0
  107. pyreduce/masks/mask_sarg_2x2b.fits.gz +0 -0
  108. pyreduce/masks/mask_subaru_hds_red.fits.gz +0 -0
  109. pyreduce/masks/mask_uves_blue.fits.gz +0 -0
  110. pyreduce/masks/mask_uves_blue_binned_2_2.fits.gz +0 -0
  111. pyreduce/masks/mask_uves_middle.fits.gz +0 -0
  112. pyreduce/masks/mask_uves_middle_2x2_split.fits.gz +0 -0
  113. pyreduce/masks/mask_uves_middle_binned_2_2.fits.gz +0 -0
  114. pyreduce/masks/mask_uves_red.fits.gz +0 -0
  115. pyreduce/masks/mask_uves_red_2x2.fits.gz +0 -0
  116. pyreduce/masks/mask_uves_red_2x2_split.fits.gz +0 -0
  117. pyreduce/masks/mask_uves_red_binned_2_2.fits.gz +0 -0
  118. pyreduce/masks/mask_xshooter_nir.fits.gz +0 -0
  119. pyreduce/pipeline.py +619 -0
  120. pyreduce/rectify.py +138 -0
  121. pyreduce/reduce.py +2065 -0
  122. pyreduce/settings/settings_AJ.json +19 -0
  123. pyreduce/settings/settings_ANDES.json +89 -0
  124. pyreduce/settings/settings_CRIRES_PLUS.json +89 -0
  125. pyreduce/settings/settings_HARPN.json +73 -0
  126. pyreduce/settings/settings_HARPS.json +69 -0
  127. pyreduce/settings/settings_JWST_MIRI.json +55 -0
  128. pyreduce/settings/settings_JWST_NIRISS.json +55 -0
  129. pyreduce/settings/settings_LICK_APF.json +62 -0
  130. pyreduce/settings/settings_MCDONALD.json +58 -0
  131. pyreduce/settings/settings_METIS_IFU.json +77 -0
  132. pyreduce/settings/settings_METIS_LSS.json +77 -0
  133. pyreduce/settings/settings_MICADO.json +78 -0
  134. pyreduce/settings/settings_NEID.json +73 -0
  135. pyreduce/settings/settings_NIRSPEC.json +58 -0
  136. pyreduce/settings/settings_NTE.json +60 -0
  137. pyreduce/settings/settings_UVES.json +54 -0
  138. pyreduce/settings/settings_XSHOOTER.json +78 -0
  139. pyreduce/settings/settings_pyreduce.json +184 -0
  140. pyreduce/settings/settings_schema.json +850 -0
  141. pyreduce/tools/__init__.py +0 -0
  142. pyreduce/tools/combine.py +117 -0
  143. pyreduce/trace.py +979 -0
  144. pyreduce/util.py +1366 -0
  145. pyreduce/wavecal/MICADO_HK_3arcsec_chip5.npz +0 -0
  146. pyreduce/wavecal/atlas/thar.fits +4946 -13
  147. pyreduce/wavecal/atlas/thar_list.txt +4172 -0
  148. pyreduce/wavecal/atlas/une.fits +0 -0
  149. pyreduce/wavecal/convert.py +38 -0
  150. pyreduce/wavecal/crires_plus_J1228_Open_det1.npz +0 -0
  151. pyreduce/wavecal/crires_plus_J1228_Open_det2.npz +0 -0
  152. pyreduce/wavecal/crires_plus_J1228_Open_det3.npz +0 -0
  153. pyreduce/wavecal/harpn_harpn_2D.npz +0 -0
  154. pyreduce/wavecal/harps_blue_2D.npz +0 -0
  155. pyreduce/wavecal/harps_blue_pol_2D.npz +0 -0
  156. pyreduce/wavecal/harps_red_2D.npz +0 -0
  157. pyreduce/wavecal/harps_red_pol_2D.npz +0 -0
  158. pyreduce/wavecal/mcdonald.npz +0 -0
  159. pyreduce/wavecal/metis_lss_l_2D.npz +0 -0
  160. pyreduce/wavecal/metis_lss_m_2D.npz +0 -0
  161. pyreduce/wavecal/nirspec_K2.npz +0 -0
  162. pyreduce/wavecal/uves_blue_360nm_2D.npz +0 -0
  163. pyreduce/wavecal/uves_blue_390nm_2D.npz +0 -0
  164. pyreduce/wavecal/uves_blue_437nm_2D.npz +0 -0
  165. pyreduce/wavecal/uves_middle_2x2_2D.npz +0 -0
  166. pyreduce/wavecal/uves_middle_565nm_2D.npz +0 -0
  167. pyreduce/wavecal/uves_middle_580nm_2D.npz +0 -0
  168. pyreduce/wavecal/uves_middle_600nm_2D.npz +0 -0
  169. pyreduce/wavecal/uves_middle_665nm_2D.npz +0 -0
  170. pyreduce/wavecal/uves_middle_860nm_2D.npz +0 -0
  171. pyreduce/wavecal/uves_red_580nm_2D.npz +0 -0
  172. pyreduce/wavecal/uves_red_600nm_2D.npz +0 -0
  173. pyreduce/wavecal/uves_red_665nm_2D.npz +0 -0
  174. pyreduce/wavecal/uves_red_760nm_2D.npz +0 -0
  175. pyreduce/wavecal/uves_red_860nm_2D.npz +0 -0
  176. pyreduce/wavecal/xshooter_nir.npz +0 -0
  177. pyreduce/wavelength_calibration.py +1871 -0
  178. pyreduce_astro-0.7a4.dist-info/METADATA +106 -0
  179. pyreduce_astro-0.7a4.dist-info/RECORD +182 -0
  180. pyreduce_astro-0.7a4.dist-info/WHEEL +4 -0
  181. pyreduce_astro-0.7a4.dist-info/entry_points.txt +2 -0
  182. 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