def _match_models(models, channel, degree, center=None, center_cs='image'): from ..cube_build import CubeBuildStep # create a list of cubes: cbs = CubeBuildStep() cbs.channel = str(channel) cbs.band = 'ALL' cbs.single = True cbs.weighting = 'EMSM' cube_models = cbs.process(models) if len(cube_models) != len(models): raise RuntimeError("The number of generated cube models does not " "match the number of input 2D images.") # retrieve WCS (all cubes must have identical WCS so we use the first): meta = cube_models[0].meta if hasattr(meta, 'wcs'): wcs = meta.wcs else: raise ValueError("Cubes build from input 2D images do not contain WCS." " Unable to proceed.") wcsinfo = meta.wcsinfo if hasattr(meta, 'wcsinfo') else None if wcsinfo is not None and (wcsinfo.crval1 is None or wcsinfo.crval2 is None or wcsinfo.crval3 is None): raise ValueError("'wcsinfo' cannot have its 'crvaln' set to None.") # set center of the coordinate system to CRVAL if available: if center is None and wcsinfo is not None: center = (wcsinfo.crval1, wcsinfo.crval2, wcsinfo.crval3) center_cs = 'world' # build lists of data, masks, and sigmas (weights) image_data = [] mask_data = [] sigma_data = [] for cm in cube_models: #TODO: at this time it is not clear that data should be weighted # by exptime the way it is done below and possibly should be # revised later. exptime = cm.meta.exposure.exposure_time if exptime is None: exptime = 1.0 # process weights and create masks: if not hasattr(cm, 'weightmap') or cm.weightmap is None: weights = np.ones_like(cm.data, dtype=np.float64) sigmas = weights / np.sqrt(exptime) mask = np.ones_like(weights, dtype=np.uint8) mask_data.append(mask) else: weights = cm.weightmap.copy() eps = np.finfo(weights.dtype).tiny bad_data = weights < eps weights[bad_data] = eps # in order to avoid runtime warnings sigmas = 1.0 / np.sqrt(exptime * weights) mask = np.logical_not(bad_data).astype(np.uint8) mask_data.append(mask) image_data.append(cm.data) sigma_data.append(sigmas) # leaving in below commented out lines for # Mihia to de-bug step when coefficients are NAN #mask_array = np.asarray(mask_data) #image_array = np.asarray(image_data) #sigma_array = np.asarray(sigma_data) #test_data = image_array[mask_array>0] #test_sigma = sigma_array[mask_array>0] #if np.isnan(test_data).any(): # print('a nan exists in test data') #if np.isnan(sigma_data).any(): # print('a nan exists in sigma data') # MRS fields of view are small compared to source sizes, # and undersampling produces significant differences # in overlap regions between exposures. # Therefore use sigma-clipping to detect and remove sources # (and unmasked outliers) prior to doing the background matching. # Loop over input exposures for image, mask in zip(image_data, mask_data): # Do statistics wavelength by wavelength for thisimg, thismask in zip(image, mask): # Avoid bug in sigma_clipped_stats (fixed in astropy 4.0.2) which # fails on all-zero arrays passed when mask_value=0 if not np.any(thisimg): themed = 0. clipsig = 0. else: # Sigma clipped statistics, ignoring zeros where no data _, themed, clipsig = sigclip(thisimg, mask_value=0.) # Reject beyond 3 sigma reject = np.where(np.abs(thisimg - themed) > 3 * clipsig) thismask[reject] = 0 bkg_poly_coef, mat, _, _, effc, cs = match_lsq(images=image_data, masks=mask_data, sigmas=sigma_data, degree=degree, center=center, image2world=wcs.__call__, center_cs=center_cs, ext_return=True) if cs != 'world': raise RuntimeError("Unexpected coordinate system.") #TODO: try to identify if all images overlap #if nsubspace > 1: #self.log.warning("Not all cubes have been sky matched as " #"some of them do not overlap.") # save background info in 'meta' and subtract sky from 2D images # if requested: ##### model.meta.instrument.channel if np.isnan(bkg_poly_coef).any(): bkg_poly_coef = None for im in models: im.meta.cal_step.mrs_imatch = 'SKIPPED' im.meta.background.subtracted = False else: # set 2D models' background meta info: for im, poly in zip(models, bkg_poly_coef): im.meta.background.subtracted = False im.meta.background.polynomial_info.append({ 'degree': degree, 'refpoint': center, 'coefficients': poly.ravel().tolist(), 'channel': channel }) return models
def trace_slice(thisslice, data, snum, basex, basey, nmed, method, verbose): # Zero out everything outside the peak slice indx = np.where(snum == thisslice) data_slice = data * 0. data_slice[indx] = data[indx] ysize, xsize = data.shape xmin, xmax = np.min(basex[indx]), np.max(basex[indx]) ################### # First pass for x locations in this slice; if verbose: print('First pass trace fitting') xcen_pass1 = np.zeros(ysize) for ii in range(0, ysize): ystart = max(0, int(ii - nmed / 2)) ystop = min(ysize, ystart + nmed) cut = np.nanmedian(data_slice[ystart:ystop, :], axis=0) xcen_pass1[ii] = np.argmax(cut) # Clean up any bad values by looking for 3sigma outliers # and replacing them with the median value rms, med = np.nanstd(xcen_pass1), np.nanmedian(xcen_pass1) indx = np.where((xcen_pass1 < med - 3 * rms) | (xcen_pass1 > med + 3 * rms)) xcen_pass1[indx] = med xwid_pass1 = np.ones(ysize) # First pass width is 1 pixel ################### # Second pass for x locations along the trace within this slice if verbose: print('Second pass trace fitting') xcen_pass2 = np.zeros(ysize) xwid_pass2 = np.zeros(ysize) for ii in range(0, ysize): xtemp = np.arange(xmin, xmax, 1) ftemp = data_slice[ii, xtemp] # Initial guess at fit parameters p0 = [ftemp.max(), xcen_pass1[ii], xwid_pass1[ii], 0.] # Bounds for fit parameters bound_low = [0., xcen_pass1[ii] - 3 * rms, 0, -ftemp.max()] bound_hi = [ 10 * np.max(ftemp), xcen_pass1[ii] + 3 * rms, 10, ftemp.max() ] # Do the fit popt, _ = curve_fit(gauss1d, xtemp, ftemp, p0=p0, bounds=(bound_low, bound_hi), method='trf') xcen_pass2[ii] = popt[1] xwid_pass2[ii] = popt[2] ################### # Third pass for x location; use a fixed profile width twidth = np.nanmedian(xwid_pass2) if verbose: print('Third pass trace fitting, median trace width ', twidth, ' pixels') xcen_pass3 = np.zeros(ysize) for ii in range(0, ysize): xtemp = np.arange(xmin, xmax, 1) ftemp = data_slice[ii, xtemp] # Initial guess at fit parameters p0 = [ftemp.max(), xcen_pass2[ii], twidth, 0.] # Bounds for fit parameters bound_low = [ 0., xcen_pass2[ii] - 3 * rms, twidth * 0.999, -ftemp.max() ] bound_hi = [ 10 * np.max(ftemp), xcen_pass2[ii] + 3 * rms, twidth * 1.001, ftemp.max() ] # Do the fit popt, _ = curve_fit(gauss1d, xtemp, ftemp, p0=p0, bounds=(bound_low, bound_hi), method='trf') xcen_pass3[ii] = popt[1] # Clean up the fit to remove outliers qual = np.ones(ysize) # Low order polynomial fit to find the worst outliers using plain RMS fit = np.polyfit(basey[:, 0], xcen_pass3, 2) temp = np.polyval(fit, basey[:, 0]) indx = np.where( np.abs(xcen_pass3 - temp) > 3 * np.nanstd(xcen_pass3 - temp)) qual[indx] = 0 good = (np.where(qual == 1))[0] # Another fit to find lesser outliers using sigma-clipped RMS fit = np.polyfit(basey[good, 0], xcen_pass3[good], 2) temp = np.polyval(fit, basey[:, 0]) indx = np.where( np.abs(xcen_pass3 - temp) > 3 * (sigclip(xcen_pass3 - temp)[2])) qual[indx] = 0 # Look for bad failures (e.g., steps that occur if the source is at the edge # of the field) diff = xcen_pass3 - temp good = (np.where(qual == 1))[0] rms = np.nanstd(diff[good]) # If rms of the GOOD point fit is over 0.3 pixels don't do spline fitting if (rms > 0.3): print('WARNING: Alpha trace is poor! Source at edge of field?') # Replace bad values bad = np.where(qual == 0) xcen_pass3[bad] = temp[bad] else: # Spline fit spl = UnivariateSpline(basey[:, 0], xcen_pass3, w=qual, s=1) model = spl(basey[:, 0]) # Replace bad values bad = np.where(qual == 0) xcen_pass3[bad] = model[bad] # If method='model' then return the model itself for everything if (method == 'model'): xcen_pass3 = model return xcen_pass2, xcen_pass3