Skip to content

Commit 4db3619

Browse files
ivhclaude
andcommitted
Update METIS_IFU for dynamic wavelength-based channels
Follow CRIRES_PLUS pattern: ~100 wavelength settings × 4 detectors. Channel format: {wavelength}_{detector}, e.g. 4.555_det1 - Add discover_channels() to find channels from ESO INS WLEN CEN header - Add wavelength filter to shared for calibration file matching - Fix extension pattern to DET{n}.DATA - Update config for actual METIS simulation headers Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 15cbd2f commit 4db3619

3 files changed

Lines changed: 137 additions & 66 deletions

File tree

Lines changed: 96 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,118 @@
11
"""
2-
Handles instrument specific info for the METIS_IFU IFU spectrograph
2+
METIS IFU instrument - ELT/METIS integral field unit spectrograph.
33
4-
Mostly reading data from the header
4+
L/M band IFU with ~100 wavelength settings and 4 detectors.
5+
Channels are {wavelength}_{detector}, e.g. 4.555_det1.
56
"""
67

78
import logging
89
import os.path
10+
import re
11+
from glob import glob
12+
13+
from astropy.io import fits
914

1015
from ..common import Instrument
16+
from ..filters import Filter
1117

1218
logger = logging.getLogger(__name__)
1319

1420

1521
class METIS_IFU(Instrument):
16-
def add_header_info(self, header, channel, **kwargs):
17-
"""read data from header and add it as REDUCE keyword back to the header"""
18-
# "Normal" stuff is handled by the general version, specific changes to values happen here
19-
# alternatively you can implement all of it here, whatever works
20-
header = super().add_header_info(header, channel)
21-
22-
# header["e_backg"] = (
23-
# header["e_readn"] + header["e_exptime"] * header["e_drk"] / 3600
24-
# )
25-
#
26-
# header["e_ra"] /= 15
27-
# if header["e_jd"] is not None:
28-
# header["e_jd"] += header["e_exptime"] / 2 / 3600 / 24 + 0.5
22+
def __init__(self):
23+
super().__init__()
24+
self.filters["wavelength"] = Filter(self.info["id_wavelength"])
25+
self.shared += ["wavelength"]
2926

27+
def add_header_info(self, header, channel, **kwargs):
28+
"""Read data from header and add it as REDUCE keyword back to the header."""
29+
wavelength, detector = self.parse_channel(channel)
30+
header = super().add_header_info(header, wavelength)
31+
self.load_info()
3032
return header
3133

32-
def get_extension(self, header, channel):
33-
extension = 1
34+
def get_supported_channels(self):
35+
"""Return sample channels for testing.
36+
37+
Can't enumerate all ~400 channels; actual channels are discovered
38+
dynamically via discover_channels().
39+
"""
40+
return ["4.555_det1", "4.555_det2", "4.555_det3", "4.555_det4"]
41+
42+
def discover_channels(self, input_dir):
43+
"""Discover available channels from METIS IFU files.
44+
45+
Extracts wavelength setting from headers and combines with detector
46+
numbers from extension names to form channel identifiers.
47+
"""
48+
channels = set()
49+
files = glob(os.path.join(input_dir, "*.fits"))
50+
for f in files:
51+
try:
52+
with fits.open(f) as hdul:
53+
wlen_cen = hdul[0].header.get("ESO INS WLEN CEN")
54+
if wlen_cen is None:
55+
continue
56+
for hdu in hdul[1:]:
57+
name = hdu.name
58+
if name.startswith("DET") and ".DATA" in name:
59+
det_num = name[3] # "DET1.DATA" -> "1"
60+
channels.add(f"{wlen_cen}_det{det_num}")
61+
except Exception:
62+
continue
63+
return sorted(channels) if channels else [None]
64+
65+
def parse_channel(self, channel):
66+
"""Parse channel string into wavelength and detector.
67+
68+
Parameters
69+
----------
70+
channel : str
71+
Channel identifier, e.g. "4.555_det1"
3472
35-
return extension
73+
Returns
74+
-------
75+
wavelength : str
76+
Wavelength setting, e.g. "4.555"
77+
detector : str
78+
Detector number, e.g. "1"
79+
"""
80+
pattern = r"([\d.]+)_det(\d)"
81+
match = re.match(pattern, channel, re.IGNORECASE)
82+
if not match:
83+
raise ValueError(f"Invalid channel format: {channel}")
84+
wavelength = match.group(1)
85+
detector = match.group(2)
86+
return wavelength, detector
87+
88+
def get_expected_values(self, target, night, channel):
89+
"""Get expected header values for file classification."""
90+
expectations = super().get_expected_values(target, night)
91+
wavelength, detector = self.parse_channel(channel)
92+
93+
for key in expectations.keys():
94+
if key == "bias":
95+
continue
96+
expectations[key]["wavelength"] = float(wavelength)
97+
98+
return expectations
99+
100+
def get_extension(self, header, channel):
101+
"""Get FITS extension for the given channel."""
102+
wavelength, detector = self.parse_channel(channel)
103+
return f"DET{detector}.DATA"
36104

