pyreduce-astro 0.6.0__cp314-cp314-win_amd64.whl → 0.7a2__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 (107) hide show
  1. pyreduce/__main__.py +194 -94
  2. pyreduce/cli.py +342 -0
  3. pyreduce/clib/Release/_slitfunc_2d.obj +0 -0
  4. pyreduce/clib/Release/_slitfunc_bd.obj +0 -0
  5. pyreduce/clib/_slitfunc_2d.cp311-win_amd64.pyd +0 -0
  6. pyreduce/clib/_slitfunc_2d.cp312-win_amd64.pyd +0 -0
  7. pyreduce/clib/_slitfunc_2d.cp313-win_amd64.pyd +0 -0
  8. pyreduce/clib/_slitfunc_2d.cp314-win_amd64.pyd +0 -0
  9. pyreduce/clib/_slitfunc_bd.cp311-win_amd64.pyd +0 -0
  10. pyreduce/clib/_slitfunc_bd.cp312-win_amd64.pyd +0 -0
  11. pyreduce/clib/_slitfunc_bd.cp313-win_amd64.pyd +0 -0
  12. pyreduce/clib/_slitfunc_bd.cp314-win_amd64.pyd +0 -0
  13. pyreduce/combine_frames.py +46 -40
  14. pyreduce/configuration.py +6 -1
  15. pyreduce/continuum_normalization.py +3 -3
  16. pyreduce/datasets.py +12 -5
  17. pyreduce/estimate_background_scatter.py +2 -1
  18. pyreduce/extract.py +2 -1
  19. pyreduce/instruments/aj.py +9 -0
  20. pyreduce/instruments/aj.yaml +51 -0
  21. pyreduce/instruments/andes.py +17 -17
  22. pyreduce/instruments/andes.yaml +72 -0
  23. pyreduce/instruments/common.py +141 -113
  24. pyreduce/instruments/common.yaml +57 -0
  25. pyreduce/instruments/crires_plus.py +17 -17
  26. pyreduce/instruments/crires_plus.yaml +101 -0
  27. pyreduce/instruments/filters.py +1 -1
  28. pyreduce/instruments/harpn.py +19 -17
  29. pyreduce/instruments/harpn.yaml +140 -0
  30. pyreduce/instruments/harps.py +19 -17
  31. pyreduce/instruments/harps.yaml +144 -0
  32. pyreduce/instruments/instrument_info.py +14 -14
  33. pyreduce/instruments/jwst_miri.py +4 -4
  34. pyreduce/instruments/jwst_miri.yaml +53 -0
  35. pyreduce/instruments/jwst_niriss.py +8 -8
  36. pyreduce/instruments/jwst_niriss.yaml +60 -0
  37. pyreduce/instruments/lick_apf.py +3 -3
  38. pyreduce/instruments/lick_apf.yaml +60 -0
  39. pyreduce/instruments/mcdonald.py +8 -8
  40. pyreduce/instruments/mcdonald.yaml +56 -0
  41. pyreduce/instruments/metis_ifu.py +7 -7
  42. pyreduce/instruments/metis_ifu.yaml +62 -0
  43. pyreduce/instruments/metis_lss.py +7 -7
  44. pyreduce/instruments/metis_lss.yaml +62 -0
  45. pyreduce/instruments/micado.py +5 -5
  46. pyreduce/instruments/micado.yaml +62 -0
  47. pyreduce/instruments/models.py +257 -0
  48. pyreduce/instruments/neid.py +19 -17
  49. pyreduce/instruments/neid.yaml +61 -0
  50. pyreduce/instruments/nirspec.py +12 -12
  51. pyreduce/instruments/nirspec.yaml +63 -0
  52. pyreduce/instruments/nte.py +7 -7
  53. pyreduce/instruments/nte.yaml +55 -0
  54. pyreduce/instruments/uves.py +9 -9
  55. pyreduce/instruments/uves.yaml +65 -0
  56. pyreduce/instruments/xshooter.py +6 -6
  57. pyreduce/instruments/xshooter.yaml +63 -0
  58. pyreduce/make_shear.py +3 -2
  59. pyreduce/pipeline.py +619 -0
  60. pyreduce/reduce.py +90 -230
  61. pyreduce/settings/settings_AJ.json +19 -0
  62. pyreduce/settings/settings_ANDES.json +1 -1
  63. pyreduce/settings/settings_CRIRES_PLUS.json +1 -1
  64. pyreduce/settings/settings_HARPN.json +1 -1
  65. pyreduce/settings/settings_HARPS.json +1 -1
  66. pyreduce/settings/settings_JWST_MIRI.json +1 -1
  67. pyreduce/settings/settings_JWST_NIRISS.json +1 -1
  68. pyreduce/settings/settings_LICK_APF.json +2 -2
  69. pyreduce/settings/settings_MCDONALD.json +1 -1
  70. pyreduce/settings/settings_METIS_IFU.json +1 -1
  71. pyreduce/settings/settings_METIS_LSS.json +1 -1
  72. pyreduce/settings/settings_MICADO.json +1 -1
  73. pyreduce/settings/settings_NEID.json +1 -1
  74. pyreduce/settings/settings_NIRSPEC.json +2 -2
  75. pyreduce/settings/settings_NTE.json +1 -1
  76. pyreduce/settings/settings_UVES.json +1 -1
  77. pyreduce/settings/settings_XSHOOTER.json +1 -1
  78. pyreduce/settings/settings_pyreduce.json +8 -2
  79. pyreduce/settings/settings_schema.json +27 -4
  80. pyreduce/tools/combine.py +2 -2
  81. pyreduce/{trace_orders.py → trace.py} +364 -30
  82. pyreduce/util.py +82 -4
  83. pyreduce/wavelength_calibration.py +12 -14
  84. pyreduce_astro-0.7a2.dist-info/METADATA +106 -0
  85. {pyreduce_astro-0.6.0.dist-info → pyreduce_astro-0.7a2.dist-info}/RECORD +88 -82
  86. pyreduce_astro-0.7a2.dist-info/entry_points.txt +2 -0
  87. pyreduce/instruments/andes.json +0 -61
  88. pyreduce/instruments/common.json +0 -46
  89. pyreduce/instruments/crires_plus.json +0 -63
  90. pyreduce/instruments/harpn.json +0 -136
  91. pyreduce/instruments/harps.json +0 -155
  92. pyreduce/instruments/instrument_schema.json +0 -318
  93. pyreduce/instruments/jwst_miri.json +0 -53
  94. pyreduce/instruments/jwst_niriss.json +0 -52
  95. pyreduce/instruments/lick_apf.json +0 -53
  96. pyreduce/instruments/mcdonald.json +0 -59
  97. pyreduce/instruments/metis_ifu.json +0 -63
  98. pyreduce/instruments/metis_lss.json +0 -65
  99. pyreduce/instruments/micado.json +0 -53
  100. pyreduce/instruments/neid.json +0 -51
  101. pyreduce/instruments/nirspec.json +0 -56
  102. pyreduce/instruments/nte.json +0 -47
  103. pyreduce/instruments/uves.json +0 -59
  104. pyreduce/instruments/xshooter.json +0 -66
  105. pyreduce_astro-0.6.0.dist-info/METADATA +0 -114
  106. {pyreduce_astro-0.6.0.dist-info → pyreduce_astro-0.7a2.dist-info}/WHEEL +0 -0
  107. {pyreduce_astro-0.6.0.dist-info → pyreduce_astro-0.7a2.dist-info}/licenses/LICENSE +0 -0
