def first_cc_val_neg(param, *args): center_x, center_y = param data = args[0] radius_size = args[1] ones = np.array([[1] * 600] * 600) center_ap = CircularAperture([center_x, center_y], radius_size) center_area = center_ap.area center_mask = center_ap.to_mask(method='exact') center_data = center_mask.multiply(data) center_weights = center_mask.multiply(ones) center_std = twoD_weighted_std(center_data, center_weights) center_val = (np.sum(center_data)) / center_area + 5 * center_std first_ap = CircularAnnulus([center_x, center_y], r_in=radius_size, r_out=2 * radius_size) first_area = first_ap.area first_mask = first_ap.to_mask(method='exact') first_data = first_mask.multiply(data) first_weights = first_mask.multiply(ones) first_std = twoD_weighted_std(first_data, first_weights) first_val = (np.sum(first_data)) / first_area + 5 * first_std result = (-2.5 * math.log(first_val / center_val, 10)) return -1 * (result)
def compute_circular_aperture(self, centre, radius, flux_return=False, plot=False): from photutils.aperture import CircularAperture#, aperture_photometry aperture = CircularAperture(centre, r=radius) aperture_mask = aperture.to_mask() aperture_mask = np.array( aperture_mask.to_image(self.wcs.celestial.array_shape), dtype=bool) if flux_return: integrated_flux = np.nansum(self.flux[:, aperture_mask], axis=1) integrated_no_cov = np.nansum(self.flux_error[:, aperture_mask]**2, axis=1) ## Accounting for covariance errors if aperture_mask[aperture_mask].size<100: integrated_flux_cov = integrated_no_cov*( 1+1.62*np.log10(aperture_mask[aperture_mask].size)) integrated_flux_err = np.sqrt(integrated_flux_cov) else: integrated_flux_cov = integrated_no_cov*4.2 integrated_flux_err = np.sqrt(integrated_flux_cov) else: integrated_flux = None integrated_flux_err = None if plot: fig = plt.figure() ax = fig.add_subplot(111) ax.imshow(self.flux[self.wl.size//2, :, :], cmap='gist_earth_r') aperture.plot(lw=1, color='r') ax.annotate(r'$R_e={:4.3}~(Kpc)$'.format(self.eff_radius_physical), xy=(.9,.95), xycoords='axes fraction', va='top', ha='right') ax.annotate(r'$R_e={:4.3}~(arcsec)$'.format(self.eff_radius), xy=(.9,.9), xycoords='axes fraction', va='top', ha='right') aperture.plot(lw=1, color='r') ax.annotate(r'$R_e={:4.3}~(pix)$'.format(self.eff_radius_pix), xy=(.9,.85), xycoords='axes fraction', va='top', ha='right') ax.annotate(r'$R_a={:4.3}~(pix)$'.format(radius), xy=(.9,.80), xycoords='axes fraction', va='top', ha='right', color='r') aperture.plot(lw=1, color='r') # plt.savefig('bpt_apertures/'+name_i+'.png') plt.show() plt.close() return aperture, aperture_mask, integrated_flux, integrated_flux_err, fig else: return aperture, aperture_mask, integrated_flux, integrated_flux_err
def ConCur(star_data, radius_size=1, center=None, background_method='astropy', find_hots=False, find_center=False): data = star_data.copy() background_mean, background_std = background_calc(data, background_method) x, y = np.indices((data.shape)) if not center: center = np.array([(x.max() - x.min()) / 2.0, (y.max() - y.min()) / 2.0]) if find_hots == True: hots = hot_pixels(data, center, background_mean, background_std) if find_center == True: center_vals = find_best_center(data, radius_size, center) center = np.array([center_vals[0], center_vals[1]]) radii = np.sqrt((x - center[0])**2 + (y - center[1])**2) radii = radii.astype(np.int) ones = np.array([[1] * len(data)] * len(data[0])) number_of_a = radii.max() / radius_size center_ap = CircularAperture([center[0], center[1]], radius_size) all_apers, all_apers_areas, all_masks = [center_ap], [center_ap.area], [ center_ap.to_mask(method='exact') ] all_data, all_weights = [all_masks[0].multiply(data) ], [all_masks[0].multiply(ones)] all_stds = [twoD_weighted_std(all_data[0], all_weights[0])] for j in range(int(number_of_a)): aper = CircularAnnulus([center[0], center[1]], r_in=(j * radius_size + radius_size), r_out=(j * radius_size + 2 * radius_size)) all_apers.append(aper) all_apers_areas.append(aper.area) mask = aper.to_mask(method='exact') all_masks.append(mask) mask_data = mask.multiply(data) mask_weight = mask.multiply(ones) all_data.append(mask_data) all_weights.append(mask_weight) all_stds.append(twoD_weighted_std(mask_data, mask_weight)) phot_table = aperture_photometry(data, all_apers) center_val = np.sum(all_data[0]) / all_apers_areas[0] + 5 * all_stds[0] delta_mags = [] for i in range(len(phot_table[0]) - 3): try: delta_mags.append(-2.5 * math.log((np.sum(all_data[i])/all_apers_areas[i] + \ 5*all_stds[i])/center_val,10)) except ValueError: print('annulus',i, 'relative flux equal to', (np.sum(all_data[i])/all_apers_areas[i] + \ 5*all_stds[i])/center_val, '...it is not included') delta_mags.append(np.NaN) arc_lengths = [] for i in range(len(delta_mags)): arc_lengths.append( (i * 0.033 + 0.033) * radius_size) #make sure center radius size is correct arc_lengths = np.array(arc_lengths) lim_arc_lengths = arc_lengths[arc_lengths < 10] delta_mags = delta_mags[:len(lim_arc_lengths)] delta_mags = np.array(delta_mags) if delta_mags[1] < 0: print('Warning: first annulus has negative relative flux of value,', '%.5f' % delta_mags[1], 'consider changing center or radius size') return (lim_arc_lengths, delta_mags, all_stds)
class RadialProfile: """Main function to calulate radial profiles Computes a radial profile of a source in an array. This function leverages some of the tools in photutils to cutout the small region around the source. This function can first recenter the source via a 2d Gaussian fit (radial profiles are sensitive to centroids) and then fit a 1D Moffat profile to the values. The profile is calculated by computing the distance from the center of each pixel within a box of size r to the centroid of the source in the box. Additionally, the profile and the fit can be plotted. If fit is set to True, then the profile is fit with a 1D Moffat. If show is set to True, then profile (and/or fit) is plotted. If an axes object is provided, the plot(s) will be on that object. NOTE: THE POSITIONS ARE 0 INDEXED (bottom left corner pixel center is set to (0,0)). Parameters ---------- x : float The x position of the centroid of the source. ZERO INDEXED. stored in the .x attribute y : float The y position of the centroid of the source. ZERO INDEXED. .x attribute data : array A 2D array containing the full image data. A small box is cut out of this array for the radial profile r : float, optional The size of the box used to cut out the source pixels. The box is typically square with side length ~ 2*r + 1. Default is 5 pix. fit: bool Fit a 1D Moffat profile? Default True. Required for computation of FWHM. recenter : bool, optional Compute new centroid via 2D Gaussian fit? Default False. show : bool, optional Plot the profile? Default False. See ax parameter for info. ax : matplotlib.axes.Axes, optional Axes object to make the plots on. Default None. If None and show is True, an axes object will be created. Attributes ---------- x : float The x position in pixels of the source centroid. Gets updated if the profile is recentered. y : float The y position in pixels of the source centroid. Gets updated if the profile is recentered. fwhm : float The FWHM of the fitted profile, only computed if fit=True old_x : float The x position in pixels of the original input centroid. Only set if recenter = True old_y : float The y position in pixels of the original input centroid. Only set if recenter = True fitted : bool Whether the data has had a profile fit. Only True if fit=True and fitting was successful is_empty : bool Whether cutout is empty or not. True if position falls entirely off of data. cutout : array 2D array containing small cutout of data around source distances : array Array containing distance to each pixel in cutout from centroid value : array Array containing all the values in the cutout """ def __init__(self, x, y, data, r=5, fit=True, recenter=False, show=False, ax=None): self.x = x # X position self.y = y # Y Position self.r = r # radius (acutally makes a box) self.is_empty = False # if gets set True, cutout is empty self._setup_cutout(data) # Make the cutout if recenter: self.recenter_source(data) # recalculates centroid self.fit = fit self.fitted = False # Initial state, set to true if fit success if self.is_empty: self.fwhm = np.nan else: self._create_profile() # creates distances and values arrays if fit: self.fit_profile() # performs fit, updates self.fitted if show: self.show_profile(ax) def _create_profile(self): """Compute distances to pixels in cutout""" iY, iX = np.mgrid[self.sy, self.sx] # Pixel grid indices # extent = [sx.start, sx.stop-1, sy.start, sy.stop-1] self.distances = np.sqrt((iX - self.x)**2. + (iY - self.y)**2.).flatten() self.values = self.cutout.flatten() def _setup_cutout(self, data): """Cuts out the aperture and defines slice objects. General setup procedure. """ self.ap = CircularAperture((self.x, self.y), r=self.r) mask = self.ap.to_mask()[0] self.sy = mask.bbox.slices[0] self.sx = mask.bbox.slices[1] self.cutout = mask.cutout(data, fill_value=np.nan) if self.cutout is None: self.is_empty = True def fit_profile(self): """Fits 1d Moffat function to measured radial profile. Fits a moffat profile to the distance and values of the pixels. Further development may allow user defined models. """ try: amp0 = np.amax(self.values) bias0 = np.nanmedian(self.values) best_vals, covar = curve_fit(RadialProfile.profile_model, self.distances, self.values, p0=[amp0, 1.5, 1.5, bias0], bounds=([0., .3, .5, 0], [np.inf, 10., 10., np.inf])) hwhm = best_vals[1] * np.sqrt(2.**(1. / best_vals[2]) - 1.) self.fwhm = 2 * hwhm self.amp, self.gamma, self.alpha, self.bias = best_vals self.fitted = True mod = RadialProfile.profile_model(self.distances, *best_vals) self.chisquared = chisquare(self.values, mod, ddof=4)[0] except Exception as e: print(e) self.amp, self.gamma, self.alpha, self.bias = [np.nan] * 4 self.fwhm = np.nan self.fitted = False self.chisquared = np.nan @staticmethod def profile_model(r, amp, gamma, alpha, bias): """Returns 1D Moffat profile evaluated at r values. This function takes radius values and parameters in a simple 1D moffat profiles and returns the values of the profile at those radius values. The model is defined as: model = amp * (1. + (r / gamma) ** 2.) ** (-1. * alpha) + bias Parameters ---------- r : array The distances at which to sample the model amp : float The amplitude of the of the model gamma: float The width of the profile. alpha: float The decay of the profile. bias: float The bias level (piston term) of the data. This is like a background value. Returns ------- model : array The values of the model sampled at the r values. """ model = amp * (1. + (r / gamma)**2.)**(-1. * alpha) + bias return model def recenter_source(self, data): """Recenters source position in cutout and updates x,y attributes""" # Archive old positions. self.old_x = self.x self.old_y = self.y if self.is_empty: self.x, self.y = np.nan, np.nan else: # Fit 2D gaussian xg1, yg1 = centroid_2dg(self.cutout) dx = xg1 + self.sx.start - self.x dy = yg1 + self.sy.start - self.y dr = (dx**2. + dy**2.)**.5 if dr > 2.: print('Large shift of {},{} computed.'.format(dx, dy)) print('Rejecting and keeping original x, y coordinates') else: self.x = xg1 + self.sx.start self.y = yg1 + self.sy.start self._setup_cutout(data) def show_profile(self, ax=None, show_fit=True): """Makes plot of radial profile Plots the radial profile, that is pixel distance vs pixel value. Can plot on an existing axes object if the an axes object is passed in via the ax parameter. The function attempts to set sensible axes limits, specifically half of the smallest positive value (axes are logarithmic). The axes object is returned by this, so that parameters can be set by the user later. Parameters ---------- ax : matplotlib.axes.Axes, optional An axes object to plot the radial profile on (for integrating) the plot into other figures. If not set, the script will create an axes object. show_fit : bool, optional Plot the fitted model. Only done if fit was successful. Returns ------- ax : matplotlib.axes.Axes The axes object containing the radial profile plot/ """ if ax is None: fig = plt.figure() ax = fig.add_subplot(111) ax.scatter(self.distances, self.values, alpha=.5) min_y = np.amin(self.values[self.values > 0.]) / 2. ax.set_ylim(min_y, np.nanmax(self.values) * 2.) ax.set_xlim(0.) ax.set_yscale('log') ax.set_ylabel('Pixel Value') ax.set_xlabel('Distance from centroid [pix]') if self.fitted and show_fit: tmp_r = np.arange(0, np.ceil(np.amax(self.distances)), .1) model_fit = RadialProfile.profile_model(tmp_r, self.amp, self.gamma, self.alpha, self.bias) label = r'$\gamma$= {}, $\alpha$ = {}'.format( round(self.gamma, 2), round(self.alpha, 2)) label += '\nFWHM = {}'.format(round(self.fwhm, 2)) ax.plot(tmp_r, model_fit, label=label) ax.legend(loc=1) return ax