37105
def get_wavecal_filename(self, header, channel, **kwargs):
38-
"""Get the filename of the wavelength calibration config file"""
106+
"""Get the filename of the wavelength calibration config file."""
107+
cwd = os.path.dirname(__file__)
108+
fname = f"wavecal_{channel}.npz"
109+
fname = os.path.join(cwd, fname)
110+
return fname
111+
112+
def get_mask_filename(self, channel, **kwargs):
113+
"""Get bad pixel mask filename (per detector)."""
114+
wavelength, detector = self.parse_channel(channel)
115+
fname = f"mask_det{detector}.fits.gz"
39116
cwd = os.path.dirname(__file__)
40-
fname = f"wavecal_{channel.lower()}_2D.npz"
41117
fname = os.path.join(cwd, fname)
42118
return fname
Lines changed: 31 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
11
# METIS IFU instrument configuration
2+
# ELT/METIS integral field unit spectrograph, L/M bands
3+
# ~100 wavelength settings × 4 detectors = ~400 channels
4+
# Channel format: {wavelength}_{detector}, e.g. 4.555_det1
25

36
__instrument__: METIS_IFU
47
instrument: INSTRUME
5-
id_instrument: METIS_IFU
8+
id_instrument: METIS
69
telescope: ELT
710

811
date: MJD-OBS
912
date_format: mjd
1013

11-
channels: [IFU_NOMINAL, IFU_EXTENDED]
12-
kw_channel: INSTMODE
13-
id_channel: [ifu_nom, ifu_ext]
14-
extension: 0
15-
orientation: [1, 1, 1]
14+
# Detectors - data in DET1.DATA through DET4.DATA extensions
15+
chips: [det1, det2, det3, det4]
16+
extension: DET1.DATA
17+
orientation: 0
18+
transpose: false
1619

1720
prescan_x: 0
1821
overscan_x: 0
@@ -23,40 +26,38 @@ naxis_y: NAXIS2
2326

2427
gain: 1
2528
readnoise: 4
26-
dark: 10
27-
exposure_time: "HIERARCH OBS_EXPTIME"
29+
dark: "ESO DET DIT"
30+
exposure_time: "ESO DET DIT"
2831

2932
ra: RA
3033
dec: DEC
3134
jd: MJD-OBS
32-
longitude: -70
33-
latitude: -24
35+
longitude: -70.1918
36+
latitude: -24.5899
3437
altitude: 3060
35-
instrument_mode: "HIERARCH ESO INS MODE"
36-
observation_type: "HIERARCH ESO DPR TYPE"
37-
observation_category: "HIERARCH ESO DPR CATG"
38-
target: ""
39-
object: OBJECT
38+
target: OBJECT
4039

