Source code for corgidrp.combine

"""
Module to support frame combination
"""
import warnings
import numpy as np
import corgidrp.data as data
from pyklip.klip import rotate
import corgidrp

[docs] def combine_images(data_subset, err_subset, dq_subset, collapse, num_frames_scaling, other_hdus=None): """ Combines several images together Args: data_subset (np.array): 3-D array of N 2-D images err_subset (np.array): 4-D array of N 3-D error maps dq_subset (np.array): 3-D array of N 2-D DQ maps collapse (str): "mean" or "median". num_frames_scaling (bool): Multiply by number of frames in sequence in order to ~conserve photons other_hdus (list of HDULists, optional): list of other HDULists to be combined in the same way Returns: tuple: combined images np.array: 2-D array of combined images np.array: 3-D array of combined error map np.array: 2-D array of combined DQ maps list of np.array: list of the combined data of other HUDs """ tot_frames = data_subset.shape[0] # mask bad pixels bad = np.where(dq_subset > 0) data_subset[bad] = np.nan err_subset[bad[0],:,bad[1],bad[2]] = np.nan # track the number of good values that go into the combination n_samples = np.ones(data_subset.shape) n_samples[bad] = 0 n_samples = np.sum(n_samples, axis=0) if collapse.lower() == "mean": with warnings.catch_warnings(): # prevent RuntimeWarning: Mean of empty slice warnings.filterwarnings('ignore', category=RuntimeWarning) data_collapse = np.nanmean(data_subset, axis=0) err_collapse = np.sqrt(np.nanmean(err_subset**2, axis=0)) /np.sqrt(n_samples) # correct assuming standard error propagation elif collapse.lower() == "median": with warnings.catch_warnings(): # prevent RuntimeWarning: Mean of empty slice warnings.filterwarnings('ignore', category=RuntimeWarning) data_collapse = np.nanmedian(data_subset, axis=0) err_collapse = np.sqrt(np.nanmean(err_subset**2, axis=0)) /np.sqrt(n_samples) * np.sqrt(np.pi/2) # inflate median error if num_frames_scaling: # scale up by the number of frames data_collapse *= tot_frames err_collapse *= tot_frames # dq collpase: keep all flags on dq_collapse = np.bitwise_or.reduce(dq_subset, axis=0) # except for those pixels that have been replaced with good values dq_collapse[np.where((dq_collapse > 0) & (~np.isnan(data_collapse)))] = 0 # other hdus if other_hdus is not None: combined_hdus = [[] for _ in range(len(other_hdus[0]))] # iterate over each hdulist and append the data for hdul in other_hdus: for i, hdu in enumerate(hdul): combined_hdus[i].append(np.copy(hdu.data)) # now combine each hdu data for i in range(len(combined_hdus)): # TODO: not implemented how to take means of anything beyond np.arrays (e.g., np.recarray) try: if collapse.lower() == "mean": combined_hdus[i] = np.nanmean(np.array(combined_hdus[i]), axis=0) elif collapse.lower() == "median": combined_hdus[i] = np.nanmedian(np.array(combined_hdus[i]), axis=0) # nothing here makes sense to scale by number of frames # if num_frames_scaling: # combined_hdus[i] *= tot_frames except: combined_hdus[i] = combined_hdus[i][0] # just take the first one if cannot combine else: combined_hdus = None return data_collapse, err_collapse, dq_collapse, combined_hdus
[docs] def combine_subexposures(input_dataset, num_frames_per_group=None, collapse="mean", num_frames_scaling=True, combine_other_hdus=False): """ Combines a sequence of exposures assuming a constant nubmer of frames per group. The length of the dataset must be divisible by the number of frames per group. The combination is done with either the mean or median, but the collapsed image can be scaled in order to ~conserve the total number of photons in the input dataset (this essentially turns a median into a sum) Args: input_dataset (corgidrp.data.Dataset): input data. num_frames_per_group (int): number of subexposures per group. If None, combines all images together collapse (str): "mean" or "median". (default: mean) num_frames_scaling (bool): Multiply by number of frames in sequence in order to ~conserve photons (default: True) combine_other_hdus (bool): Whether to combine other HDUs in the same way as the main data, err, DQ. Otherwise, uses the HDUs from the first frame in a subset (default: False) Returns: corgidrp.data.Dataset: dataset after combination of every "num_frames_per_group" frames together """ if num_frames_per_group is None: num_frames_per_group = len(input_dataset) if len(input_dataset) % num_frames_per_group != 0: raise ValueError("Input dataset of length {0} cannot be grouped in sets of {1}".format(len(input_dataset), num_frames_per_group)) if collapse.lower() not in ["mean", "median"]: raise ValueError("combine_subexposures can only collapse with mean or median") num_groups = len(input_dataset) // num_frames_per_group new_dataset = [] for i in range(num_groups): data_subset = np.copy(input_dataset.all_data[num_frames_per_group*i:num_frames_per_group*(i+1)]) err_subset = np.copy(input_dataset.all_err[num_frames_per_group*i:num_frames_per_group*(i+1)]) dq_subset = np.copy(input_dataset.all_dq[num_frames_per_group*i:num_frames_per_group*(i+1)]) if combine_other_hdus: other_hdus = [input_dataset[j].hdu_list for j in range(num_frames_per_group*i, num_frames_per_group*(i+1))] else: other_hdus = None data_collapse, err_collapse, dq_collapse, combined_hdus = combine_images(data_subset, err_subset, dq_subset, collapse=collapse, num_frames_scaling=num_frames_scaling, other_hdus=other_hdus) # grab the headers from the first frame in this sub sequence pri_hdr = input_dataset[num_frames_per_group*i].pri_hdr.copy() ext_hdr = input_dataset[num_frames_per_group*i].ext_hdr.copy() ext_hdr["NUM_FR"] = num_frames_per_group err_hdr = input_dataset[num_frames_per_group*i].err_hdr.copy() dq_hdr = input_dataset[num_frames_per_group*i].dq_hdr.copy() hdulist = input_dataset[num_frames_per_group*i].hdu_list.copy() # update other hdus if needed if combine_other_hdus: for j, hdu in enumerate(hdulist): hdu.data = combined_hdus[j] hdu.header["NUM_FR"] = num_frames_per_group hdu.header['HISTORY'] = "Combined {0} frames by {1}".format(num_frames_per_group, collapse) new_image = data.Image(data_collapse, pri_hdr=pri_hdr, ext_hdr=ext_hdr, err=err_collapse, dq=dq_collapse, err_hdr=err_hdr, dq_hdr=dq_hdr, input_hdulist=hdulist) # always take the last filename in the group for the combined frame last_idx_in_group = num_frames_per_group*(i+1) - 1 new_image.filename = input_dataset[last_idx_in_group].filename new_image._record_parent_filenames(input_dataset[num_frames_per_group*i:num_frames_per_group*(i+1)]) new_dataset.append(new_image) new_dataset = data.Dataset(new_dataset) drpnfile = new_dataset[0].ext_hdr['DRPNFILE'] # Here we change header keywords only for the combined non-coronagraphic imaging datasets if (input_dataset[0].ext_hdr['DPAMNAME'] == 'IMAGING' and input_dataset[0].ext_hdr['LSAMNAME'] == 'OPEN') and input_dataset[0].ext_hdr['DATALVL'] == 'L3': # average/delete header keywords as L4 involves combination of multiple frames pri_hdr_comb, ext_hdr_comb, _, _ = corgidrp.check.merge_headers(input_dataset, last_frame_keywords=['VISITID', 'MJDEND', 'SCTEND'], first_frame_keywords=['MJDSRT','SCTSRT','CD1_1', 'CD1_2', 'CD2_1', 'CD2_2', 'CRPIX1', 'CRPIX2','NORTHANG'], deleted_keywords=['CDELT1','CDELT2','FILE0'] + corgidrp.check.deleted_keywords_default, #we re-add FILE0 below invalid_keywords=[ #Primary header keywords 'FILETIME', 'PA_V3', 'PA_APER','SVB_1', 'SVB_2', 'SVB_3', 'ROLL', 'PITCH', 'YAW', 'WBJ_1', 'WBJ_2', 'WBJ_3', #Extension header keywords 'DATETIME', 'FTIMEUTC','DATATYPE'], averaged_keywords=['EXCAMT','NOVEREXP','PROXET', 'FCMPOS','FSMSG1', 'FSMSG2', 'FSMSG3', 'FSMX', 'FSMY', 'SB_FP_DX', 'SB_FP_DY', 'SB_FS_DX', 'SB_FS_DY', 'Z2AVG', 'Z3AVG', 'Z4AVG', 'Z5AVG', 'Z6AVG', 'Z7AVG', 'Z8AVG', 'Z9AVG', 'Z10AVG', 'Z11AVG', 'Z12AVG', 'Z13AVG', 'Z14AVG', 'Z2RES', 'Z3RES', 'Z4RES', 'Z5RES', 'Z6RES', 'Z7RES', 'Z8RES', 'Z9RES', 'Z10RES', 'Z11RES', 'Z2VAR', 'Z3VAR']) # incorporate modified headers in L4 dataset for img in new_dataset: img.pri_hdr = pri_hdr_comb img.ext_hdr = ext_hdr_comb img.ext_hdr['NUM_FR'] = num_frames_per_group img.ext_hdr['DRPNFILE'] = drpnfile img._record_parent_filenames(input_dataset) new_dataset.update_after_processing_step("Combine_subexposures: combined every {0} frames by {1}".format(num_frames_per_group, collapse)) return new_dataset
def _append_unique(default_keywords, input_keywords): """ Append input keywords after default keywords while preserving order and removing duplicates. """ combined_keywords = [] for keyword in default_keywords + list(input_keywords): if keyword not in combined_keywords: combined_keywords.append(keyword) return combined_keywords
[docs] def combine_frames_per_visit(input_dataset, collapse="mean", num_frames_per_group=None, num_frames_scaling=False, combine_other_hdus=False, max_combined=100, pri_split_keywords=['VISITID'], ext_split_keywords=['DPAMNAME']): """ Combines frames in the input dataset by calling combine_subexposures on each unique split of VISITID and polarization state (DPAMNAME). The combination is done with either the mean or median, but the collapsed image can be scaled in order to ~conserve the total number of photons in the input dataset (this essentially turns a median into a sum). Args: input_dataset (corgidrp.data.Dataset): a dataset of Images (L3-level) collapse (str): "mean" or "median". (default: mean) num_frames_per_group (int or None): number of consecutive frames to combine within each VISITID/DPAMNAME split subset. If None, combine all frames in each subset into one output frame. If the requested grouping would create more than ``max_combined`` output frames in a split subset, it is overridden with the smallest group size that keeps the per-subset output count at or below ``max_combined``; trailing remainder frames are left out. (default: None) num_frames_scaling (bool): Multiply by number of frames in sequence in order to ~conserve photons (default: False) combine_other_hdus (bool): Whether to combine other HDUs in the same way as the main data, err, DQ. Otherwise, uses the HDUs from the first frame in a subset (default: False) max_combined (int): maximum number of output frames allowed per split subset before ``num_frames_per_group`` is automatically increased. (default: 100) pri_split_keywords (list of str): Additional primary header keywords to use for splitting the dataset into subsets. The default split keyword ['VISITID'] is always included, and input values are appended. (default: ['VISITID']) ext_split_keywords (list of str): Additional extension header keywords to use for splitting the dataset into subsets. The default split keyword ['DPAMNAME'] is always included, and input values are appended. (default: ['DPAMNAME']) Returns: corgidrp.data.Dataset: dataset of combined frames. If ``num_frames_per_group`` is None, there is one output frame per split subset, so ``len(output_dataset)`` equals the number of unique VISITID groups or unique ``(VISITID, DPAMNAME)`` groups when ``DPAMNAME`` is present for all frames. If ``num_frames_per_group`` is set, each split subset is further combined in sequential groups of ``num_frames_per_group``. If a requested grouping would create more than ``max_combined`` output frames in a split subset, the grouping is increased so that the output count stays at or below ``max_combined`` and trailing remainder frames are left out. Each output frame keeps the same internal data cube shape as an input frame; only the dataset/frame axis is collapsed. """ if num_frames_per_group is not None and num_frames_per_group <= 0: raise ValueError("num_frames_per_group must be a positive integer or None") if max_combined <= 0: raise ValueError("max_combined must be a positive integer") default_pri_split_keywords = ['VISITID'] default_ext_split_keywords = ['DPAMNAME'] if pri_split_keywords is None: pri_split_keywords = [] if ext_split_keywords is None: ext_split_keywords = [] pri_split_keywords = _append_unique(default_pri_split_keywords, pri_split_keywords) ext_split_keywords = _append_unique(default_ext_split_keywords, ext_split_keywords) split_keywords = list(pri_split_keywords) + list(ext_split_keywords) split_description = "/".join(split_keywords) if split_keywords else "input" # Combine each split subset independently. split_datasets, _ = input_dataset.split_dataset(prihdr_keywords=pri_split_keywords, exthdr_keywords=ext_split_keywords) combined_frames = [] any_auto_override = False for split_dataset in split_datasets: effective_num_frames_per_group = num_frames_per_group if num_frames_per_group is not None: requested_output_frames = len(split_dataset) / num_frames_per_group if requested_output_frames > max_combined: effective_num_frames_per_group = -(-len(split_dataset) // max_combined) any_auto_override = True combined_split = combine_subexposures( split_dataset, num_frames_per_group=effective_num_frames_per_group, collapse=collapse, num_frames_scaling=num_frames_scaling, combine_other_hdus=combine_other_hdus, ) combined_frames.extend(combined_split.frames.tolist()) combined_dataset = data.Dataset(combined_frames) if num_frames_per_group is None: history_msg = ( f"Combined frames by applying {collapse} within each {split_description} subset, " f"result is a dataset with {len(combined_dataset)} frame(s)" ) else: history_msg = ( f"Combined frames by applying {collapse} in sequential groups of {num_frames_per_group} " f"within each {split_description} subset, result is a dataset with " f"{len(combined_dataset)} frame(s)" ) if any_auto_override: history_msg += f" Automatically increased some group sizes to keep per-subset output frame counts at or below {max_combined}." combined_dataset.update_after_processing_step(history_msg) return combined_dataset
[docs] def derotate_arr(data_arr,northang_deg, xcen,ycen,new_center=None,astr_hdr=None, is_dq=False,dq_round_threshold=0.05): """Derotates an array based on the provided NORTHANG angle, about the provided center. Treats DQ arrays specially, converting to float to do the rotation, and converting back to np.int64 afterwards. DQ output becomes only zeros and ones, so detailed DQ flag information is not preserved. Args: data_arr (np.array): an array with 2-4 dimensions northang_deg (float): angle (measured counter-clockwise) of the detector y axis from celestial north (degrees). Calculated from the northangle of the astrometric cal frame and the PA_APER offset between the astrom cal frame and the science frame. xcen (float): x-coordinate of center about which to rotate ycen (float): y-coordinate of center about which to rotate new_center (tuple, optional): tuple of x- and y- coordinate of the new center to shift to. astr_hdr (astropy.fits.Header, optional): WCS header which will be updated. Defaults to None. is_dq (bool, optional): Flag to determine if this is a DQ array. Defaults to False. dq_round_threshold (float, optional): value between 0-1 which determines the threshold for spreading dq values to neighboring pixels after derotation. Returns: np.array: The derotated array. """ # Temporarily convert dq to floats if is_dq: data_arr = data_arr.astype(np.float32) if data_arr.ndim == 2: derotated_arr = rotate(data_arr,northang_deg,(xcen,ycen), new_center=new_center, astr_hdr=astr_hdr) # astr_hdr is corrected at above lines elif data_arr.ndim == 3: derotated_arr = [] for i,im in enumerate(data_arr): derotated_im = rotate(im,northang_deg,(xcen,ycen), new_center=new_center, astr_hdr=astr_hdr if (i==0) else None) # astr_hdr is corrected only once derotated_arr.append(derotated_im) derotated_arr = np.array(derotated_arr) elif data_arr.ndim == 4: derotated_arr = [] for s,set in enumerate(data_arr): derotated_set = [] for i,im in enumerate(set): derotated_im = rotate(im,northang_deg,(xcen,ycen), new_center=new_center, astr_hdr=astr_hdr if (i==0 and s==0) else None) # astr_hdr is corrected only once derotated_set.append(derotated_im) derotated_arr.append(derotated_set) derotated_arr = np.array(derotated_arr) else: raise ValueError('derotate_arr() not configured for data with >4 dimensions') # convert dq_array back to ints if is_dq: derotated_arr[np.isnan(derotated_arr)] = 1 # assign nans to 1 derotated_arr_int = (derotated_arr>dq_round_threshold).astype(np.int64) # import matplotlib.pyplot as plt # plt.imshow(derotated_arr_int,origin='lower') # plt.colorbar() # plt.title(f'round_threshold: {round_threshold}') # plt.show() return derotated_arr_int return derotated_arr
[docs] def prop_err_dq(sci_dataset,ref_dataset,mode,dq_thresh=1,new_center=None): """Applies logic to propagate the dq arrays and error arrays in a dataset through PSF subtraction. Args: sci_dataset (corgidrp.data.Dataset): The input science dataset. ref_dataset (corgidrp.data.Dataset): The input reference dataset (or None if ADI only). mode (str): The PSF subtraction mode, e.g. "ADI", "RDI", "ADI+RDI". dq_thresh (int): Minimum dq flag value to be considered a bad pixel. Defaults to 1. new_center (tuple): New center (xy) to align all frames. Defaults to pixel closest to array center. Returns: tuple of np.array: the dq array and err array which should apply to the PSF subtraction output dataset. """ # Assign master output dq & error (before derotation) # dq shape = (n_rolls, n_wls(optional), y, x) sci_input_dqs = sci_dataset.all_dq >= dq_thresh sci_input_errs = np.full_like(sci_dataset.all_err,np.nan) # Set errors to np.nan for now if new_center is None: new_center = [int(sci_dataset.all_data.shape[-1]//2), int(sci_dataset.all_data.shape[-2]//2)] # Align frames aligned_sci_dq_arr = [] aligned_sci_err_arr = [] for i,frame in enumerate(sci_dataset): xcen, ycen = frame.ext_hdr['STARLOCX'], frame.ext_hdr['STARLOCY'] frame.ext_hdr['STARLOCX'], frame.ext_hdr['STARLOCY'] = new_center aligned_sci_dq = derotate_arr(sci_input_dqs[i],0, xcen,ycen, new_center=new_center,is_dq=True) aligned_sci_err = derotate_arr(sci_input_errs[i],0, xcen,ycen, new_center=new_center) aligned_sci_dq_arr.append(aligned_sci_dq) aligned_sci_err_arr.append(aligned_sci_err) aligned_sci_dq_arr = np.array(aligned_sci_dq_arr) aligned_sci_err_arr = np.array(aligned_sci_err_arr) if "RDI" in mode: ref_input_dqs = ref_dataset.all_dq >= dq_thresh ref_input_errs = np.full_like(ref_dataset.all_err,np.nan) # Set errors to np.nan for now aligned_ref_dq_arr = [] aligned_ref_err_arr = [] for i,frame in enumerate(ref_dataset): xcen, ycen = frame.ext_hdr['STARLOCX'], frame.ext_hdr['STARLOCY'] frame.ext_hdr['STARLOCX'], frame.ext_hdr['STARLOCY'] = new_center aligned_ref_dq = derotate_arr(ref_input_dqs[i],0, xcen,ycen, new_center=new_center,is_dq=True) aligned_ref_err = derotate_arr(ref_input_errs[i],0, xcen,ycen, new_center=new_center) aligned_ref_dq_arr.append(aligned_ref_dq) aligned_ref_err_arr.append(aligned_ref_err) aligned_ref_dq_arr = np.array(aligned_ref_dq_arr) aligned_ref_err_arr = np.array(aligned_ref_err_arr) # If doing ADI, flag pixels that are bad in all science frames if 'ADI' in mode: aligned_sci_dq_arr[:] = np.all(aligned_sci_dq_arr,axis=0) # If using references, flag pixels that are bad in all the ref frames if 'RDI' in mode: ref_output_dqs_flat = np.all(aligned_ref_dq_arr,axis=0,keepdims=True) aligned_sci_dq_arr = np.logical_or(aligned_sci_dq_arr,ref_output_dqs_flat) # Derotate dq & error derotated_dq_arr = [] derotated_err_arr = [] for i,frame in enumerate(sci_dataset): northang_deg = frame.ext_hdr['NORTHANG'] xcen, ycen = frame.ext_hdr['STARLOCX'], frame.ext_hdr['STARLOCY'] derotated_dq = derotate_arr(aligned_sci_dq_arr[i],northang_deg, xcen,ycen,is_dq=True) derotated_err = derotate_arr(aligned_sci_err_arr[i],northang_deg, xcen,ycen) derotated_dq_arr.append(derotated_dq) derotated_err_arr.append(derotated_err) # Collapse dq & error dq_out_collapsed = np.where(np.all(derotated_dq_arr,axis=0),1,0) err_out_collapsed = np.sqrt(np.sum(np.array(derotated_err_arr)**2,axis=0)) return dq_out_collapsed, err_out_collapsed