pyreduce-astro 0.7a5__cp314-cp314-win_amd64.whl → 0.7a6__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/__main__.py +82 -18
- pyreduce/cli.py +1 -1
- pyreduce/clib/Release/_slitfunc_2d.obj +0 -0
- pyreduce/clib/Release/_slitfunc_bd.obj +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.cp313-win_amd64.pyd +0 -0
- pyreduce/clib/_slitfunc_bd.cp314-win_amd64.pyd +0 -0
- pyreduce/combine_frames.py +8 -0
- pyreduce/instruments/common.py +201 -15
- pyreduce/instruments/harpn.py +2 -2
- pyreduce/instruments/harps.py +2 -2
- pyreduce/instruments/models.py +24 -0
- pyreduce/instruments/neid.py +49 -77
- pyreduce/instruments/neid.yaml +39 -40
- pyreduce/instruments/nirspec.py +2 -2
- pyreduce/pipeline.py +6 -6
- pyreduce/reduce.py +5 -5
- pyreduce/settings/settings_AJ.json +1 -1
- pyreduce/settings/settings_ANDES.json +1 -1
- pyreduce/settings/settings_CRIRES_PLUS.json +1 -1
- pyreduce/settings/settings_HARPN.json +1 -1
- pyreduce/settings/settings_HARPS.json +1 -1
- pyreduce/settings/settings_JWST_MIRI.json +1 -1
- pyreduce/settings/settings_JWST_NIRISS.json +1 -1
- pyreduce/settings/settings_LICK_APF.json +1 -1
- pyreduce/settings/settings_MCDONALD.json +1 -1
- pyreduce/settings/settings_METIS_IFU.json +1 -1
- pyreduce/settings/settings_METIS_LSS.json +1 -1
- pyreduce/settings/settings_MICADO.json +1 -1
- pyreduce/settings/settings_NEID.json +9 -18
- pyreduce/settings/settings_NIRSPEC.json +1 -1
- pyreduce/settings/settings_NTE.json +1 -1
- pyreduce/settings/settings_UVES.json +1 -1
- pyreduce/settings/settings_XSHOOTER.json +1 -1
- pyreduce/settings/settings_pyreduce.json +1 -1
- pyreduce/settings/settings_schema.json +3 -3
- {pyreduce_astro-0.7a5.dist-info → pyreduce_astro-0.7a6.dist-info}/METADATA +2 -2
- {pyreduce_astro-0.7a5.dist-info → pyreduce_astro-0.7a6.dist-info}/RECORD +42 -42
- {pyreduce_astro-0.7a5.dist-info → pyreduce_astro-0.7a6.dist-info}/WHEEL +0 -0
- {pyreduce_astro-0.7a5.dist-info → pyreduce_astro-0.7a6.dist-info}/entry_points.txt +0 -0
- {pyreduce_astro-0.7a5.dist-info → pyreduce_astro-0.7a6.dist-info}/licenses/LICENSE +0 -0
pyreduce/__main__.py
CHANGED
|
@@ -4,8 +4,8 @@ PyReduce command-line interface.
|
|
|
4
4
|
Usage:
|
|
5
5
|
uv run reduce --help
|
|
6
6
|
uv run reduce run UVES HD132205 --night 2010-04-01
|
|
7
|
-
uv run reduce run UVES HD132205 --steps bias,flat,
|
|
8
|
-
uv run reduce
|
|
7
|
+
uv run reduce run UVES HD132205 --steps bias,flat,trace
|
|
8
|
+
uv run reduce trace UVES HD132205
|
|
9
9
|
uv run reduce combine --output combined.fits *.final.fits
|
|
10
10
|
"""
|
|
11
11
|
|
|
@@ -14,7 +14,7 @@ import click
|
|
|
14
14
|
ALL_STEPS = (
|
|
15
15
|
"bias",
|
|
16
16
|
"flat",
|
|
17
|
-
"
|
|
17
|
+
"trace",
|
|
18
18
|
"curvature",
|
|
19
19
|
"scatter",
|
|
20
20
|
"norm_flat",
|
|
@@ -279,30 +279,94 @@ def make_step_command(step_name):
|
|
|
279
279
|
|
|
280
280
|
@click.command(name=step_name)
|
|
281
281
|
@click.argument("instrument")
|
|
282
|
-
@click.argument("target")
|
|
282
|
+
@click.argument("target", required=False, default="")
|
|
283
283
|
@click.option("--night", "-n", default=None, help="Observation night")
|
|
284
284
|
@click.option("--channel", "-c", default=None, help="Instrument channel")
|
|
285
285
|
@click.option("--base-dir", "-b", default=None, help="Base directory")
|
|
286
286
|
@click.option("--input-dir", "-i", default="raw", help="Input directory")
|
|
287
287
|
@click.option("--output-dir", "-o", default="reduced", help="Output directory")
|
|
288
288
|
@click.option("--plot", "-p", default=0, help="Plot level")
|
|
289
|
-
|
|
289
|
+
@click.option(
|
|
290
|
+
"--file",
|
|
291
|
+
"-f",
|
|
292
|
+
default=None,
|
|
293
|
+
help="Specific input file (bypasses file discovery)",
|
|
294
|
+
)
|
|
295
|
+
def cmd(
|
|
296
|
+
instrument, target, night, channel, base_dir, input_dir, output_dir, plot, file
|
|
297
|
+
):
|
|
290
298
|
from .configuration import get_configuration_for_instrument
|
|
291
299
|
from .reduce import main as reduce_main
|
|
292
300
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
301
|
+
if file:
|
|
302
|
+
# Direct file mode: run step on specific file
|
|
303
|
+
import os
|
|
304
|
+
|
|
305
|
+
import numpy as np
|
|
306
|
+
|
|
307
|
+
from . import reduce as reduce_module
|
|
308
|
+
from .configuration import get_configuration_for_instrument
|
|
309
|
+
from .instruments.instrument_info import load_instrument
|
|
310
|
+
|
|
311
|
+
inst = load_instrument(instrument)
|
|
312
|
+
channel = channel or (inst.channels[0] if inst.channels else "")
|
|
313
|
+
output_dir_full = output_dir
|
|
314
|
+
if base_dir:
|
|
315
|
+
output_dir_full = os.path.join(base_dir, output_dir)
|
|
316
|
+
os.makedirs(output_dir_full, exist_ok=True)
|
|
317
|
+
|
|
318
|
+
# Load configuration for this step
|
|
319
|
+
config = get_configuration_for_instrument(instrument)
|
|
320
|
+
step_config = config.get(step_name, {})
|
|
321
|
+
step_config["plot"] = plot
|
|
322
|
+
|
|
323
|
+
# Get the step class
|
|
324
|
+
step_classes = {
|
|
325
|
+
"bias": reduce_module.Bias,
|
|
326
|
+
"flat": reduce_module.Flat,
|
|
327
|
+
"trace": reduce_module.OrderTracing,
|
|
328
|
+
"curvature": reduce_module.SlitCurvatureDetermination,
|
|
329
|
+
"scatter": reduce_module.BackgroundScatter,
|
|
330
|
+
"norm_flat": reduce_module.NormalizeFlatField,
|
|
331
|
+
"wavecal_master": reduce_module.WavelengthCalibrationMaster,
|
|
332
|
+
"wavecal_init": reduce_module.WavelengthCalibrationInitialize,
|
|
333
|
+
"wavecal": reduce_module.WavelengthCalibrationFinalize,
|
|
334
|
+
"freq_comb_master": reduce_module.LaserFrequencyCombMaster,
|
|
335
|
+
"freq_comb": reduce_module.LaserFrequencyCombFinalize,
|
|
336
|
+
"science": reduce_module.ScienceExtraction,
|
|
337
|
+
"continuum": reduce_module.ContinuumNormalization,
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if step_name not in step_classes:
|
|
341
|
+
raise click.ClickException(
|
|
342
|
+
f"Step '{step_name}' does not support --file option"
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
step_class = step_classes[step_name]
|
|
346
|
+
step = step_class(
|
|
347
|
+
inst,
|
|
348
|
+
channel,
|
|
349
|
+
target=target or "",
|
|
350
|
+
night=night,
|
|
351
|
+
output_dir=output_dir_full,
|
|
352
|
+
order_range=None,
|
|
353
|
+
**step_config,
|
|
354
|
+
)
|
|
355
|
+
step.run(files=np.array([file]), mask=None, bias=None)
|
|
356
|
+
else:
|
|
357
|
+
config = get_configuration_for_instrument(instrument)
|
|
358
|
+
reduce_main(
|
|
359
|
+
instrument=instrument,
|
|
360
|
+
target=target,
|
|
361
|
+
night=night,
|
|
362
|
+
channels=channel,
|
|
363
|
+
steps=(step_name,),
|
|
364
|
+
base_dir=base_dir or "",
|
|
365
|
+
input_dir=input_dir,
|
|
366
|
+
output_dir=output_dir,
|
|
367
|
+
configuration=config,
|
|
368
|
+
plot=plot,
|
|
369
|
+
)
|
|
306
370
|
|
|
307
371
|
cmd.__doc__ = f"Run the '{step_name}' step."
|
|
308
372
|
return cmd
|
pyreduce/cli.py
CHANGED
|
@@ -287,7 +287,7 @@ def run(config_file: str, steps: str, skip_existing: bool, plot: int):
|
|
|
287
287
|
pipe = pipe.flat(_expand_globs(files["flat"]))
|
|
288
288
|
|
|
289
289
|
if "trace" in config_steps:
|
|
290
|
-
trace_files = files.get("
|
|
290
|
+
trace_files = files.get("trace") or files.get("flat")
|
|
291
291
|
pipe = pipe.trace_orders(_expand_globs(trace_files) if trace_files else None)
|
|
292
292
|
|
|
293
293
|
if "scatter" in config_steps:
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
pyreduce/combine_frames.py
CHANGED
|
@@ -305,6 +305,14 @@ def combine_frames(
|
|
|
305
305
|
if instrument is None or isinstance(instrument, str):
|
|
306
306
|
instrument = load_instrument(instrument)
|
|
307
307
|
|
|
308
|
+
# For multi-amplifier instruments, use simple combination since the
|
|
309
|
+
# row-by-row approach doesn't work with multi-extension assembly
|
|
310
|
+
if instrument.config.amplifiers is not None:
|
|
311
|
+
logger.debug("Multi-amplifier instrument detected, using simple combination")
|
|
312
|
+
return combine_frames_simple(
|
|
313
|
+
files, instrument, channel, extension=extension, dtype=dtype, **kwargs
|
|
314
|
+
)
|
|
315
|
+
|
|
308
316
|
# summarize file info
|
|
309
317
|
logger.debug("Files:")
|
|
310
318
|
for i, fname in zip(range(len(files)), files, strict=False):
|
pyreduce/instruments/common.py
CHANGED
|
@@ -24,6 +24,29 @@ from .models import InstrumentConfig
|
|
|
24
24
|
logger = logging.getLogger(__name__)
|
|
25
25
|
|
|
26
26
|
|
|
27
|
+
def parse_iraf_section(section_str):
|
|
28
|
+
"""Parse IRAF-style section string into pixel coordinates.
|
|
29
|
+
|
|
30
|
+
IRAF format: [x1:x2,y1:y2] where coordinates are 1-based inclusive.
|
|
31
|
+
|
|
32
|
+
Parameters
|
|
33
|
+
----------
|
|
34
|
+
section_str : str
|
|
35
|
+
Section string like "[28:1179,1:4616]"
|
|
36
|
+
|
|
37
|
+
Returns
|
|
38
|
+
-------
|
|
39
|
+
x1, x2, y1, y2 : int
|
|
40
|
+
1-based inclusive pixel coordinates
|
|
41
|
+
"""
|
|
42
|
+
# Remove brackets and split
|
|
43
|
+
s = section_str.strip("[]")
|
|
44
|
+
x_part, y_part = s.split(",")
|
|
45
|
+
x1, x2 = map(int, x_part.split(":"))
|
|
46
|
+
y1, y2 = map(int, y_part.split(":"))
|
|
47
|
+
return x1, x2, y1, y2
|
|
48
|
+
|
|
49
|
+
|
|
27
50
|
def find_first_index(arr, value):
|
|
28
51
|
"""find the first element equal to value in the array arr"""
|
|
29
52
|
try:
|
|
@@ -121,7 +144,7 @@ class Instrument:
|
|
|
121
144
|
"target": ObjectFilter(self.config.target, regex=True),
|
|
122
145
|
"bias": Filter(self.config.kw_bias),
|
|
123
146
|
"flat": Filter(self.config.kw_flat),
|
|
124
|
-
"
|
|
147
|
+
"trace": Filter(self.config.kw_orders),
|
|
125
148
|
"curvature": Filter(self.config.kw_curvature),
|
|
126
149
|
"scatter": Filter(self.config.kw_scatter),
|
|
127
150
|
"wave": Filter(self.config.kw_wave),
|
|
@@ -142,7 +165,7 @@ class Instrument:
|
|
|
142
165
|
"flat",
|
|
143
166
|
"wavecal_master",
|
|
144
167
|
"freq_comb_master",
|
|
145
|
-
"
|
|
168
|
+
"trace",
|
|
146
169
|
"scatter",
|
|
147
170
|
"curvature",
|
|
148
171
|
]
|
|
@@ -224,6 +247,147 @@ class Instrument:
|
|
|
224
247
|
|
|
225
248
|
return config, info
|
|
226
249
|
|
|
250
|
+
def get_amplifier_extensions(self, header):
|
|
251
|
+
"""Get list of amplifier extensions if multi-amp mode is configured.
|
|
252
|
+
|
|
253
|
+
Parameters
|
|
254
|
+
----------
|
|
255
|
+
header : fits.Header
|
|
256
|
+
Primary FITS header
|
|
257
|
+
|
|
258
|
+
Returns
|
|
259
|
+
-------
|
|
260
|
+
list or None
|
|
261
|
+
List of extension names/indices if multi-amp, None otherwise
|
|
262
|
+
"""
|
|
263
|
+
if self.config.amplifiers is None:
|
|
264
|
+
return None
|
|
265
|
+
|
|
266
|
+
amp_config = self.config.amplifiers
|
|
267
|
+
# Get number of amplifiers
|
|
268
|
+
if isinstance(amp_config.count, int):
|
|
269
|
+
n_amps = amp_config.count
|
|
270
|
+
else:
|
|
271
|
+
n_amps = header.get(amp_config.count)
|
|
272
|
+
if n_amps is None:
|
|
273
|
+
logger.warning(
|
|
274
|
+
"Amplifier count keyword '%s' not found in header",
|
|
275
|
+
amp_config.count,
|
|
276
|
+
)
|
|
277
|
+
return None
|
|
278
|
+
|
|
279
|
+
# Generate extension names from template
|
|
280
|
+
extensions = []
|
|
281
|
+
for n in range(1, n_amps + 1):
|
|
282
|
+
ext_name = amp_config.extension_template.format(n=n)
|
|
283
|
+
extensions.append(ext_name)
|
|
284
|
+
|
|
285
|
+
return extensions
|
|
286
|
+
|
|
287
|
+
def assemble_amplifiers(self, hdu, amp_extensions, channel):
|
|
288
|
+
"""Assemble multi-amplifier readout into single frame.
|
|
289
|
+
|
|
290
|
+
Reads data from multiple FITS extensions, each representing one
|
|
291
|
+
amplifier's readout region, and assembles them into a single frame
|
|
292
|
+
using DATASEC/DETSEC mappings.
|
|
293
|
+
|
|
294
|
+
Parameters
|
|
295
|
+
----------
|
|
296
|
+
hdu : HDUList
|
|
297
|
+
Open FITS file
|
|
298
|
+
amp_extensions : list
|
|
299
|
+
List of extension names to read
|
|
300
|
+
channel : str
|
|
301
|
+
Instrument channel
|
|
302
|
+
|
|
303
|
+
Returns
|
|
304
|
+
-------
|
|
305
|
+
data : ndarray
|
|
306
|
+
Assembled frame (float32)
|
|
307
|
+
header : fits.Header
|
|
308
|
+
Combined header with per-amplifier e_gain{n}, e_readn{n}
|
|
309
|
+
"""
|
|
310
|
+
amp_config = self.config.amplifiers
|
|
311
|
+
h_prime = hdu[0].header
|
|
312
|
+
|
|
313
|
+
# First pass: determine output size and collect amp info
|
|
314
|
+
xmax, ymax = 0, 0
|
|
315
|
+
amp_info = []
|
|
316
|
+
|
|
317
|
+
for ext in amp_extensions:
|
|
318
|
+
h = hdu[ext].header
|
|
319
|
+
datasec = parse_iraf_section(h[amp_config.datasec])
|
|
320
|
+
detsec = parse_iraf_section(h[amp_config.detsec])
|
|
321
|
+
|
|
322
|
+
# DETSEC gives the destination in the assembled image
|
|
323
|
+
xmax = max(xmax, detsec[1])
|
|
324
|
+
ymax = max(ymax, detsec[3])
|
|
325
|
+
|
|
326
|
+
amp_info.append(
|
|
327
|
+
{
|
|
328
|
+
"ext": ext,
|
|
329
|
+
"datasec": datasec,
|
|
330
|
+
"detsec": detsec,
|
|
331
|
+
"gain": h.get(amp_config.gain, 1.0),
|
|
332
|
+
"readnoise": h.get(amp_config.readnoise, 0.0),
|
|
333
|
+
}
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
# Allocate output array
|
|
337
|
+
assembled = np.zeros((ymax, xmax), dtype=np.float32)
|
|
338
|
+
|
|
339
|
+
# Second pass: place each amplifier's data
|
|
340
|
+
for info in amp_info:
|
|
341
|
+
# Extract valid data region (DATASEC is 1-based inclusive)
|
|
342
|
+
dx1, dx2, dy1, dy2 = info["datasec"]
|
|
343
|
+
raw = hdu[info["ext"]].data[dy1 - 1 : dy2, dx1 - 1 : dx2]
|
|
344
|
+
|
|
345
|
+
# Place into output at DETSEC location (1-based inclusive)
|
|
346
|
+
ox1, ox2, oy1, oy2 = info["detsec"]
|
|
347
|
+
assembled[oy1 - 1 : oy2, ox1 - 1 : ox2] = raw
|
|
348
|
+
|
|
349
|
+
# Build combined header
|
|
350
|
+
header = hdu[amp_extensions[0]].header.copy()
|
|
351
|
+
header.extend(h_prime, strip=False)
|
|
352
|
+
|
|
353
|
+
# Add standard header info first (sets e_orient, e_transpose, etc.)
|
|
354
|
+
header = self.add_header_info(header, channel)
|
|
355
|
+
header["e_input"] = (
|
|
356
|
+
os.path.basename(hdu.filename()),
|
|
357
|
+
"Original input filename",
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
# Store per-amplifier calibration values
|
|
361
|
+
# Use e_namps (not e_ampl) to avoid triggering clipnflip's multi-amp path
|
|
362
|
+
# since we've already assembled the frame
|
|
363
|
+
header["e_namps"] = (len(amp_info), "Number of amplifiers in raw data")
|
|
364
|
+
for i, info in enumerate(amp_info, 1):
|
|
365
|
+
header[f"HIERARCH e_gain{i}"] = (
|
|
366
|
+
info["gain"],
|
|
367
|
+
f"Gain for amplifier {i}",
|
|
368
|
+
)
|
|
369
|
+
header[f"HIERARCH e_readn{i}"] = (
|
|
370
|
+
info["readnoise"],
|
|
371
|
+
f"Readnoise for amplifier {i}",
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
# Override e_gain/e_readn with median across amplifiers
|
|
375
|
+
gains = [info["gain"] for info in amp_info]
|
|
376
|
+
readnoises = [info["readnoise"] for info in amp_info]
|
|
377
|
+
header["e_gain"] = (float(np.median(gains)), "Median gain across amplifiers")
|
|
378
|
+
header["e_readn"] = (
|
|
379
|
+
float(np.median(readnoises)),
|
|
380
|
+
"Median readnoise across amplifiers",
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
# Override bounds for assembled frame (full frame, no prescan/overscan)
|
|
384
|
+
header["e_xlo"] = 0
|
|
385
|
+
header["e_xhi"] = xmax
|
|
386
|
+
header["e_ylo"] = 0
|
|
387
|
+
header["e_yhi"] = ymax
|
|
388
|
+
|
|
389
|
+
return assembled, header
|
|
390
|
+
|
|
227
391
|
def load_fits(
|
|
228
392
|
self, fname, channel, extension=None, mask=None, header_only=False, dtype=None
|
|
229
393
|
):
|
|
@@ -235,6 +399,9 @@ class Instrument:
|
|
|
235
399
|
data is clipnflipped
|
|
236
400
|
mask is applied
|
|
237
401
|
|
|
402
|
+
For multi-amplifier instruments, data from multiple extensions
|
|
403
|
+
is assembled into a single frame before clipnflip.
|
|
404
|
+
|
|
238
405
|
Parameters
|
|
239
406
|
----------
|
|
240
407
|
fname : str
|
|
@@ -266,20 +433,39 @@ class Instrument:
|
|
|
266
433
|
|
|
267
434
|
hdu = fits.open(fname)
|
|
268
435
|
h_prime = hdu[0].header
|
|
269
|
-
if extension is None:
|
|
270
|
-
extension = self.get_extension(h_prime, channel)
|
|
271
436
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
437
|
+
# Check for multi-amplifier mode
|
|
438
|
+
amp_extensions = self.get_amplifier_extensions(h_prime)
|
|
439
|
+
|
|
440
|
+
if amp_extensions is not None:
|
|
441
|
+
# Multi-amplifier path: assemble from multiple extensions
|
|
442
|
+
if header_only:
|
|
443
|
+
# For header_only, just return first extension header + primary
|
|
444
|
+
header = hdu[amp_extensions[0]].header.copy()
|
|
445
|
+
header.extend(h_prime, strip=False)
|
|
446
|
+
header = self.add_header_info(header, channel)
|
|
447
|
+
header["e_input"] = (os.path.basename(fname), "Original input filename")
|
|
448
|
+
hdu.close()
|
|
449
|
+
return header
|
|
450
|
+
|
|
451
|
+
data, header = self.assemble_amplifiers(hdu, amp_extensions, channel)
|
|
452
|
+
data = clipnflip(data, header)
|
|
453
|
+
else:
|
|
454
|
+
# Single extension path (original behavior)
|
|
455
|
+
if extension is None:
|
|
456
|
+
extension = self.get_extension(h_prime, channel)
|
|
457
|
+
|
|
458
|
+
header = hdu[extension].header
|
|
459
|
+
if extension != 0:
|
|
460
|
+
header.extend(h_prime, strip=False)
|
|
461
|
+
header = self.add_header_info(header, channel)
|
|
462
|
+
header["e_input"] = (os.path.basename(fname), "Original input filename")
|
|
277
463
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
464
|
+
if header_only:
|
|
465
|
+
hdu.close()
|
|
466
|
+
return header
|
|
281
467
|
|
|
282
|
-
|
|
468
|
+
data = clipnflip(hdu[extension].data, header)
|
|
283
469
|
|
|
284
470
|
if dtype is not None:
|
|
285
471
|
data = data.astype(dtype)
|
|
@@ -398,10 +584,10 @@ class Instrument:
|
|
|
398
584
|
"night": night,
|
|
399
585
|
"flat": self.config.id_flat,
|
|
400
586
|
},
|
|
401
|
-
"
|
|
587
|
+
"trace": {
|
|
402
588
|
"instrument": self.config.id_instrument,
|
|
403
589
|
"night": night,
|
|
404
|
-
"
|
|
590
|
+
"trace": self.config.id_orders,
|
|
405
591
|
},
|
|
406
592
|
"scatter": {
|
|
407
593
|
"instrument": self.config.id_instrument,
|
pyreduce/instruments/harpn.py
CHANGED
|
@@ -70,7 +70,7 @@ class HARPN(Instrument):
|
|
|
70
70
|
"flat",
|
|
71
71
|
"wavecal_master",
|
|
72
72
|
"freq_comb_master",
|
|
73
|
-
"
|
|
73
|
+
"trace",
|
|
74
74
|
"scatter",
|
|
75
75
|
]
|
|
76
76
|
|
|
@@ -109,7 +109,7 @@ class HARPN(Instrument):
|
|
|
109
109
|
expectations = {
|
|
110
110
|
"bias": {"instrument": "HARPN", "night": night, "type": r"BIAS,BIAS"},
|
|
111
111
|
"flat": {"instrument": "HARPN", "night": night, "type": r"LAMP,LAMP,TUN"},
|
|
112
|
-
"
|
|
112
|
+
"trace": {
|
|
113
113
|
"instrument": "HARPN",
|
|
114
114
|
"night": night,
|
|
115
115
|
"type": id_orddef,
|
pyreduce/instruments/harps.py
CHANGED
|
@@ -111,7 +111,7 @@ class HARPS(Instrument):
|
|
|
111
111
|
"flat",
|
|
112
112
|
"wavecal_master",
|
|
113
113
|
"freq_comb_master",
|
|
114
|
-
"
|
|
114
|
+
"trace",
|
|
115
115
|
"scatter",
|
|
116
116
|
"curvature",
|
|
117
117
|
]
|
|
@@ -202,7 +202,7 @@ class HARPS(Instrument):
|
|
|
202
202
|
expectations = {
|
|
203
203
|
"bias": {"instrument": "HARPS", "night": night, "type": r"BIAS,BIAS"},
|
|
204
204
|
"flat": {"instrument": "HARPS", "night": night, "type": r"(LAMP,LAMP),.*"},
|
|
205
|
-
"
|
|
205
|
+
"trace": {
|
|
206
206
|
"instrument": "HARPS",
|
|
207
207
|
"night": night,
|
|
208
208
|
"fiber": fiber,
|
pyreduce/instruments/models.py
CHANGED
|
@@ -48,6 +48,27 @@ class FileClassification(BaseModel):
|
|
|
48
48
|
model_config = ConfigDict(extra="allow")
|
|
49
49
|
|
|
50
50
|
|
|
51
|
+
class AmplifiersConfig(BaseModel):
|
|
52
|
+
"""Configuration for multi-amplifier readout assembly.
|
|
53
|
+
|
|
54
|
+
Used when detector data is split across multiple FITS extensions,
|
|
55
|
+
each with its own gain/readnoise and region mapping.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
# Extension naming: template with {n} placeholder, e.g. "AMPLIFIER {n:02d}"
|
|
59
|
+
extension_template: str
|
|
60
|
+
# Number of amplifiers: header keyword or literal int
|
|
61
|
+
count: str | int
|
|
62
|
+
# Header keywords in each extension for calibration values
|
|
63
|
+
gain: str = "GAIN"
|
|
64
|
+
readnoise: str = "RDNOISE"
|
|
65
|
+
# Header keywords for region mapping (IRAF section format)
|
|
66
|
+
datasec: str = "DATASEC"
|
|
67
|
+
detsec: str = "DETSEC"
|
|
68
|
+
|
|
69
|
+
model_config = ConfigDict(extra="forbid")
|
|
70
|
+
|
|
71
|
+
|
|
51
72
|
class InstrumentConfig(BaseModel):
|
|
52
73
|
"""Configuration for an astronomical instrument.
|
|
53
74
|
|
|
@@ -76,6 +97,9 @@ class InstrumentConfig(BaseModel):
|
|
|
76
97
|
orientation: int | list[int] = 0
|
|
77
98
|
transpose: bool = False
|
|
78
99
|
|
|
100
|
+
# Multi-amplifier readout (optional)
|
|
101
|
+
amplifiers: AmplifiersConfig | None = None
|
|
102
|
+
|
|
79
103
|
# Detector dimensions
|
|
80
104
|
naxis_x: str | int = "NAXIS1"
|
|
81
105
|
naxis_y: str | int = "NAXIS2"
|