|
1 | 1 | """ |
2 | | -Handles instrument specific info for the METIS_IFU IFU spectrograph |
| 2 | +METIS IFU instrument - ELT/METIS integral field unit spectrograph. |
3 | 3 |
|
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. |
5 | 6 | """ |
6 | 7 |
|
7 | 8 | import logging |
8 | 9 | import os.path |
| 10 | +import re |
| 11 | +from glob import glob |
| 12 | + |
| 13 | +from astropy.io import fits |
9 | 14 |
|
10 | 15 | from ..common import Instrument |
| 16 | +from ..filters import Filter |
11 | 17 |
|
12 | 18 | logger = logging.getLogger(__name__) |
13 | 19 |
|
14 | 20 |
|
15 | 21 | 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"] |
29 | 26 |
|
| 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() |
30 | 32 | return header |
31 | 33 |
|
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" |
34 | 72 |
|
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" |
36 | 104 |
|
37 | 105 | 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" |
39 | 116 | cwd = os.path.dirname(__file__) |
40 | | - fname = f"wavecal_{channel.lower()}_2D.npz" |
41 | 117 | fname = os.path.join(cwd, fname) |
42 | 118 | return fname |
0 commit comments