pyreduce/__main__.py CHANGED
@@ -1,106 +1,206 @@
1
1
  """
2
- This is used when pyreduce is used as a script
2
+ PyReduce command-line interface.
3
+
4
+ Usage:
5
+ uv run reduce --help
6
+ uv run reduce run UVES HD132205 --night 2010-04-01
7
+ uv run reduce run UVES HD132205 --steps bias,flat,orders
8
+ uv run reduce bias UVES HD132205
9
+ uv run reduce combine --output combined.fits *.final.fits
3
10
  """
4
11
 
5
- import argparse
6
- import sys
12
+ import click
7
13
 
8
- from .reduce import main
14
+ from . import datasets
15
+ from .configuration import get_configuration_for_instrument
16
+ from .reduce import main as reduce_main
9
17
  from .tools.combine import combine as tools_combine
10
18
 
11
- scripts = ["reduce", "combine"]
12
-
13
-
14
- def help():
15
- parser = argparse.ArgumentParser(
16
- description="PyReduce script tools interface", prog="python -m pyreduce"
17
- )
18
- parser.add_argument("script", help="which script to execute", choices=scripts)
19
- parser.print_help()
20
-
21
-
22
- def reduce():
23
- parser = argparse.ArgumentParser(
24
- description="PyReduce script tools interface", prog="python -m pyreduce"
25
- )
26
- parser.add_argument("script", help="which script to execute", choices=["reduce"])
27
-
28
- parser.add_argument("-b", "--bias", action="store_true", help="Create master bias")
29
- parser.add_argument("-f", "--flat", action="store_true", help="Create master flat")
30
- parser.add_argument("-o", "--orders", action="store_true", help="Trace orders")
31
- parser.add_argument("-n", "--norm_flat", action="store_true", help="Normalize flat")
32
- parser.add_argument(
33
- "-w", "--wavecal", action="store_true", help="Prepare wavelength calibration"
34
- )
35
- parser.add_argument(
36
- "-s", "--science", action="store_true", help="Extract science spectrum"
37
- )
38
- parser.add_argument(
39
- "-c", "--continuum", action="store_true", help="Normalize continuum"
40
- )
41
-
42
- parser.add_argument("instrument", type=str, help="instrument used")
43
- parser.add_argument("target", type=str, help="target star")
44
-
45
- args = parser.parse_args()
46
- instrument = args.instrument.upper()
47
- target = args.target.upper()
48
-
49
- steps_to_take = {
50
- "bias": args.bias,
51
- "flat": args.flat,
52
- "orders": args.orders,
53
- "norm_flat": args.norm_flat,
54
- "wavecal": args.wavecal,
55
- "science": args.science,
56
- "continuum": args.continuum,
57
- }
58
- steps_to_take = [k for k, v in steps_to_take.items() if v]
59
-
60
- # if no steps are specified use all
61
- if len(steps_to_take) == 0:
62
- steps_to_take = "all"
63
-
64
- main(instrument=instrument, target=target, steps=steps_to_take)
65
-
66
-
67
- def combine():
68
- parser = argparse.ArgumentParser(
69
- description="PyReduce script tools interface", prog="python -m pyreduce"
70
- )
71
- parser.add_argument("script", help="which script to execute", choices=["combine"])
72
-
73
- parser.add_argument(
74
- "--output", help="destination of the combined file", default="./combined.ech"
75
- )
76
- parser.add_argument(
77
- "--plot", type=int, help="plot the results of the desired order"
78
- )
79
-
80
- parser.add_argument(
81
- "input", nargs="+", help="input files to use", default="./*.final.ech"
19
+ ALL_STEPS = (
20
+ "bias",
21
+ "flat",
22
+ "orders",
23
+ "curvature",
24
+ "scatter",
25
+ "norm_flat",
26
+ "wavecal_master",
27
+ "wavecal_init",
28
+ "wavecal",
29
+ "freq_comb_master",
30
+ "freq_comb",
31
+ "science",
32
+ "continuum",
33
+ "finalize",
34
+ )
35
+
36
+
37
+ @click.group()
38
+ @click.version_option(package_name="pyreduce-astro")
39
+ def cli():
40
+ """PyReduce - Echelle spectrograph data reduction pipeline."""
41
+ pass
42
+
43
+
44
+ @cli.command()
45
+ @click.argument("instrument")
46
+ @click.argument("target")
47
+ @click.option("--night", "-n", default=None, help="Observation night (YYYY-MM-DD)")
48
+ @click.option("--arm", "-a", default=None, help="Instrument arm/detector")
49
+ @click.option(
50
+ "--steps",
51
+ "-s",
52
+ default=None,
53
+ help="Comma-separated steps to run (default: all)",
54
+ )
55
+ @click.option(
56
+ "--base-dir",
57
+ "-b",
58
+ default=None,
59
+ help="Base directory for data (default: $REDUCE_DATA or ~/REDUCE_DATA)",
60
+ )
61
+ @click.option(
62
+ "--input-dir", "-i", default="raw", help="Input directory relative to base"
63
+ )
64
+ @click.option(
65
+ "--output-dir", "-o", default="reduced", help="Output directory relative to base"
66
+ )
67
+ @click.option(
68
+ "--plot", "-p", default=0, help="Plot level (0=none, 1=save, 2=interactive)"
69
+ )
70
+ @click.option(
71
+ "--order-range",
72
+ default=None,
73
+ help="Order range to process (e.g., '1,21')",
74
+ )
75
+ def run(
76
+ instrument,
77
+ target,
78
+ night,
79
+ arm,
80
+ steps,
81
+ base_dir,
82
+ input_dir,
83
+ output_dir,
84
+ plot,
85
+ order_range,
86
+ ):
87
+ """Run the reduction pipeline.
88
+
89
+ INSTRUMENT: Name of the instrument (e.g., UVES, HARPS, XSHOOTER)
90
+ TARGET: Target star name or regex pattern
91
+ """
92
+ # Parse steps
93
+ if steps:
94
+ steps = tuple(s.strip() for s in steps.split(","))
95
+ else:
96
+ steps = "all"
97
+
98
+ # Parse order range
99
+ if order_range:
100
+ parts = order_range.split(",")
101
+ order_range = (int(parts[0]), int(parts[1]))
102
+
103
+ # Load configuration
104
+ config = get_configuration_for_instrument(instrument)
105
+
106
+ # Run reduction
107
+ reduce_main(
108
+ instrument=instrument,
109
+ target=target,
110
+ night=night,
111
+ arms=arm,
112
+ steps=steps,
113
+ base_dir=base_dir or "",
114
+ input_dir=input_dir,
115
+ output_dir=output_dir,
116
+ configuration=config,
117
+ order_range=order_range,
118
+ plot=plot,
82
119
  )
83
120
 
84
- args = parser.parse_args()
85
-
86
- files = args.input
87
- output = args.output
88
- plot = args.plot
89
121
 
90
- tools_combine(files, output, plot=plot)
122
+ @cli.command()
123
+ @click.argument("files", nargs=-1, required=True)
124
+ @click.option("--output", "-o", default="combined.fits", help="Output filename")
125
+ @click.option("--plot", "-p", default=None, type=int, help="Plot specific order")
126
+ def combine(files, output, plot):
127
+ """Combine multiple reduced spectra.
128
+
129
+ FILES: Input .final.fits files to combine
130
+ """
131
+ tools_combine(list(files), output, plot=plot)
132
+
133
+
134
+ @cli.command()
135
+ @click.argument("instrument")
136
+ def download(instrument):
137
+ """Download sample dataset for an instrument.
138
+
139
+ INSTRUMENT: Name of the instrument (e.g., UVES, HARPS)
140
+ """
141
+ instrument = instrument.upper()
142
+ dataset_func = getattr(datasets, instrument, None)
143
+ if dataset_func is None:
144
+ available = [
145
+ name
146
+ for name in dir(datasets)
147
+ if name.isupper() and not name.startswith("_")
148
+ ]
149
+ raise click.ClickException(
150
+ f"Unknown instrument '{instrument}'. Available: {', '.join(available)}"
151
+ )
152
+ path = dataset_func()
153
+ click.echo(f"Dataset downloaded to: {path}")
154
+
155
+
156
+ @cli.command("list-steps")
157
+ def list_steps():
158
+ """List all available reduction steps."""
159
+ click.echo("Available reduction steps:")
160
+ for step in ALL_STEPS:
161
+ click.echo(f" - {step}")
162
+
163
+
164
+ def make_step_command(step_name):
165
+ """Factory to create a command for a single step."""
166
+
167
+ @click.command(name=step_name)
168
+ @click.argument("instrument")
169
+ @click.argument("target")
170
+ @click.option("--night", "-n", default=None, help="Observation night")
171
+ @click.option("--arm", "-a", default=None, help="Instrument arm")
172
+ @click.option("--base-dir", "-b", default=None, help="Base directory")
173
+ @click.option("--input-dir", "-i", default="raw", help="Input directory")
174
+ @click.option("--output-dir", "-o", default="reduced", help="Output directory")
175
+ @click.option("--plot", "-p", default=0, help="Plot level")
176
+ def cmd(instrument, target, night, arm, base_dir, input_dir, output_dir, plot):
177
+ config = get_configuration_for_instrument(instrument)
178
+ reduce_main(
179
+ instrument=instrument,
180
+ target=target,
181
+ night=night,
182
+ arms=arm,
183
+ steps=(step_name,),
184
+ base_dir=base_dir or "",
185
+ input_dir=input_dir,
186
+ output_dir=output_dir,
187
+ configuration=config,
188
+ plot=plot,
189
+ )
190
+
191
+ cmd.__doc__ = f"Run the '{step_name}' step."
192
+ return cmd
193
+
194
+
195
+ # Register individual step commands
196
+ for _step in ALL_STEPS:
197
+ cli.add_command(make_step_command(_step))
198
+
199
+
200
+ def main():
201
+ """Entry point for the CLI."""
202
+ cli()
91
203
 
92
204
 
93
205
  if __name__ == "__main__":
94
- # Determine which script to run
95
- if len(sys.argv) == 1:
96
- script = "help"
97
- else:
98
- script = sys.argv[1]
99
-
100
- # Run the chosen script
101
- if script == "reduce":
102
- reduce()
103
- elif script == "combine":
104
- combine()
105
- else:
106
- help()
206
+ main()
pyreduce/cli.py ADDED
@@ -0,0 +1,342 @@
1
+ """Click-based CLI for PyReduce.
2
+
3
+ Usage:
4
+ uv run reduce --help
5
+ uv run reduce bias INSTRUMENT --files bias/*.fits --output output/
6
+ uv run reduce run reduction.yaml
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from glob import glob
12
+ from pathlib import Path
13
+
14
+ import click
15
+
16
+ from . import datasets
17
+ from .instruments.instrument_info import load_instrument
18
+ from .pipeline import Pipeline
19
+
20
+ # Map CLI names to dataset functions
21
+ AVAILABLE_DATASETS = {
22
+ "UVES": datasets.UVES,
23
+ "HARPS": datasets.HARPS,
24
+ "XSHOOTER": datasets.XSHOOTER,
25
+ "NIRSPEC": datasets.KECK_NIRSPEC,
26
+ "JWST_NIRISS": datasets.JWST_NIRISS,
27
+ "JWST_MIRI": datasets.JWST_MIRI,
28
+ "LICK_APF": datasets.LICK_APF,
29
+ "MCDONALD": datasets.MCDONALD,
30
+ }
31
+
32
+
33
+ @click.group()
34
+ @click.version_option(package_name="pyreduce-astro")
35
+ def cli():
36
+ """PyReduce echelle spectrograph reduction pipeline."""
37
+ pass
38
+
39
+
40
+ @cli.command()
41
+ @click.argument("instrument")
42
+ @click.option(
43
+ "--output",
44
+ "-o",
45
+ default=None,
46
+ help="Output directory (default: $REDUCE_DATA or ~/REDUCE_DATA)",
47
+ )
48
+ def download(instrument: str, output: str | None):
49
+ """Download example dataset for an instrument.
50
+
51
+ Available instruments: UVES, HARPS, XSHOOTER, NIRSPEC, JWST_NIRISS, JWST_MIRI,
52
+ LICK_APF, MCDONALD
53
+
54
+ Data is saved to $REDUCE_DATA if set, otherwise ~/REDUCE_DATA.
55
+
56
+ \b
57
+ Examples:
58
+ uv run reduce download UVES
59
+ uv run reduce download UVES -o ~/data/
60
+ """
61
+ instrument_upper = instrument.upper()
62
+ if instrument_upper not in AVAILABLE_DATASETS:
63
+ available = ", ".join(sorted(AVAILABLE_DATASETS.keys()))
64
+ raise click.ClickException(
65
+ f"Unknown instrument '{instrument}'. Available: {available}"
66
+ )
67
+
68
+ click.echo(f"Downloading {instrument_upper} example dataset...")
69
+ data_dir = AVAILABLE_DATASETS[instrument_upper](output)
70
+ click.echo(f"Dataset saved to: {data_dir}")
71
+
72
+
73
+ @cli.command("list-datasets")
74
+ def list_datasets():
75
+ """List available example datasets."""
76
+ click.echo("Available example datasets:")
77
+ click.echo()
78
+ for name in sorted(AVAILABLE_DATASETS.keys()):
79
+ click.echo(f" {name}")
80
+ click.echo()
81
+ click.echo("Download with: uv run reduce download <INSTRUMENT> -o <DIR>")
82
+
83
+
84
+ @cli.command()
85
+ @click.argument("instrument")
86
+ @click.option(
87
+ "--files",
88
+ "-f",
89
+ multiple=True,
90
+ help="Input FITS files (can use glob patterns)",
91
+ )
92
+ @click.option("--output", "-o", default=".", help="Output directory")
93
+ @click.option("--mode", "-m", default="", help="Instrument mode (e.g., RED, BLUE)")
94
+ @click.option(
95
+ "--plot", "-p", default=0, type=int, help="Plot level (0=off, 1=basic, 2=detailed)"
96
+ )
97
+ def bias(instrument: str, files: tuple[str, ...], output: str, mode: str, plot: int):
98
+ """Create master bias from bias frames."""
99
+ inst = load_instrument(instrument)
100
+ file_list = _expand_globs(files)
101
+
102
+ if not file_list:
103
+ raise click.ClickException("No input files specified. Use --files option.")
104
+
105
+ click.echo(f"Creating master bias from {len(file_list)} files...")
106
+ Pipeline(inst, output, mode=mode, plot=plot).bias(file_list).run()
107
+ click.echo(f"Master bias saved to {output}")
108
+
109
+
110
+ @cli.command()
111
+ @click.argument("instrument")
112
+ @click.option(
113
+ "--files",
114
+ "-f",
115
+ multiple=True,
116
+ help="Input FITS files (can use glob patterns)",
117
+ )
118
+ @click.option("--output", "-o", default=".", help="Output directory")
119
+ @click.option("--mode", "-m", default="", help="Instrument mode")
120
+ @click.option(
121
+ "--plot", "-p", default=0, type=int, help="Plot level (0=off, 1=basic, 2=detailed)"
122
+ )
123
+ def flat(instrument: str, files: tuple[str, ...], output: str, mode: str, plot: int):
124
+ """Create master flat from flat frames."""
125
+ inst = load_instrument(instrument)
126
+ file_list = _expand_globs(files)
127
+
128
+ if not file_list:
129
+ raise click.ClickException("No input files specified. Use --files option.")
130
+
131
+ click.echo(f"Creating master flat from {len(file_list)} files...")
132
+ Pipeline(inst, output, mode=mode, plot=plot).flat(file_list).run()
133
+ click.echo(f"Master flat saved to {output}")
134
+
135
+
136
+ @cli.command()
137
+ @click.argument("instrument")
138
+ @click.option(
139
+ "--files",
140
+ "-f",
141
+ multiple=True,
142
+ help="Flat files for tracing (optional if flat already exists)",
143
+ )
144
+ @click.option("--output", "-o", default=".", help="Output directory")
145
+ @click.option("--mode", "-m", default="", help="Instrument mode")
146
+ @click.option(
147
+ "--plot", "-p", default=0, type=int, help="Plot level (0=off, 1=basic, 2=detailed)"
148
+ )
149
+ def trace(instrument: str, files: tuple[str, ...], output: str, mode: str, plot: int):
150
+ """Trace echelle orders on flat field."""
151
+ inst = load_instrument(instrument)
152
+ file_list = _expand_globs(files) if files else None
153
+
154
+ click.echo("Tracing echelle orders...")
155
+ pipe = Pipeline(inst, output, mode=mode, plot=plot)
156
+ if file_list:
157
+ pipe = pipe.flat(file_list)
158
+ pipe.trace_orders(file_list).run()
159
+ click.echo(f"Order trace saved to {output}")
160
+
161
+
162
+ @cli.command()
163
+ @click.argument("instrument")
164
+ @click.option(
165
+ "--files",
166
+ "-f",
167
+ multiple=True,
168
+ required=True,
169
+ help="Wavelength calibration files (ThAr, etc.)",
170
+ )
171
+ @click.option("--output", "-o", default=".", help="Output directory")
172
+ @click.option("--mode", "-m", default="", help="Instrument mode")
173
+ @click.option(
174
+ "--plot", "-p", default=0, type=int, help="Plot level (0=off, 1=basic, 2=detailed)"
175
+ )
176
+ def wavecal(instrument: str, files: tuple[str, ...], output: str, mode: str, plot: int):
177
+ """Perform wavelength calibration."""
178
+ inst = load_instrument(instrument)
179
+ file_list = _expand_globs(files)
180
+
181
+ if not file_list:
182
+ raise click.ClickException("No input files specified. Use --files option.")
183
+
184
+ click.echo(f"Running wavelength calibration with {len(file_list)} files...")
185
+ Pipeline(inst, output, mode=mode, plot=plot).wavelength_calibration(file_list).run()
186
+ click.echo(f"Wavelength calibration saved to {output}")
187
+
188
+
189
+ @cli.command()
190
+ @click.argument("instrument")
191
+ @click.option(
192
+ "--files",
193
+ "-f",
194
+ multiple=True,
195
+ required=True,
196
+ help="Science observation files",
197
+ )
198
+ @click.option("--output", "-o", default=".", help="Output directory")
199
+ @click.option("--mode", "-m", default="", help="Instrument mode")
200
+ @click.option("--target", "-t", default="", help="Target name")
201
+ @click.option(
202
+ "--plot", "-p", default=0, type=int, help="Plot level (0=off, 1=basic, 2=detailed)"
203
+ )
204
+ def extract(
205
+ instrument: str,
206
+ files: tuple[str, ...],
207
+ output: str,
208
+ mode: str,
209
+ target: str,
210
+ plot: int,
211
+ ):
212
+ """Extract spectra from science frames."""
213
+ inst = load_instrument(instrument)
214
+ file_list = _expand_globs(files)
215
+
216
+ if not file_list:
217
+ raise click.ClickException("No input files specified. Use --files option.")
218
+
219
+ click.echo(f"Extracting spectra from {len(file_list)} files...")
220
+ Pipeline(inst, output, mode=mode, target=target, plot=plot).extract(file_list).run()
221
+ click.echo(f"Extracted spectra saved to {output}")
222
+
223
+
224
+ @cli.command()
225
+ @click.argument("config_file", type=click.Path(exists=True))
226
+ @click.option(
227
+ "--steps",
228
+ "-s",
229
+ default="all",
230
+ help="Steps to run (comma-separated, or 'all')",
231
+ )
232
+ @click.option("--skip-existing", is_flag=True, help="Skip steps with existing output")
233
+ @click.option(
234
+ "--plot", "-p", default=0, type=int, help="Plot level (0=off, 1=basic, 2=detailed)"
235
+ )
236
+ def run(config_file: str, steps: str, skip_existing: bool, plot: int):
237
+ """Run full reduction pipeline from config file.
238
+
239
+ CONFIG_FILE should be a YAML file with instrument, files, and output settings.
240
+
241
+ Example config.yaml:
242
+
243
+ \b
244
+ instrument: UVES
245
+ output: /data/reduced/
246
+ mode: RED
247
+ files:
248
+ bias: /data/raw/bias/*.fits
249
+ flat: /data/raw/flat/*.fits
250
+ wavecal: /data/raw/thar/*.fits
251
+ science: /data/raw/science/*.fits
252
+ steps: [bias, flat, trace, wavecal, extract]
253
+ """
254
+ import yaml
255
+
256
+ with open(config_file) as f:
257
+ config = yaml.safe_load(f)
258
+
259
+ instrument_name = config.get("instrument")
260
+ if not instrument_name:
261
+ raise click.ClickException("Config file must specify 'instrument'")
262
+
263
+ inst = load_instrument(instrument_name)
264
+ output = config.get("output", ".")
265
+ mode = config.get("mode", "")
266
+ target = config.get("target", "")
267
+ files = config.get("files", {})
268
+ config_steps = config.get("steps", [])
269
+
270
+ # Parse steps
271
+ if steps != "all":
272
+ config_steps = [s.strip() for s in steps.split(",")]
273
+ elif not config_steps:
274
+ config_steps = ["bias", "flat", "trace", "wavecal", "extract"]
275
+
276
+ click.echo(f"Running pipeline for {instrument_name}")
277
+ click.echo(f"Steps: {', '.join(config_steps)}")
278
+ click.echo(f"Output: {output}")
279
+
280
+ pipe = Pipeline(inst, output, mode=mode, target=target, plot=plot)
281
+
282
+ # Add steps based on config
283
+ if "bias" in config_steps and files.get("bias"):
284
+ pipe = pipe.bias(_expand_globs(files["bias"]))
285
+
286
+ if "flat" in config_steps and files.get("flat"):
287
+ pipe = pipe.flat(_expand_globs(files["flat"]))
288
+
289
+ if "trace" in config_steps:
290
+ trace_files = files.get("orders") or files.get("flat")
291
+ pipe = pipe.trace_orders(_expand_globs(trace_files) if trace_files else None)
292
+
293
+ if "scatter" in config_steps:
294
+ pipe = pipe.scatter()
295
+
296
+ if "norm_flat" in config_steps:
297
+ pipe = pipe.normalize_flat()
298
+
299
+ if "wavecal" in config_steps and files.get("wavecal"):
300
+ pipe = pipe.wavelength_calibration(_expand_globs(files["wavecal"]))
301
+
302
+ if "curvature" in config_steps:
303
+ curv_files = files.get("curvature") or files.get("wavecal")
304
+ pipe = pipe.curvature(_expand_globs(curv_files) if curv_files else None)
305
+
306
+ if "extract" in config_steps and files.get("science"):
307
+ pipe = pipe.extract(_expand_globs(files["science"]))
308
+
309
+ if "continuum" in config_steps:
310
+ pipe = pipe.continuum()
311
+
312
+ if "finalize" in config_steps:
313
+ pipe = pipe.finalize()
314
+
315
+ pipe.run(skip_existing=skip_existing)
316
+ click.echo("Pipeline complete!")
317
+
318
+
319
+ def _expand_globs(patterns) -> list[str]:
320
+ """Expand glob patterns to file list."""
321
+ if isinstance(patterns, str):
322
+ patterns = [patterns]
323
+
324
+ files = []
325
+ for pattern in patterns:
326
+ expanded = glob(pattern)
327
+ if expanded:
328
+ files.extend(expanded)
329
+ else:
330
+ # If no glob match, treat as literal path
331
+ if Path(pattern).exists():
332
+ files.append(pattern)
333
+ return sorted(set(files))
334
+
335
+
336
+ def main():
337
+ """Entry point for the CLI."""
338
+ cli()
339
+
340
+
341
+ if __name__ == "__main__":
342
+ main()
Binary file
Binary file