def make_channel_models(channel): f = fits.open('MIRI_FM_LW_A_D2C_01.00.00.fits') if channel == 3: slice_mask = f[3].data[:, :500] b1 = f[0].header['B_DEL3'] b0 = f[0].header['B_MIN3'] elif channel == 4: slice_mask = f[3].data[:, 500:] b1 = f[0].header['B_DEL4'] b0 = f[0].header['B_MIN4'] slices = np.unique(slice_mask) slices = np.asarray(slices, dtype=np.int16).tolist() slices.remove(0) #read alpha and lambda planes from pixel maps file lam = f[1].data alpha = f[2].data # create a model to fit to each slice in alpha plane amodel = models.Chebyshev2D(x_degree=2, y_degree=1) # a model to be fitted to each slice in lambda plane lmodel = models.Chebyshev2D(x_degree=1, y_degree=1) lmodel.c0_1.fixed = True lmodel.c1_1.fixed = True fitter = fitting.LinearLSQFitter() reg_models = {} for sl in slices: #print('sl', sl) ind = ( slice_mask == sl).nonzero() #(slice_mask[:, :500] ==sl).nonzero() x0 = ind[0].min() x1 = ind[0].max() y0 = ind[1].min() y1 = ind[1].max() if channel == 4: y0 += 500 y1 += 500 x, y = np.mgrid[x0:x1, y0:y1] sllam = lam[x0:x1, y0:y1] slalpha = alpha[x0:x1, y0:y1] lfitted = fitter(lmodel, x, y, sllam) afitted = fitter(amodel, x, y, slalpha) if channel == 4: beta_model = models.Const1D(b0 + b1 * (sl + 12)) reg_models[sl + 12] = lfitted, afitted, beta_model, (x0, x1, y0, y1) else: beta_model = models.Const1D(b0 + b1 * sl) reg_models[sl] = lfitted, afitted, beta_model, (x0, x1, y0, y1) return reg_models
def test_window_orthopoly(tmpdir): model1d = astmodels.Chebyshev1D(2, c0=2, c1=3, c2=0.5, domain=[-2, 2], window=[-0.5, 0.5]) model2d = astmodels.Chebyshev2D(1, 1, c0_0=1, c0_1=2, c1_0=3, x_domain=[-2, 2], y_domain=[-2, 2], x_window=[-0.5, 0.5], y_window=[-0.1, 0.5]) fa = AsdfFile() fa.tree['model1d'] = model1d fa.tree['model2d'] = model2d file_path = str(tmpdir.join('orthopoly_window.asdf')) fa.write_to(file_path) with asdf.open(file_path) as f: assert f.tree['model1d'](1.8) == model1d(1.8) assert f.tree['model2d'](1.8, -1.5) == model2d(1.8, -1.5)
def fit_poly_surface_2D(x_norm, y_norm, z, weights=None, polytype='chebyshev', poly_deg=5, timit=False, debug_level=0): """ Calculate 2D polynomial fit to normalized x and y values. Wrapper function for using the astropy fitting library. INPUT: 'x_norm' : x-values (pixels) of all the lines, re-normalized to [-1,+1] 'm_norm' : order numbers of all the lines, re-normalized to [-1,+1] 'z' : the 2-dim array of 'observed' values 'weights' : weights to use in the fitting 'polytype' : types of polynomials to use (either '(p)olynomial' (default), '(l)egendre', or '(c)hebyshev' are accepted) 'poly_deg' : degree of the polynomials 'timit' : boolean - do you want to measure execution run time? 'debug_level' : for debugging... OUTPUT: 'p' : coefficients of the best-fit polynomials """ if timit: start_time = time.time() if polytype.lower() in ['p', 'polynomial']: p_init = models.Polynomial2D(poly_deg) if debug_level > 0: print('OK, using standard polynomials...') elif polytype.lower() in ['c', 'chebyshev']: p_init = models.Chebyshev2D(poly_deg, poly_deg) if debug_level > 0: print('OK, using Chebyshev polynomials...') elif polytype.lower() in ['l', 'legendre']: p_init = models.Legendre2D(poly_deg, poly_deg) if debug_level > 0: print('OK, using Legendre polynomials...') else: print( "ERROR: polytype not recognised ['(P)olynomial' / '(C)hebyshev' / '(L)egendre']" ) return fit_p = fitting.LevMarLSQFitter() with warnings.catch_warnings(): # Ignore model linearity warning from the fitter warnings.simplefilter('ignore') p = fit_p(p_init, x_norm, y_norm, z, weights=weights) if timit: print('Time elapsed: ' + np.round(time.time() - start_time, 2).astype(str) + ' seconds...') return p
def test_chebyshev2D_nonlinear_fitting(self): cheb2d = models.Chebyshev2D(2, 2) cheb2d.parameters = np.arange(9) z = cheb2d(self.x, self.y) cheb2d.parameters = [0.1, .6, 1.8, 2.9, 3.7, 4.9, 6.7, 7.5, 8.9] nlfitter = LevMarLSQFitter() model = nlfitter(cheb2d, self.x, self.y, z) assert_allclose(model.parameters, [0, 1, 2, 3, 4, 5, 6, 7, 8], atol=10**-9)
def test_chebyshev2D_nonlinear_fitting(self): cheb2d = models.Chebyshev2D(2, 2) cheb2d.parameters = np.arange(9) z = cheb2d(self.x, self.y) cheb2d.parameters = [0.1, .6, 1.8, 2.9, 3.7, 4.9, 6.7, 7.5, 8.9] nlfitter = LevMarLSQFitter() with pytest.warns(AstropyUserWarning, match=r'Model is linear in parameters'): model = nlfitter(cheb2d, self.x, self.y, z) assert_allclose(model.parameters, [0, 1, 2, 3, 4, 5, 6, 7, 8], atol=10**-9)
def test_chebyshev2D_nonlinear_fitting_with_weights(self, fitter): fitter = fitter() cheb2d = models.Chebyshev2D(2, 2) cheb2d.parameters = np.arange(9) z = cheb2d(self.x, self.y) cheb2d.parameters = [0.1, .6, 1.8, 2.9, 3.7, 4.9, 6.7, 7.5, 8.9] weights = np.ones_like(self.y) with pytest.warns(AstropyUserWarning, match=r'Model is linear in parameters'): model = fitter(cheb2d, self.x, self.y, z, weights=weights) assert_allclose(model.parameters, [0, 1, 2, 3, 4, 5, 6, 7, 8], atol=10**-9)
def dict_to_chebyshev(model_dict): """ This is the inverse of chebyshev_to_dict(), taking a dict of property/ parameter names and their values and making a ChebyshevND model instance. Parameters ---------- model_dict: dict Dictionary with pair/value that defines the Chebyshev model. Returns ------- models.ChebyshevND or None Returns the models if it is parsed successfully. If not, it will return None. Examples -------- .. code-block:: python my_model = dict( zip( ad[0].WAVECAL['name'], ad[0].WAVECAL['coefficient'] ) ) """ try: ndim = int(model_dict.pop('ndim')) if ndim == 1: model = models.Chebyshev1D(degree=int(model_dict.pop('degree'))) elif ndim == 2: model = models.Chebyshev2D(x_degree=int(model_dict.pop('x_degree')), y_degree=int(model_dict.pop('y_degree'))) else: return None except KeyError: return None for k, v in model_dict.items(): try: if k.endswith('domain_start'): setattr(model, k.replace('_start', ''), [v, model_dict[k.replace('start', 'end')]]) elif k and not k.endswith('domain_end'): # ignore k=="" setattr(model, k, v) except (KeyError, AttributeError): return None return model
def fit_background(data, mask, npx=4, npy=4): """Fit a polynomial surface to all elements of a 2D `data` array where the corresponding `mask` is True. Return the fit evaluated at each point of the original data array. """ assert data.shape == mask.shape ny, nx = data.shape # y = np.arange(ny).reshape((ny,1)) # x = np.arange(nx).reshape((1,nx)) y, x = np.mgrid[:ny, :nx] p_init = models.Chebyshev2D(x_degree=npx, y_degree=npy) fit_p = fitting.LevMarLSQFitter() p = fit_p(p_init, x[mask], y[mask], data[mask]) return p(x, y)
def getmod(self): """ Return model for current attributes """ if self.type == 'poly': mod = models.Polynomial1D(degree=self.degree) elif self.type == 'chebyshev': mod = models.Chebyshev1D(degree=self.degree) elif self.type == 'chebyshev2D': sz = self.spectrum.data.shape mod = models.Chebyshev2D(x_degree=self.degree, y_degree=self.ydegree, x_domain=[0, sz[1]], y_domain=[0, sz[0]]) else: raise ValueError('unknown fitting type: ' + self.type) return return mod
def fit2d(self, deg=(6,3)): for i, p in enumerate(self.model.param_names): if self.ord_len == 1: cheb = models.Chebyshev1D(deg[0]) else: cheb = models.Chebyshev2D(deg[0], deg[1]) fit = fitting.LinearLSQFitter() chebfit = fit(cheb, self.x_step, self.ord_step, self.val_step[:, i]) self.val_full[:, i] = chebfit(self.x_full, self.ord_full) #print chebfit.__dict__#fit_info #print chebfit.c0_0 # print chebfit.coeffs # print chebfit._parameters # prova = chebfit.evaluate(self.x_full, self.ord_full, chebfit._parameters) if self.plots: plt.scatter(self.x_step, self.val_step[:, i], s=2) plt.scatter(self.x_full, self.val_full[:, i], s=2) # plt.scatter(self.x_full, prova, s=2) plt.show()
def setup_class(self): self.pmodel = models.Polynomial2D(2) self.y, self.x = np.mgrid[:5, :5] self.z = self.pmodel(self.x, self.y) self.cheb2 = models.Chebyshev2D(2, 2) self.fitter = LinearLSQFitter()
p4 = astmodels.Polynomial1D(1) m1 = p1 & p2 m2 = p3 & p4 m1.inverse = m2 return m1 test_models = [ astmodels.Identity(2), astmodels.Polynomial1D(2, c0=1, c1=2, c2=3), astmodels.Polynomial2D(1, c0_0=1, c0_1=2, c1_0=3), astmodels.Shift(2.), astmodels.Hermite1D(2, c0=2, c1=3, c2=0.5), astmodels.Legendre1D(2, c0=2, c1=3, c2=0.5), astmodels.Chebyshev1D(2, c0=2, c1=3, c2=0.5), astmodels.Chebyshev2D(1, 1, c0_0=1, c0_1=2, c1_0=3), astmodels.Legendre2D(1, 1, c0_0=1, c0_1=2, c1_0=3), astmodels.Hermite2D(1, 1, c0_0=1, c0_1=2, c1_0=3), astmodels.Scale(3.4), astmodels.RotateNative2Celestial(5.63, -72.5, 180), astmodels.Multiply(3), astmodels.Multiply(10 * u.m), astmodels.RotateCelestial2Native(5.63, -72.5, 180), astmodels.EulerAngleRotation(23, 14, 2.3, axes_order='xzx'), astmodels.Mapping((0, 1), n_inputs=3), astmodels.Shift(2. * u.deg), astmodels.Scale(3.4 * u.deg), astmodels.RotateNative2Celestial(5.63 * u.deg, -72.5 * u.deg, 180 * u.deg), astmodels.RotateCelestial2Native(5.63 * u.deg, -72.5 * u.deg, 180 * u.deg), astmodels.RotationSequence3D([1.2, 2.3, 3.4, .3], 'xyzx'), astmodels.SphericalRotationSequence([1.2, 2.3, 3.4, .3], 'xyzy'),
def makeSlitIllum(self, adinputs=None, **params): """ Makes the processed Slit Illumination Function by binning a 2D spectrum along the dispersion direction, fitting a smooth function for each bin, fitting a smooth 2D model, and reconstructing the 2D array using this last model. Its implementation based on the IRAF's `noao.twodspec.longslit.illumination` task following the algorithm described in [Valdes, 1968]. It expects an input calibration image to be an a dispersed image of the slit without illumination problems (e.g, twilight flat). The spectra is not required to be smooth in wavelength and may contain strong emission and absorption lines. The image should contain a `.mask` attribute in each extension, and it is expected to be overscan and bias corrected. Parameters ---------- adinputs : list List of AstroData objects containing the dispersed image of the slit of a source free of illumination problems. The data needs to have been overscan and bias corrected and is expected to have a Data Quality mask. bins : {None, int}, optional Total number of bins across the dispersion axis. If None, the number of bins will match the number of extensions on each input AstroData object. It it is an int, it will create N bins with the same size. border : int, optional Border size that is added on every edge of the slit illumination image before cutting it down to the input AstroData frame. smooth_order : int, optional Order of the spline that is used in each bin fitting to smooth the data (Default: 3) x_order : int, optional Order of the x-component in the Chebyshev2D model used to reconstruct the 2D data from the binned data. y_order : int, optional Order of the y-component in the Chebyshev2D model used to reconstruct the 2D data from the binned data. Return ------ List of AstroData : containing an AstroData with the Slit Illumination Response Function for each of the input object. References ---------- .. [Valdes, 1968] Francisco Valdes "Reduction Of Long Slit Spectra With IRAF", Proc. SPIE 0627, Instrumentation in Astronomy VI, (13 October 1986); https://doi.org/10.1117/12.968155 """ log = self.log log.debug(gt.log_message("primitive", self.myself(), "starting")) timestamp_key = self.timestamp_keys[self.myself()] suffix = params["suffix"] bins = params["bins"] border = params["border"] debug_plot = params["debug_plot"] smooth_order = params["smooth_order"] cheb2d_x_order = params["x_order"] cheb2d_y_order = params["y_order"] ad_outputs = [] for ad in adinputs: if len(ad) > 1 and "mosaic" not in ad[0].wcs.available_frames: log.info('Add "mosaic" gWCS frame to input data') geotable = import_module('.geometry_conf', self.inst_lookups) # deepcopy prevents modifying input `ad` inplace ad = transform.add_mosaic_wcs(deepcopy(ad), geotable) log.info("Temporarily mosaicking multi-extension file") mosaicked_ad = transform.resample_from_wcs( ad, "mosaic", attributes=None, order=1, process_objcat=False) else: log.info('Input data already has one extension and has a ' '"mosaic" frame.') # deepcopy prevents modifying input `ad` inplace mosaicked_ad = deepcopy(ad) log.info("Transposing data if needed") dispaxis = 2 - mosaicked_ad[0].dispersion_axis() # python sense should_transpose = dispaxis == 1 data, mask, variance = _transpose_if_needed( mosaicked_ad[0].data, mosaicked_ad[0].mask, mosaicked_ad[0].variance, transpose=should_transpose) log.info("Masking data") data = np.ma.masked_array(data, mask=mask) variance = np.ma.masked_array(variance, mask=mask) std = np.sqrt(variance) # Easier to work with log.info("Creating bins for data and variance") height = data.shape[0] width = data.shape[1] if bins is None: nbins = max(len(ad), 12) bin_limits = np.linspace(0, height, nbins + 1, dtype=int) elif isinstance(bins, int): nbins = bins bin_limits = np.linspace(0, height, nbins + 1, dtype=int) else: # ToDo: Handle input bins as array raise TypeError("Expected None or Int for `bins`. " "Found: {}".format(type(bins))) bin_top = bin_limits[1:] bin_bot = bin_limits[:-1] binned_data = np.zeros_like(data) binned_std = np.zeros_like(std) log.info("Smooth binned data and variance, and normalize them by " "smoothed central value") for bin_idx, (b0, b1) in enumerate(zip(bin_bot, bin_top)): rows = np.arange(width) avg_data = np.ma.mean(data[b0:b1], axis=0) model_1d_data = astromodels.UnivariateSplineWithOutlierRemoval( rows, avg_data, order=smooth_order) avg_std = np.ma.mean(std[b0:b1], axis=0) model_1d_std = astromodels.UnivariateSplineWithOutlierRemoval( rows, avg_std, order=smooth_order) slit_central_value = model_1d_data(rows)[width // 2] binned_data[b0:b1] = model_1d_data(rows) / slit_central_value binned_std[b0:b1] = model_1d_std(rows) / slit_central_value log.info("Reconstruct 2D mosaicked data") bin_center = np.array(0.5 * (bin_bot + bin_top), dtype=int) cols_fit, rows_fit = np.meshgrid(np.arange(width), bin_center) fitter = fitting.SLSQPLSQFitter() model_2d_init = models.Chebyshev2D(x_degree=cheb2d_x_order, x_domain=(0, width), y_degree=cheb2d_y_order, y_domain=(0, height)) model_2d_data = fitter(model_2d_init, cols_fit, rows_fit, binned_data[rows_fit, cols_fit]) model_2d_std = fitter(model_2d_init, cols_fit, rows_fit, binned_std[rows_fit, cols_fit]) rows_val, cols_val = \ np.mgrid[-border:height+border, -border:width+border] slit_response_data = model_2d_data(cols_val, rows_val) slit_response_mask = np.pad( mask, border, mode='edge') # ToDo: any update to the mask? slit_response_std = model_2d_std(cols_val, rows_val) slit_response_var = slit_response_std**2 del cols_fit, cols_val, rows_fit, rows_val _data, _mask, _variance = _transpose_if_needed( slit_response_data, slit_response_mask, slit_response_var, transpose=dispaxis == 1) log.info("Update slit response data and data_section") slit_response_ad = deepcopy(mosaicked_ad) slit_response_ad[0].data = _data slit_response_ad[0].mask = _mask slit_response_ad[0].variance = _variance if "mosaic" in ad[0].wcs.available_frames: log.info( "Map coordinates between slit function and mosaicked data" ) # ToDo: Improve message? slit_response_ad = _split_mosaic_into_extensions( ad, slit_response_ad, border_size=border) elif len(ad) == 1: log.info("Trim out borders") slit_response_ad[0].data = \ slit_response_ad[0].data[border:-border, border:-border] slit_response_ad[0].mask = \ slit_response_ad[0].mask[border:-border, border:-border] slit_response_ad[0].variance = \ slit_response_ad[0].variance[border:-border, border:-border] log.info("Update metadata and filename") gt.mark_history(slit_response_ad, primname=self.myself(), keyword=timestamp_key) slit_response_ad.update_filename(suffix=suffix, strip=True) ad_outputs.append(slit_response_ad) # Plotting ------ if debug_plot: log.info("Creating plots") palette = copy(plt.cm.cividis) palette.set_bad('r', 0.75) norm = vis.ImageNormalize(data[~data.mask], stretch=vis.LinearStretch(), interval=vis.PercentileInterval(97)) fig = plt.figure(num="Slit Response from MEF - {}".format( ad.filename), figsize=(12, 9), dpi=110) gs = gridspec.GridSpec(nrows=2, ncols=3, figure=fig) # Display raw mosaicked data and its bins --- ax1 = fig.add_subplot(gs[0, 0]) im1 = ax1.imshow(data, cmap=palette, origin='lower', vmin=norm.vmin, vmax=norm.vmax) ax1.set_title("Mosaicked Data\n and Spectral Bins", fontsize=10) ax1.set_xlim(-1, data.shape[1]) ax1.set_xticks([]) ax1.set_ylim(-1, data.shape[0]) ax1.set_yticks(bin_center) ax1.tick_params(axis=u'both', which=u'both', length=0) ax1.set_yticklabels( ["Bin {}".format(i) for i in range(len(bin_center))], fontsize=6) _ = [ax1.spines[s].set_visible(False) for s in ax1.spines] _ = [ax1.axhline(b, c='w', lw=0.5) for b in bin_limits] divider = make_axes_locatable(ax1) cax1 = divider.append_axes("right", size="5%", pad=0.05) plt.colorbar(im1, cax=cax1) # Display non-smoothed bins --- ax2 = fig.add_subplot(gs[0, 1]) im2 = ax2.imshow(binned_data, cmap=palette, origin='lower') ax2.set_title("Binned, smoothed\n and normalized data ", fontsize=10) ax2.set_xlim(0, data.shape[1]) ax2.set_xticks([]) ax2.set_ylim(0, data.shape[0]) ax2.set_yticks(bin_center) ax2.tick_params(axis=u'both', which=u'both', length=0) ax2.set_yticklabels( ["Bin {}".format(i) for i in range(len(bin_center))], fontsize=6) _ = [ax2.spines[s].set_visible(False) for s in ax2.spines] _ = [ax2.axhline(b, c='w', lw=0.5) for b in bin_limits] divider = make_axes_locatable(ax2) cax2 = divider.append_axes("right", size="5%", pad=0.05) plt.colorbar(im2, cax=cax2) # Display reconstructed slit response --- vmin = slit_response_data.min() vmax = slit_response_data.max() ax3 = fig.add_subplot(gs[1, 0]) im3 = ax3.imshow(slit_response_data, cmap=palette, origin='lower', vmin=vmin, vmax=vmax) ax3.set_title("Reconstructed\n Slit response", fontsize=10) ax3.set_xlim(0, data.shape[1]) ax3.set_xticks([]) ax3.set_ylim(0, data.shape[0]) ax3.set_yticks([]) ax3.tick_params(axis=u'both', which=u'both', length=0) _ = [ax3.spines[s].set_visible(False) for s in ax3.spines] divider = make_axes_locatable(ax3) cax3 = divider.append_axes("right", size="5%", pad=0.05) plt.colorbar(im3, cax=cax3) # Display extensions --- ax4 = fig.add_subplot(gs[1, 1]) ax4.set_xticks([]) ax4.set_yticks([]) _ = [ax4.spines[s].set_visible(False) for s in ax4.spines] sub_gs4 = gridspec.GridSpecFromSubplotSpec(nrows=len(ad), ncols=1, subplot_spec=gs[1, 1], hspace=0.03) # The [::-1] is needed to put the fist extension in the bottom for i, ext in enumerate(slit_response_ad[::-1]): ext_data, ext_mask, ext_variance = _transpose_if_needed( ext.data, ext.mask, ext.variance, transpose=dispaxis == 1) ext_data = np.ma.masked_array(ext_data, mask=ext_mask) sub_ax = fig.add_subplot(sub_gs4[i]) im4 = sub_ax.imshow(ext_data, origin="lower", vmin=vmin, vmax=vmax, cmap=palette) sub_ax.set_xlim(0, ext_data.shape[1]) sub_ax.set_xticks([]) sub_ax.set_ylim(0, ext_data.shape[0]) sub_ax.set_yticks([ext_data.shape[0] // 2]) sub_ax.set_yticklabels( ["Ext {}".format(len(slit_response_ad) - i - 1)], fontsize=6) _ = [ sub_ax.spines[s].set_visible(False) for s in sub_ax.spines ] if i == 0: sub_ax.set_title( "Multi-extension\n Slit Response Function") divider = make_axes_locatable(ax4) cax4 = divider.append_axes("right", size="5%", pad=0.05) plt.colorbar(im4, cax=cax4) # Display Signal-To-Noise Ratio --- snr = data / np.sqrt(variance) norm = vis.ImageNormalize(snr[~snr.mask], stretch=vis.LinearStretch(), interval=vis.PercentileInterval(97)) ax5 = fig.add_subplot(gs[0, 2]) im5 = ax5.imshow(snr, cmap=palette, origin='lower', vmin=norm.vmin, vmax=norm.vmax) ax5.set_title("Mosaicked Data SNR", fontsize=10) ax5.set_xlim(-1, data.shape[1]) ax5.set_xticks([]) ax5.set_ylim(-1, data.shape[0]) ax5.set_yticks(bin_center) ax5.tick_params(axis=u'both', which=u'both', length=0) ax5.set_yticklabels( ["Bin {}".format(i) for i in range(len(bin_center))], fontsize=6) _ = [ax5.spines[s].set_visible(False) for s in ax5.spines] _ = [ax5.axhline(b, c='w', lw=0.5) for b in bin_limits] divider = make_axes_locatable(ax5) cax5 = divider.append_axes("right", size="5%", pad=0.05) plt.colorbar(im5, cax=cax5) # Display Signal-To-Noise Ratio of Slit Illumination --- slit_response_snr = np.ma.masked_array( slit_response_data / np.sqrt(slit_response_var), mask=slit_response_mask) ax6 = fig.add_subplot(gs[1, 2]) im6 = ax6.imshow(slit_response_snr, origin="lower", vmin=norm.vmin, vmax=norm.vmax, cmap=palette) ax6.set_xlim(0, slit_response_snr.shape[1]) ax6.set_xticks([]) ax6.set_ylim(0, slit_response_snr.shape[0]) ax6.set_yticks([]) ax6.set_title("Reconstructed\n Slit Response SNR") _ = [ax6.spines[s].set_visible(False) for s in ax6.spines] divider = make_axes_locatable(ax6) cax6 = divider.append_axes("right", size="5%", pad=0.05) plt.colorbar(im6, cax=cax6) # Save plots --- fig.tight_layout(rect=[0, 0, 0.95, 1], pad=0.5) fname = slit_response_ad.filename.replace(".fits", ".png") log.info("Saving plots to {}".format(fname)) plt.savefig(fname) return ad_outputs
def test_round_trip_gwcs(tmpdir): """ Add a 2-step gWCS instance to NDAstroData, save to disk, reload & compare. """ from gwcs import coordinate_frames as cf from gwcs import WCS arr = np.zeros((10, 10), dtype=np.float32) ad1 = astrodata.create(fits.PrimaryHDU(), [fits.ImageHDU(arr, name='SCI')]) # Transformation from detector pixels to pixels in some reference row, # removing relative distortions in wavelength: det_frame = cf.Frame2D(name='det_mosaic', axes_names=('x', 'y'), unit=(u.pix, u.pix)) dref_frame = cf.Frame2D(name='dist_ref_row', axes_names=('xref', 'y'), unit=(u.pix, u.pix)) # A made-up example model that looks vaguely like some real distortions: fdist = models.Chebyshev2D(2, 2, c0_0=4.81125, c1_0=5.43375, c0_1=-0.135, c1_1=-0.405, c0_2=0.30375, c1_2=0.91125, x_domain=[0., 9.], y_domain=[0., 9.]) # This is not an accurate inverse, but will do for this test: idist = models.Chebyshev2D(2, 2, c0_0=4.89062675, c1_0=5.68581232, c2_0=-0.00590263, c0_1=0.11755526, c1_1=0.35652358, c2_1=-0.01193828, c0_2=-0.29996306, c1_2=-0.91823397, c2_2=0.02390594, x_domain=[-1.5, 12.], y_domain=[0., 9.]) # The resulting 2D co-ordinate mapping from detector to ref row pixels: distrans = models.Mapping((0, 1, 1)) | (fdist & models.Identity(1)) distrans.inverse = models.Mapping((0, 1, 1)) | (idist & models.Identity(1)) # Transformation from reference row pixels to linear, row-stacked spectra: spec_frame = cf.SpectralFrame(axes_order=(0, ), unit=u.nm, axes_names='lambda', name='wavelength') row_frame = cf.CoordinateFrame(1, 'SPATIAL', axes_order=(1, ), unit=u.pix, axes_names='y', name='row') rss_frame = cf.CompositeFrame([spec_frame, row_frame]) # Toy wavelength model & approximate inverse: fwcal = models.Chebyshev1D(2, c0=500.075, c1=0.05, c2=0.001, domain=[0, 9]) iwcal = models.Chebyshev1D(2, c0=4.59006292, c1=4.49601817, c2=-0.08989608, domain=[500.026, 500.126]) # The resulting 2D co-ordinate mapping from ref pixels to wavelength: wavtrans = fwcal & models.Identity(1) wavtrans.inverse = iwcal & models.Identity(1) # The complete WCS chain for these 2 transformation steps: ad1[0].nddata.wcs = WCS([(det_frame, distrans), (dref_frame, wavtrans), (rss_frame, None)]) # Save & re-load the AstroData instance with its new WCS attribute: testfile = str(tmpdir.join('round_trip_gwcs.fits')) ad1.write(testfile) ad2 = astrodata.open(testfile) wcs1 = ad1[0].nddata.wcs wcs2 = ad2[0].nddata.wcs # # Temporary workaround for issue #9809, to ensure the test is correct: # wcs2.forward_transform[1].x_domain = (0, 9) # wcs2.forward_transform[1].y_domain = (0, 9) # wcs2.forward_transform[3].domain = (0, 9) # wcs2.backward_transform[0].domain = (500.026, 500.126) # wcs2.backward_transform[3].x_domain = (-1.5, 12.) # wcs2.backward_transform[3].y_domain = (0, 9) # Did we actually get a gWCS instance back? assert isinstance(wcs2, WCS) # Do the transforms have the same number of submodels, with the same types, # degrees, domains & parameters? Here the inverse gets checked redundantly # as both backward_transform and forward_transform.inverse, but it would be # convoluted to ensure that both are correct otherwise (since the transforms # get regenerated as new compound models each time they are accessed). compare_models(wcs1.forward_transform, wcs2.forward_transform) compare_models(wcs1.backward_transform, wcs2.backward_transform) # Do the instances have matching co-ordinate frames? for f in wcs1.available_frames: assert repr(getattr(wcs1, f)) == repr(getattr(wcs2, f)) # Also compare a few transformed values, as the "proof of the pudding": y, x = np.mgrid[0:9:2, 0:9:2] np.testing.assert_allclose(wcs1(x, y), wcs2(x, y), rtol=1e-7, atol=0.) y, w = np.mgrid[0:9:2, 500.025:500.12:0.0225] np.testing.assert_allclose(wcs1.invert(w, y), wcs2.invert(w, y), rtol=1e-7, atol=0.)
astropy.modeling.math_functions.True_divideUfunc(), # astropy.modeling.physical_models astropy_models.BlackBody(scale=10.0, temperature=6000. * u.K), astropy_models.Drude1D(amplitude=10.0, x_0=0.5, fwhm=2.5), # TODO: NFW # astropy.modeling.polynomial astropy_models.Chebyshev1D(2, c0=2, c1=3, c2=0.5), astropy_models.Chebyshev1D(2, c0=2, c1=3, c2=0.5, domain=(0.0, 1.0), window=(1.5, 2.5)), astropy_models.Chebyshev2D(1, 1, c0_0=1, c0_1=2, c1_0=3), astropy_models.Chebyshev2D(1, 1, c0_0=1, c0_1=2, c1_0=3, x_domain=(1.0, 2.0), y_domain=(3.0, 4.0), x_window=(5.0, 6.0), y_window=(7.0, 8.0)), astropy_models.Hermite1D(2, c0=2, c1=3, c2=0.5), astropy_models.Hermite2D(1, 1, c0_0=1, c0_1=2, c1_0=3), astropy_models.Legendre1D(2, c0=2, c1=3, c2=0.5), astropy_models.Legendre2D(1, 1, c0_0=1, c0_1=2, c1_0=3), astropy_models.Polynomial1D(2, c0=1, c1=2, c2=3), astropy_models.Polynomial2D(1, c0_0=1, c0_1=2, c1_0=3),