Source code for toirex.grouping_frames

#!/usr/bin/env python3
from collections import defaultdict
import numpy as np
from pathlib import Path
import re

from .setups import get_logger

from .obscatalog import read_catalog
from .instrument import instruments
from .utils import open_in_editor
from .utils import extract_fname_prefix


[docs] def remove_repeated_values(candidate_list): """ Remove duplicate elements from a list, with special handling for NumPy arrays. This function iterates through the input list and removes repeated elements. Unlike the built-in `set`, it preserves the original order and works with elements that are NumPy arrays by comparing their contents (via `tolist()`) rather than object identity. Parameters ---------- candidate_list : list Input list potentially containing repeated elements. Elements can be of arbitrary type, including NumPy arrays. Returns ------- filtered_list : list A new list with repeated elements removed, preserving the first occurrence of each unique element. Notes ----- - For NumPy arrays, equality is determined by comparing the result of `.tolist()`, so arrays with the same contents but different memory locations are considered duplicates. - The function preserves the order of elements, unlike `set()`. Examples -------- >>> import numpy as np >>> arr1 = np.array([1, 2, 3]) >>> arr2 = np.array([1, 2, 3]) >>> candidate_list = [arr1, arr2, [4, 5], [4, 5], "a", "a"] >>> remove_repeated_values(candidate_list) [array([1, 2, 3]), [4, 5], 'a'] """ filtered_list = [] for cand in candidate_list: cand_list = cand.tolist() if isinstance(cand, np.ndarray) else cand if not any( (x.tolist() if isinstance(x, np.ndarray) else x) == cand_list for x in filtered_list ): filtered_list.append(cand) else: pass return filtered_list
[docs] def select_flats(catalog_dict, flats_keys, flat_flag): """ Select and group flat frames from a catalogue. This function extracts flat frames from the input catalogue based on classification flags and groups them according to a set of catalogue keys (e.g., grism, filter, order). Duplicate groups are removed using `remove_repeated_values`. The grouped flats are returned as a dictionary mapping a concatenated string of key values to the corresponding flat file names. Parameters ---------- catalog_dict : dict Dictionary containing the catalogue. Must include: - ``'FNAME'`` : list or array of file names - ``'FLAG'`` : list or array of classification flags - Additional keys specified in `flats_keys`. flats_keys : list of str List of catalogue dictionary keys used to define unique groups (e.g., ["GRISM", "FILTER"]). flat_flag : list of str List of flags that identify flat frames in the catalogue. Returns ------- grouped_flats : dict Dictionary where: - keys are strings obtained by joining the values of the group-defining keys with a space (e.g., "GRISM1 FILTER2"), - values are arrays of file names corresponding to that group of flats. Notes ----- - The grouping is done by extracting values from `catalog_dict` at positions where `FLAG` matches `flat_flag`. - Duplicate groups are removed using `remove_repeated_values`. - The function assumes that the catalogue dictionary values can be indexed and broadcast into NumPy arrays. Examples -------- >>> catalog_dict = { ... "FNAME": ["flat1.fits", "flat2.fits", "sci1.fits"], ... "FLAG": ["FLAT", "FLAT", "SCIENCE"], ... "FILTER": ["R", "R", "R"], ... "GRISM": ["G1", "G1", "G1"] ... } >>> flats_keys = ["FILTER", "GRISM"] >>> flat_flag = ["FLAT"] >>> select_flats(catalog_dict, flats_keys, flat_flag) {'R G1': array(['flat1.fits', 'flat2.fits'], dtype='<U10')} """ group_entries = [] fnames = np.array(catalog_dict['FNAME']) flags = catalog_dict['FLAG'] flat_mask = np.array([True if i in flat_flag else False for i in flags]) for keys in flats_keys: # print(keys) catalog_keys = np.array(catalog_dict[keys]) group_entries.append(catalog_keys[flat_mask]) group_entries = np.array(group_entries).T reduced_groups = remove_repeated_values(group_entries) flat_fnames = fnames[flat_mask] grouped_flats = {} for n, r in enumerate(reduced_groups): # print(r) mask = group_entries == r matchings = np.sum(mask, axis=1) == len(r) matched_fnames = flat_fnames[matchings] # print(matched_fnames) dictkw = " ".join(r) grouped_flats[dictkw] = matched_fnames # print(grouped_flats) return grouped_flats
[docs] def ordered_keys(catalog_dict, config, grouping_keys, flats_keys, flat_flag): """ Group files by specified catalogue keys and append matching flats. This function groups catalogue entries according to a set of header keys (e.g., grism, filter, order). For each unique group, it collects the corresponding file names and appends any flat frames that match based on `flats_keys`. The result is a dictionary mapping group indices to arrays of file names (science + flats). Parameters ---------- catalog_dict : dict Dictionary containing the catalogue. Must include: - ``'FNAME'`` : list or array of file names. - Additional keys specified in `grouping_keys` and `flats_keys`. config : dict Configuration dictionary (currently unused in this function, but passed for consistency with other pipeline functions). grouping_keys : list of str Catalogue dictionary keys used to define the main grouping of science files (e.g., ["OBJECT", "FILTER", "GRISM"]). flats_keys : list of str Catalogue dictionary keys used to group flat frames. flat_flag : list of str List of flags identifying flat frames in the catalogue. Returns ------- grouped_files : dict Dictionary where: - keys are integer group indices (0, 1, 2, …), - values are arrays of file names corresponding to that group, including any matching flat frames. Notes ----- - Groups are formed by unique combinations of values in `grouping_keys`. - Flat frames are grouped separately using `select_flats` and then merged into the corresponding science groups if their key values match. - Duplicate groups are removed using `remove_repeated_values`. - The function preserves the order of first occurrences. Examples -------- >>> catalog_dict = { ... "FNAME": ["sci1.fits", "sci2.fits", "flat1.fits"], ... "FLAG": ["SCIENCE", "SCIENCE", "FLAT"], ... "OBJECT": ["StarA", "StarA", "Lamp"], ... "FILTER": ["R", "R", "R"], ... "GRISM": ["G1", "G1", "G1"] ... } >>> grouping_keys = ["OBJECT", "FILTER", "GRISM"] >>> flats_keys = ["FILTER", "GRISM"] >>> flat_flag = ["FLAT"] >>> grouped = ordered_keys(catalog_dict, {}, grouping_keys, flats_keys, flat_flag) >>> list(grouped.keys()) [0] # one science group >>> grouped[0] array(['sci1.fits', 'sci2.fits', 'flat1.fits'], dtype='<U10') """ # Recognizing the position of flats_keys in grouping_keys. # This will filter out the entries for flat. # If this is not there, repeated entries will be grouped. flat_key_pos = np.array([True if i in flats_keys else False for i in grouping_keys]) fnames = np.array(catalog_dict['FNAME']) group_entries = [] # select_flats(catalog_dict, flats_keys, flat_flag) for keys in grouping_keys: group_entries.append(catalog_dict[keys]) group_entries = np.array(group_entries).T reduced_groups = remove_repeated_values(group_entries) grouped_flats = select_flats(catalog_dict, flats_keys, flat_flag) grouped_files = defaultdict(list) for n, r in enumerate(reduced_groups): mask = group_entries == r matchings = np.sum(mask, axis=1) == len(r) matched_fnames = fnames[matchings] for flat_kw, flat_fnames in grouped_flats.items(): flat_kwlist = flat_kw.strip().split(" ") filtered_r = np.array(r)[flat_key_pos] entry_mask = np.array([ True if i in flat_kwlist else False for i in filtered_r ]) if np.sum(entry_mask) == len(flat_kwlist): matched_fnames = np.concatenate(( matched_fnames, np.array(flat_fnames ))) grouped_files[n] = matched_fnames return grouped_files
[docs] def grouping_items(config, dirname, catalogue_dict=None, open_editor=True): """ Group catalogue files by observing setup while excluding flat frames. This function reads the catalogue for the specified directory, groups frames according to the instrument-specific grouping keys, and associates each frame with its classification flag. A human-readable summary of the groups is written to ``Grouped_txtfile.txt``, and the grouped catalogue is returned as a nested dictionary. Parameters ---------- config : dict Configuration dictionary. The following keys are required: - ``config['inits']['DICTKW']``: Instrument identifier used to select the appropriate grouping rules. - ``config['outputs']['OP_DIR']``: Output directory where the grouping summary is written. dirname : str or pathlib.Path Subdirectory of the output directory corresponding to the dataset. Returns ------- dict Nested dictionary containing the grouped catalogue. The outer dictionary maps group numbers to dictionaries of frame types. Each inner dictionary maps classification flags (e.g. ``OBJECT``, ``ARGON``, ``FLAT``) to lists of filenames. Notes ----- Flat frames are excluded using the instrument-specific ``flat_kw`` keyword. Grouping relies on the following components: - ``read_catalog()`` to load the catalogue. - ``instruments[dictkw]['grouping_keys']`` to define observing groups. - ``instruments[dictkw]['flat_grouping_keys']`` to associate flat frames. - ``ordered_keys()`` to construct the grouped catalogue. The function also writes ``Grouped_txtfile.txt`` to ``<config['outputs']['OP_DIR']>/<dirname>``. A placeholder exists for automatically associating continuum flats in a future version. Examples -------- >>> config = { ... "inits": {"DICTKW": "SpecTANSPEC"}, ... "outputs": {"OP_DIR": "Reduced_data"}, ... } >>> groups = grouping_items(config, "20240908") >>> sorted(groups.keys()) [0, 1, 2] """ logger = get_logger("grouping") logger.info("Automatic grouping of frames") if catalogue_dict is None: catalogue_dict = read_catalog(dirname, config) catalog_fnames = np.array(catalogue_dict['FNAME']) flags = catalogue_dict['FLAG'] dictkw = config['inits']['DICTKW'] flat_flag = instruments[dictkw]['flat_kw'] groups_dict = {} opdir = Path(config['outputs']['OP_DIR']) / \ dirname txt_fname = opdir / "Grouped_txtfile.txt" grouped_txt_file = open(txt_fname, 'w') grouping_keys = instruments[dictkw]['grouping_keys'] flat_keys = instruments[dictkw]['flat_grouping_keys'] ordered_dict = ordered_keys(catalogue_dict, config, grouping_keys, flat_keys, flat_flag) opfilename_prefix = open(opdir / "Filename_suggestions.txt", 'w') for order, fnames in ordered_dict.items(): subgroups_dict = defaultdict(list) group_header = "*" * 10 + "Group {} ".format(order) + "*" * 10 + "\n" grouped_txt_file.write(group_header) for crorder, fname in enumerate(catalog_fnames): if fname in fnames: # print(crorder, fname, 'in group', order) subgroups_dict[flags[crorder]].append(fname) if 'OBJECT' not in list(subgroups_dict.keys()): grouped_txt_file.write("\n No object in this group \n") continue opfilename_prefix.write( "{} : {}\n".format( order, extract_fname_prefix( subgroups_dict['OBJECT'][0], instruments[dictkw]['fname_regexp'] ) ) ) for keys, fnames in subgroups_dict.items(): grouped_txt_file.write( "{}: \n{}\n".format(keys, "\n".join(fnames)) ) grouped_txt_file.write("\n") grouped_txt_file.write("\n") groups_dict[order] = subgroups_dict # A function to add continuum flats here. grouped_txt_file.close() opfilename_prefix.close() if ( config['inits']['MODE'] == "AUTO" and config['inits']['TIMESERIES'] == 'N' ): open_editor = False if open_editor: open_in_editor(txt_fname, config) return groups_dict
[docs] def grouping_with_re(config, dirname): """ Group catalogue entries using user-specified regular expressions. This function reads a catalogue of image frames from the given directory and filters them based on user-defined regular expressions. Users are first prompted to input regex patterns for science frames and flat frames, and optionally for lamp frames (depending on the configuration). The matched frames are collected into a reduced catalogue and then grouped by instrument-specific keywords (e.g., SLIT, GRATING, FILTER) via `grouping_items`. Parameters ---------- config : dict Configuration dictionary with at least the following keys: - ``config['inits']['TODO']`` : str Indicates the processing mode. If set to ``'S'``, the user will be prompted to enter regex rules for lamp frames as well. - ``config['inits']['DICTKW']`` : str Key used to identify the instrument class from `instrument_class`. This is needed to obtain instrument-specific keywords (e.g., lamp keywords). dirname : str Path to the directory containing the catalogue and associated files. Returns ------- grouped_dict : dict A dictionary of grouped catalogue entries, as produced by `grouping_items`. The grouping is based on instrument-specific parameters (SLIT, GRATING, FILTER, etc.) after filtering by the user-provided regex rules. Notes ----- - The function is interactive: it prompts the user to enter regular expressions for selecting science, flat, and (if applicable) lamp frames. - Frames that match at least one of the provided regex rules are collected. - Grouping is performed by calling the external `grouping_items` function. - Regex documentation reference: http://docs.python.org/2/howto/regex.html#regex-howto Examples -------- >>> config = { ... 'inits': { ... 'TODO': 'S', ... 'DICTKW': 'TIRSPEC' ... } ... } >>> dirname = "/path/to/data" >>> grouped = grouping_with_re(config, dirname) Enter the regular expression for SCIECNE frames: .*M31.* Enter the regular expression for FLAT frames: .*continuum.* Enter the regular expression for LAMP frames: .*argon.* >>> print(grouped.keys()) dict_keys(['FNAME', 'SLIT', 'FILTER', 'GRATING', ...]) """ logger = get_logger("grouping_re") catalogue_dict = read_catalog(dirname, config) fnames_full = catalogue_dict['FNAME'] fnums_full = catalogue_dict['FNUM'] print('*'*10) print('For Regular Expression rules See:', end=" ") print('http://docs.python.org/2/howto/regex.html#regex-howto') print('Some examples of typical input are shown below') print('.*M31.* is the regular expression to select', end=" ") print('all the objects lines which has "M31" in it.') print('NB: Even you enter the regular expression, the objects', end=" ") print('will be grouped based on SLIT, GRATING, FILTER etc') enter_object = input("Enter the regular expression for SCIENCE frames:") logger.info(f"User entered: {enter_object}") object_re, object_fnums = reading_re(enter_object) re_dict = {object_re: object_fnums} enter_flats = input("Enter the regular expression for FLAT frames:") logger.info(f"User entered: {enter_flats}") flats_re, flat_fnums = reading_re(enter_flats) re_dict[flats_re] = flat_fnums if config['inits']['TODO'] == 'S': dictkw = config['inits']['DICTKW'] lamp_keys = instruments[dictkw]['lamp_kw'] for lamp in lamp_keys: enter_lamp = input( "Enter the regular expression for {} frames:".format(lamp) ) logger.info(f"User entered: {enter_lamp}") lamp_re, lamp_fnums = reading_re(enter_lamp) re_dict[lamp_re] = lamp_fnums selected_objects_dict = defaultdict(list) catalog_kws = catalogue_dict.keys() for res_ent, fnums in re_dict.items(): fnum_mask = (fnums_full >= fnums[0]) & \ (fnums_full <= fnums[1]) fnames = fnames_full[fnum_mask] for n, imgline in enumerate(fnames): # Converting the entered re string to re object res = re.compile(r''+res_ent) if res.search(imgline) is not None: for dictkws in catalog_kws: selected_objects_dict[dictkws].append( catalogue_dict[dictkws][fnum_mask][n]) grouped_dict = grouping_items(config, dirname, catalogue_dict=selected_objects_dict, open_editor=False) return grouped_dict
[docs] def reading_re(entered_re): """ Function to read the entered regular expression and separate FNUM from the entered item. """ if len(entered_re.strip().split(" ")) == 3: re_list = entered_re.strip().split(" ") object_re = re_list[0] start_fnum = int(re_list[1]) end_fnum = int(re_list[-1]) else: object_re = entered_re start_fnum = 0 end_fnum = 10000000 fnum_list = [start_fnum, end_fnum] return object_re, fnum_list
# End