41-
id_dark: DARK
42-
id_format: "LAMP,FMTCHK"
43-
id_tell: "STD,TELLURIC"
40+
# Wavelength setting from header
41+
id_wavelength: "ESO INS WLEN CEN"
42+
instrument_mode: "ESO INS MODE"
43+
observation_type: "ESO DPR TYPE"
44+
observation_category: "ESO DPR CATG"
4445

4546
# File classification keywords and patterns
46-
kw_bias: "HIERARCH ESO DPR TYPE"
47-
kw_flat: "HIERARCH ESO DPR TYPE"
48-
kw_curvature: "HIERARCH ESO DPR TYPE"
49-
kw_scatter: "HIERARCH ESO DPR TYPE"
50-
kw_orders: "HIERARCH ESO DPR TYPE"
51-
kw_wave: "HIERARCH ESO DPR TYPE"
47+
kw_bias: "ESO DPR TYPE"
48+
kw_flat: "ESO DPR TYPE"
49+
kw_curvature: "ESO DPR TYPE"
50+
kw_scatter: "ESO DPR TYPE"
51+
kw_orders: "ESO DPR TYPE"
52+
kw_wave: "ESO DPR TYPE"
5253
kw_comb: null
53-
kw_spec: "HIERARCH ESO DPR TYPE"
54+
kw_spec: "ESO DPR TYPE"
5455

5556
id_bias: DARK
56-
id_flat: "LAMP,FLAT"
57-
id_orders: "FLAT,PINHOLE"
58-
id_curvature: "SKY,WAVE"
59-
id_scatter: "LAMP,FLAT"
60-
id_wave: "SKY,WAVE"
57+
id_flat: RSRF
58+
id_orders: RSRF
59+
id_curvature: WAVE
60+
id_scatter: RSRF
61+
id_wave: WAVE
6162
id_comb: null
6263
id_spec: OBJECT

pyreduce/instruments/METIS_IFU/settings.json

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,27 @@
11
{
22
"__instrument__": "METIS_IFU",
33
"__inherits__": "defaults/settings.json",
4+
"bias": {
5+
"degree": 0
6+
},
47
"flat": {
5-
"bias_scaling": "number_of_files",
6-
"plot": false
8+
"bias_scaling": "exposure_time"
79
},
810
"trace": {
911
"degree": 4,
1012
"degree_before_merge": "best",
11-
"bias_scaling": "number_of_files",
1213
"filter_y": 20,
1314
"min_cluster": 100,
1415
"noise": 120,
1516
"border_width": 6,
1617
"auto_merge_threshold": 0.9,
1718
"merge_min_threshold": 0.01,
18-
"manual": false,
19-
"min_width": 0,
20-
"plot": false
19+
"bias_scaling": "exposure_time"
2120
},
2221
"scatter": {
23-
"bias_scaling": "number_of_files",
2422
"extraction_height": 0.9,
25-
"border_width": 20
23+
"border_width": 20,
24+
"bias_scaling": "exposure_time"
2625
},
2726
"norm_flat": {
2827
"smooth_slitfunction": 1000.0,
@@ -53,16 +52,10 @@
5352
"bias_scaling": "exposure_time"
5453
},
5554
"wavecal": {
56-
"extraction_method": "optimal",
57-
"bias_scaling": "exposure_time",
5855
"threshold": 70,
5956
"iterations": 3,
6057
"dimensionality": "2D",
61-
"extraction_height": 1.565,
62-
"degree": [
63-
4,
64-
4
65-
],
58+
"degree": [4, 4],
6659
"manual": true,
6760
"shift_window": 0.1
6861
},
@@ -71,6 +64,7 @@
7164
"extraction_height": 0.2847,
7265
"swath_width": 400,
7366
"smooth_slitfunction": 2,
74-
"smooth_spectrum": 0.1
67+
"smooth_spectrum": 0.1,
68+
"bias_scaling": "exposure_time"
7569
}
7670
}

0 commit comments

Comments
 (0)