def _hillclimb_loss(x, IMG, PSF, noise): center_loss = 0 for rr in range(3): RR = (rr + 1.0) * PSF / 2 isovals = _iso_extract( IMG, RR, { "ellip": 0.0, "pa": 0.0 }, { "x": np.clip(x[0], a_min=np.ceil(3 + RR), a_max=np.floor(IMG.shape[1] - 4 - RR)), "y": np.clip(x[1], a_min=np.ceil(3 + RR), a_max=np.floor(IMG.shape[0] - 4 - RR)), }, more=False, rad_interp=10 * PSF, interp_method="lanczos", interp_window=3, ) coefs = fft(isovals) center_loss += np.abs( coefs[1]) / (len(isovals) * (max(0, np.median(isovals)) + noise)) return center_loss
def _hillclimb_mean_loss(x, IMG, PSF, noise): center_loss = 0 for rr in range(3): isovals = _iso_extract(IMG,(rr+0.5)*PSF,0., 0.,{'x': x[0], 'y': x[1]}, more = False, rad_interp = 10*PSF) coefs = fft(isovals) center_loss += np.abs(coefs[1])/(len(isovals)*(max(0,np.mean(isovals))+noise)) return center_loss
def _fitEllip_loss(e, dat, r, p, c, n): isovals = _iso_extract(dat, r, e, p, c, sigmaclip=True, sclip_nsigma=3, interp_mask=True) coefs = fft(isovals) return np.abs(coefs[2]) / (len(isovals) * (max(0, np.median(isovals)) + n))
def _fitEllip_loss(e, dat, r, p, c, n, m): isovals = _iso_extract( dat, r, { "ellip": e, "pa": p }, c, sigmaclip=True, sclip_nsigma=3, mask=m, interp_mask=True, ) coefs = fft(np.clip(isovals, a_max=np.quantile(isovals, 0.85), a_min=None)) return (iqr(isovals, rng=[16, 84]) / 2 + np.abs(coefs[2]) / len(isovals)) / (max(0, np.median(isovals)) + n)
def _FFT_mean_loss(dat, R, E, PA, i, C, noise, mask=None, reg_scale=1., name=''): isovals = _iso_extract(dat, R[i], E[i], PA[i], C, mask=mask, interp_mask=False if mask is None else True) if not np.all(np.isfinite(isovals)): logging.warning( 'Failed to evaluate isophotal flux values, skipping this ellip/pa combination' ) return np.inf coefs = fft(isovals) f2_loss = np.abs(coefs[2]) / (len(isovals) * (max(0, np.mean(isovals)) + noise)) reg_loss = 0 if i < (len(R) - 1): reg_loss += abs( (E[i] - E[i + 1]) / (1 - E[i + 1]) ) #abs((_inv_x_to_eps(E[i]) - _inv_x_to_eps(E[i+1]))/0.1) reg_loss += abs(Angle_TwoAngles(2 * PA[i], 2 * PA[i + 1]) / (2 * 0.3)) if i > 0: reg_loss += abs( (E[i] - E[i - 1]) / (1 - E[i - 1]) ) #abs((_inv_x_to_eps(E[i]) - _inv_x_to_eps(E[i-1]))/0.1) reg_loss += abs(Angle_TwoAngles(2 * PA[i], 2 * PA[i - 1]) / (2 * 0.3)) return f2_loss * ( 1 + reg_loss * reg_scale ) #(np.abs(coefs[2])/(len(isovals)*(abs(np.median(isovals)))))*reg_loss*reg_scale
def _FFT_mean_loss(dat, R, E, PA, i, C, noise, mask=None, reg_scale=1.0, name=""): isovals = _iso_extract( dat, R[i], { "ellip": E[i], "pa": PA[i] }, C, mask=mask, interp_mask=False if mask is None else True, ) if not np.all(np.isfinite(isovals)): logging.warning( "Failed to evaluate isophotal flux values, skipping this ellip/pa combination" ) return np.inf coefs = fft(isovals) f2_loss = np.abs(coefs[2]) / (len(isovals) * (max(0, np.mean(isovals)) + noise)) reg_loss = 0 if i < (len(R) - 1): reg_loss += abs((E[i] - E[i + 1]) / (1 - E[i + 1])) reg_loss += abs(Angle_TwoAngles_sin(PA[i], PA[i + 1]) / (0.3)) if i > 0: reg_loss += abs((E[i] - E[i - 1]) / (1 - E[i - 1])) reg_loss += abs(Angle_TwoAngles_sin(PA[i], PA[i - 1]) / (0.3)) return f2_loss * (1 + reg_loss * reg_scale)
def _hillclimb_mean_loss(x, IMG, PSF, noise): center_loss = 0 for rr in range(3): isovals = _iso_extract( IMG, (rr + 0.5) * PSF, { "ellip": 0.0, "pa": 0.0 }, { "x": x[0], "y": x[1] }, more=False, rad_interp=10 * PSF, ) coefs = fft(isovals) center_loss += np.abs(coefs[1]) / (len(isovals) * (max(noise, np.mean(isovals)))) return center_loss
def _FFT_Robust_loss(dat, R, E, PA, i, C, noise, mask=None, reg_scale=1., name=''): isovals = _iso_extract(dat, R[i], E[i], PA[i], C, mask=mask, interp_mask=False if mask is None else True) if mask is None: coefs = fft( np.clip(isovals, a_max=np.quantile(isovals, 0.85), a_min=None)) else: coefs = fft( np.clip(isovals, a_max=np.quantile(isovals, 0.9), a_min=None)) f2_loss = np.abs(coefs[2]) / (len(isovals) * (max(0, np.median(isovals)) + noise)) reg_loss = 0 if i < (len(R) - 1): reg_loss += abs((E[i] - E[i + 1]) / (1 - E[i + 1])) reg_loss += abs(Angle_TwoAngles(2 * PA[i], 2 * PA[i + 1]) / (2 * 0.2)) if i > 0: reg_loss += abs((E[i] - E[i - 1]) / (1 - E[i - 1])) reg_loss += abs(Angle_TwoAngles(2 * PA[i], 2 * PA[i - 1]) / (2 * 0.2)) return f2_loss * (1 + reg_loss * reg_scale)
def _Generate_Profile(IMG, results, R, E, Ee, PA, PAe, options): # Create image array with background and mask applied try: if np.any(results['mask']): mask = results['mask'] else: mask = None except: mask = None dat = IMG - results['background'] zeropoint = options['ap_zeropoint'] if 'ap_zeropoint' in options else 22.5 sb = [] sbE = [] cogdirect = [] sbfix = [] sbfixE = [] count_neg = 0 medflux = np.inf end_prof = None for i in range(len(R)): isobandwidth = R[i] * (options['ap_isoband_width'] if 'ap_isoband_width' in options else 0.025) if medflux > (results['background noise'] * (options['ap_isoband_start'] if 'ap_isoband_start' in options else 2)) or isobandwidth < 0.5: isovals = _iso_extract( dat, R[i], E[i], PA[i], results['center'], mask=mask, rad_interp=(options['ap_iso_interpolate_start'] if 'ap_iso_interpolate_start' in options else 5) * results['psf fwhm'], sigmaclip=options['ap_isoclip'] if 'ap_isoclip' in options else False, sclip_iterations=options['ap_isoclip_iterations'] if 'ap_isoclip_iterations' in options else 10, sclip_nsigma=options['ap_isoclip_nsigma'] if 'ap_isoclip_nsigma' in options else 5) isovalsfix = _iso_extract( dat, R[i], results['init ellip'], results['init pa'], results['center'], mask=mask, rad_interp=(options['ap_iso_interpolate_start'] if 'ap_iso_interpolate_start' in options else 5) * results['psf fwhm'], sigmaclip=options['ap_isoclip'] if 'ap_isoclip' in options else False, sclip_iterations=options['ap_isoclip_iterations'] if 'ap_isoclip_iterations' in options else 10, sclip_nsigma=options['ap_isoclip_nsigma'] if 'ap_isoclip_nsigma' in options else 5) else: isovals = _iso_between( dat, R[i] - isobandwidth, R[i] + isobandwidth, E[i], PA[i], results['center'], mask=mask, sigmaclip=options['ap_isoclip'] if 'ap_isoclip' in options else False, sclip_iterations=options['ap_isoclip_iterations'] if 'ap_isoclip_iterations' in options else 10, sclip_nsigma=options['ap_isoclip_nsigma'] if 'ap_isoclip_nsigma' in options else 5) isovalsfix = _iso_between( dat, R[i] - isobandwidth, R[i] + isobandwidth, results['init ellip'], results['init pa'], results['center'], mask=mask, sigmaclip=options['ap_isoclip'] if 'ap_isoclip' in options else False, sclip_iterations=options['ap_isoclip_iterations'] if 'ap_isoclip_iterations' in options else 10, sclip_nsigma=options['ap_isoclip_nsigma'] if 'ap_isoclip_nsigma' in options else 5) isotot = np.sum( _iso_between(dat, 0, R[i], E[i], PA[i], results['center'], mask=mask)) medflux = _average( isovals, options['ap_isoaverage_method'] if 'ap_isoaverage_method' in options else 'median') scatflux = _scatter( isovals, options['ap_isoaverage_method'] if 'ap_isoaverage_method' in options else 'median') medfluxfix = _average( isovalsfix, options['ap_isoaverage_method'] if 'ap_isoaverage_method' in options else 'median') scatfluxfix = _scatter( isovalsfix, options['ap_isoaverage_method'] if 'ap_isoaverage_method' in options else 'median') sb.append( flux_to_sb(medflux, options['ap_pixscale'], zeropoint ) if medflux > 0 else 99.999) sbE.append((2.5 * scatflux / (np.sqrt(len(isovals)) * medflux * np.log(10))) if medflux > 0 else 99.999) sbfix.append( flux_to_sb(medfluxfix, options['ap_pixscale'], zeropoint ) if medfluxfix > 0 else 99.999) sbfixE.append((2.5 * scatfluxfix / (np.sqrt(len(isovalsfix)) * np.median(isovalsfix) * np.log(10))) if medfluxfix > 0 else 99.999) cogdirect.append( flux_to_mag(isotot, zeropoint) if isotot > 0 else 99.999) if medflux <= 0: count_neg += 1 if 'ap_truncate_evaluation' in options and options[ 'ap_truncate_evaluation'] and count_neg >= 2: end_prof = i + 1 break # Compute Curve of Growth from SB profile cog, cogE = SBprof_to_COG_errorprop(R[:end_prof] * options['ap_pixscale'], np.array(sb), np.array(sbE), 1. - E[:end_prof], Ee[:end_prof], N=100, method=0, symmetric_error=True) cogE[cog > 99] = 99.999 cogfix, cogfixE = SBprof_to_COG_errorprop(R[:end_prof] * options['ap_pixscale'], np.array(sbfix), np.array(sbfixE), 1. - E[:end_prof], Ee[:end_prof], N=100, method=0, symmetric_error=True) cogfixE[cogfix > 99] = 99.999 # For each radius evaluation, write the profile parameters params = [ 'R', 'SB', 'SB_e', 'totmag', 'totmag_e', 'ellip', 'ellip_e', 'pa', 'pa_e', 'totmag_direct', 'SB_fix', 'SB_fix_e', 'totmag_fix', 'totmag_fix_e' ] SBprof_data = dict((h, None) for h in params) SBprof_units = { 'R': 'arcsec', 'SB': 'mag*arcsec^-2', 'SB_e': 'mag*arcsec^-2', 'totmag': 'mag', 'totmag_e': 'mag', 'ellip': 'unitless', 'ellip_e': 'unitless', 'pa': 'deg', 'pa_e': 'deg', 'totmag_direct': 'mag', 'SB_fix': 'mag*arcsec^-2', 'SB_fix_e': 'mag*arcsec^-2', 'totmag_fix': 'mag', 'totmag_fix_e': 'mag' } SBprof_format = { 'R': '%.4f', 'SB': '%.4f', 'SB_e': '%.4f', 'totmag': '%.4f', 'totmag_e': '%.4f', 'ellip': '%.3f', 'ellip_e': '%.3f', 'pa': '%.2f', 'pa_e': '%.2f', 'totmag_direct': '%.4f', 'SB_fix': '%.4f', 'SB_fix_e': '%.4f', 'totmag_fix': '%.4f', 'totmag_fix_e': '%.4f' } SBprof_data['R'] = list(R[:end_prof] * options['ap_pixscale']) SBprof_data['SB'] = list(sb) SBprof_data['SB_e'] = list(sbE) SBprof_data['totmag'] = list(cog) SBprof_data['totmag_e'] = list(cogE) SBprof_data['ellip'] = list(E[:end_prof]) SBprof_data['ellip_e'] = list(Ee[:end_prof]) SBprof_data['pa'] = list(PA[:end_prof] * 180 / np.pi) SBprof_data['pa_e'] = list(PAe[:end_prof] * 180 / np.pi) SBprof_data['totmag_direct'] = list(cogdirect) SBprof_data['SB_fix'] = list(sbfix) SBprof_data['SB_fix_e'] = list(sbfixE) SBprof_data['totmag_fix'] = list(cogfix) SBprof_data['totmag_fix_e'] = list(cogfixE) if 'ap_doplot' in options and options['ap_doplot']: CHOOSE = np.logical_and( np.array(SBprof_data['SB']) < 99, np.array(SBprof_data['SB_e']) < 1) errscale = 1. if np.all(np.array(SBprof_data['SB_e'])[CHOOSE] < 0.5): errscale = 1 / np.max(np.array(SBprof_data['SB_e'])[CHOOSE]) lnlist = [] lnlist.append( plt.errorbar(np.array(SBprof_data['R'])[CHOOSE], np.array(SBprof_data['SB'])[CHOOSE], yerr=errscale * np.array(SBprof_data['SB_e'])[CHOOSE], elinewidth=1, linewidth=0, marker='.', markersize=5, color='r', label='Surface Brightness (err$\\cdot$%.1f)' % errscale)) plt.errorbar(np.array(SBprof_data['R'])[np.logical_and( CHOOSE, np.arange(len(CHOOSE)) % 4 == 0)], np.array(SBprof_data['SB'])[np.logical_and( CHOOSE, np.arange(len(CHOOSE)) % 4 == 0)], yerr=np.array(SBprof_data['SB_e'])[np.logical_and( CHOOSE, np.arange(len(CHOOSE)) % 4 == 0)], elinewidth=1, linewidth=0, marker='.', markersize=5, color='limegreen') # plt.errorbar(np.array(SBprof_data['R'])[CHOOSE], np.array(SBprof_data['totmag'])[CHOOSE], yerr = np.array(SBprof_data['totmag_e'])[CHOOSE], # elinewidth = 1, linewidth = 0, marker = '.', markersize = 5, color = 'orange', label = 'Curve of Growth') plt.xlabel('Semi-Major-Axis [arcsec]', fontsize=16) plt.ylabel('Surface Brightness [mag arcsec$^{-2}$]', fontsize=16) bkgrdnoise = -2.5 * np.log10( results['background noise']) + zeropoint + 2.5 * np.log10( options['ap_pixscale']**2) lnlist.append( plt.axhline( bkgrdnoise, color='purple', linewidth=0.5, linestyle='--', label='1$\\sigma$ noise/pixel: %.1f mag arcsec$^{-2}$' % bkgrdnoise)) plt.gca().invert_yaxis() plt.tick_params(labelsize=14) # ax2 = plt.gca().twinx() # lnlist += ax2.plot(np.array(SBprof_data['R'])[CHOOSE], np.array(SBprof_data['pa'])[CHOOSE]/180, color = 'b', label = 'PA/180') # lnlist += ax2.plot(np.array(SBprof_data['R'])[CHOOSE], np.array(SBprof_data['ellip'])[CHOOSE], color = 'orange', linestyle = '--', label = 'ellipticity') labs = [l.get_label() for l in lnlist] plt.legend(lnlist, labs, fontsize=11) # ax2.set_ylabel('Position Angle, Ellipticity', fontsize = 16) # ax2.tick_params(labelsize = 14) plt.tight_layout() if not ('ap_nologo' in options and options['ap_nologo']): AddLogo(plt.gcf()) plt.savefig( '%sphotometry_%s.jpg' % (options['ap_plotpath'] if 'ap_plotpath' in options else '', options['ap_name']), dpi=options['ap_plotdpi'] if 'ap_plotdpi' in options else 300) plt.close() useR = np.array(SBprof_data['R'])[CHOOSE] / options['ap_pixscale'] useE = np.array(SBprof_data['ellip'])[CHOOSE] usePA = np.array(SBprof_data['pa'])[CHOOSE] ranges = [[ max(0, int(results['center']['x'] - useR[-1] * 1.2)), min(dat.shape[1], int(results['center']['x'] + useR[-1] * 1.2)) ], [ max(0, int(results['center']['y'] - useR[-1] * 1.2)), min(dat.shape[0], int(results['center']['y'] + useR[-1] * 1.2)) ]] LSBImage(dat[ranges[1][0]:ranges[1][1], ranges[0][0]:ranges[0][1]], results['background noise']) for i in range(len(useR)): plt.gca().add_patch( Ellipse( (results['center']['x'] - ranges[0][0], results['center']['y'] - ranges[1][0]), 2 * useR[i], 2 * useR[i] * (1. - useE[i]), usePA[i], fill=False, linewidth=((i + 1) / len(useR))**2, color='limegreen' if (i % 4 == 0) else 'r', linestyle='-' if useR[i] < results['fit R'][-1] else '--')) if not ('ap_nologo' in options and options['ap_nologo']): AddLogo(plt.gcf()) plt.savefig( '%sphotometry_ellipse_%s.jpg' % (options['ap_plotpath'] if 'ap_plotpath' in options else '', options['ap_name']), dpi=options['ap_plotdpi'] if 'ap_plotdpi' in options else 300) plt.close() return { 'prof header': params, 'prof units': SBprof_units, 'prof data': SBprof_data, 'prof format': SBprof_format }
def Center_HillClimb_mean(IMG, results, options): """ Using 10 circular isophotes out to 10 times the PSF length, the first FFT coefficient phases are averaged to find the direction of increasing flux. Flux values are sampled along this direction and a quadratic fit gives the maximum. This is iteratively repeated until the step size becomes very small. """ current_center = {'x': IMG.shape[0]/2, 'y': IMG.shape[1]/2} current_center = {'x': IMG.shape[1]/2, 'y': IMG.shape[0]/2} if 'ap_guess_center' in options: current_center = deepcopy(options['ap_guess_center']) logging.info('%s: Center initialized by user: %s' % (options['ap_name'], str(current_center))) if 'ap_set_center' in options: logging.info('%s: Center set by user: %s' % (options['ap_name'], str(options['ap_set_center']))) return IMG, {'center': deepcopy(options['ap_set_center'])} dat = IMG - results['background'] sampleradii = np.linspace(1,10,10) * results['psf fwhm'] track_centers = [] small_update_count = 0 total_count = 0 while small_update_count <= 5 and total_count <= 100: total_count += 1 phases = [] isovals = [] coefs = [] for r in sampleradii: isovals.append(_iso_extract(dat,r,0.,0.,current_center, more = True)) coefs.append(fft(isovals[-1][0])) phases.append((-np.angle(coefs[-1][1])) % (2*np.pi)) direction = Angle_Median(phases) % (2*np.pi) levels = [] level_locs = [] for i, r in enumerate(sampleradii): floc = np.argmin(np.abs(isovals[i][1] - direction)) rloc = np.argmin(np.abs(isovals[i][1] - ((direction+np.pi) % (2*np.pi)))) smooth = np.abs(ifft(coefs[i][:min(10,len(coefs[i]))],n = len(coefs[i]))) levels.append(smooth[floc]) level_locs.append(r) levels.insert(0,smooth[rloc]) level_locs.insert(0,-r) try: p = np.polyfit(level_locs, levels, deg = 2) if p[0] < 0 and len(levels) > 3: dist = np.clip(-p[1]/(2*p[0]), a_min = min(level_locs), a_max = max(level_locs)) else: dist = level_locs[np.argmax(levels)] except: dist = results['psf fwhm'] current_center['x'] += dist*np.cos(direction) current_center['y'] += dist*np.sin(direction) if abs(dist) < (0.25*results['psf fwhm']): small_update_count += 1 else: small_update_count = 0 track_centers.append([current_center['x'], current_center['y']]) # refine center res = minimize(_hillclimb_loss, x0 = [current_center['x'], current_center['y']], args = (dat, results['psf fwhm'], results['background noise']), method = 'Nelder-Mead') if res.success: current_center['x'] = res.x[0] current_center['y'] = res.x[1] return IMG, {'center': current_center, 'auxfile center': 'center x: %.2f pix, y: %.2f pix' % (current_center['x'], current_center['y'])}
def Isophote_Fit_FixedPhase(IMG, results, options): """Simply applies fixed position angle and ellipticity at the initialization values. Parameters ----------------- ap_scale : float, default 0.2 growth scale when fitting isophotes, not the same as *ap_sample---scale*. ap_fit_limit : float, default 2 noise level out to which to extend the fit in units of pixel background noise level. Default is 2, smaller values will end fitting further out in the galaxy image. Notes ---------- :References: - 'background' - 'background noise' - 'psf fwhm' - 'center' - 'mask' (optional) - 'init ellip' - 'init pa' Returns ------- IMG : ndarray Unaltered galaxy image results : dict .. code-block:: python {'fit ellip': , # array of ellipticity values (ndarray) 'fit pa': , # array of PA values (ndarray) 'fit R': , # array of semi-major axis values (ndarray) 'fit ellip_err': , # optional, array of ellipticity error values (ndarray) 'fit pa_err': , # optional, array of PA error values (ndarray) 'auxfile fitlimit': # optional, auxfile message (string) } """ if "ap_scale" in options: scale = options["ap_scale"] else: scale = 0.2 # subtract background from image during processing dat = IMG - results["background"] mask = results["mask"] if "mask" in results else None if not np.any(mask): mask = None # Determine sampling radii ###################################################################### shrink = 0 while shrink < 5: sample_radii = [max(1.0, results["psf fwhm"] / 2)] while sample_radii[-1] < (max(IMG.shape) / 2): isovals = _iso_extract( dat, sample_radii[-1], { "ellip": results["init ellip"], "pa": results["init pa"] }, results["center"], more=False, mask=mask, ) if (np.median(isovals) < (options["ap_fit_limit"] if "ap_fit_limit" in options else 2) * results["background noise"]): break sample_radii.append(sample_radii[-1] * (1.0 + scale / (1.0 + shrink))) if len(sample_radii) < 15: shrink += 1 else: break if shrink >= 5: raise Exception( "Unable to initialize ellipse fit, check diagnostic plots. Possible missed center." ) ellip = np.ones(len(sample_radii)) * results["init ellip"] pa = np.ones(len(sample_radii)) * results["init pa"] logging.debug("%s: sample radii: %s" % (options["ap_name"], str(sample_radii))) res = { "fit ellip": ellip, "fit pa": pa, "fit R": sample_radii, "auxfile fitlimit": "fit limit semi-major axis: %.2f pix" % sample_radii[-1], } if "init ellip_err" in results: res["fit ellip_err"] = np.ones( len(sample_radii)) * results["init ellip_err"] if "init pa_err" in results: res["fit pa_err"] = np.ones(len(sample_radii)) * results["init pa_err"] return IMG, res
def Center_HillClimb(IMG, results, options): """ Using 10 circular isophotes out to 10 times the PSF length, the first FFT coefficient phases are averaged to find the direction of increasing flux. Flux values are sampled along this direction and a quadratic fit gives the maximum. This is iteratively repeated until the step size becomes very small. """ current_center = {'x': IMG.shape[1]/2, 'y': IMG.shape[0]/2} if 'ap_guess_center' in options: current_center = deepcopy(options['ap_guess_center']) logging.info('%s: Center initialized by user: %s' % (options['ap_name'], str(current_center))) if 'ap_set_center' in options: logging.info('%s: Center set by user: %s' % (options['ap_name'], str(options['ap_set_center']))) return IMG, {'center': deepcopy(options['ap_set_center'])} dat = IMG - results['background'] sampleradii = np.linspace(1,10,10) * results['psf fwhm'] track_centers = [] small_update_count = 0 total_count = 0 while small_update_count <= 5 and total_count <= 100: total_count += 1 phases = [] isovals = [] coefs = [] for r in sampleradii: isovals.append(_iso_extract(dat,r,0.,0.,current_center, more = True)) coefs.append(fft(np.clip(isovals[-1][0], a_max = np.quantile(isovals[-1][0],0.85), a_min = None))) phases.append((-np.angle(coefs[-1][1])) % (2*np.pi)) direction = Angle_Median(phases) % (2*np.pi) levels = [] level_locs = [] for i, r in enumerate(sampleradii): floc = np.argmin(np.abs((isovals[i][1] % (2*np.pi)) - direction)) rloc = np.argmin(np.abs((isovals[i][1] % (2*np.pi)) - ((direction+np.pi) % (2*np.pi)))) smooth = np.abs(ifft(coefs[i][:min(10,len(coefs[i]))],n = len(coefs[i]))) levels.append(smooth[floc]) level_locs.append(r) levels.insert(0,smooth[rloc]) level_locs.insert(0,-r) try: p = np.polyfit(level_locs, levels, deg = 2) if p[0] < 0 and len(levels) > 3: dist = np.clip(-p[1]/(2*p[0]), a_min = min(level_locs), a_max = max(level_locs)) else: dist = level_locs[np.argmax(levels)] except: dist = results['psf fwhm'] current_center['x'] += dist*np.cos(direction) current_center['y'] += dist*np.sin(direction) if abs(dist) < (0.25*results['psf fwhm']): small_update_count += 1 else: small_update_count = 0 track_centers.append([current_center['x'], current_center['y']]) # refine center ranges = [[max(0,int(current_center['x']-results['psf fwhm']*5)), min(dat.shape[1],int(current_center['x']+results['psf fwhm']*5))], [max(0,int(current_center['y']-results['psf fwhm']*5)), min(dat.shape[0],int(current_center['y']+results['psf fwhm']*5))]] res = minimize(_hillclimb_loss, x0 = [current_center['x'] - ranges[0][0], current_center['y'] - ranges[1][0]], args = (dat[ranges[1][0]:ranges[1][1],ranges[0][0]:ranges[0][1]], results['psf fwhm'], results['background noise']), method = 'Nelder-Mead') if res.success: current_center['x'] = res.x[0] + ranges[0][0] current_center['y'] = res.x[1] + ranges[1][0] track_centers.append([current_center['x'], current_center['y']]) # paper plot if 'ap_paperplots' in options and options['ap_paperplots']: plt.imshow(np.clip(dat,a_min = 0, a_max = None), origin = 'lower', cmap = 'Greys_r', norm = ImageNormalize(stretch=LogStretch())) plt.plot([current_center['x']],[current_center['y']], marker = 'x', markersize = 3, color = 'r') for i in range(3): plt.gca().add_patch(Ellipse((current_center['x'],current_center['y']), 2*((i+0.5)*results['psf fwhm']), 2*((i+0.5)*results['psf fwhm']), 0, fill = False, linewidth = 0.5, color = 'y')) if not ('ap_nologo' in options and options['ap_nologo']): AddLogo(plt.gcf()) plt.savefig('%stest_center_%s.jpg' % (options['ap_plotpath'] if 'ap_plotpath' in options else '', options['ap_name']), dpi = options['ap_plotdpi'] if 'ap_plotdpi'in options else 300) plt.close() track_centers = np.array(track_centers) xwidth = 2*max(np.abs(track_centers[:,0] - current_center['x'])) ywidth = 2*max(np.abs(track_centers[:,1] - current_center['y'])) width = max(xwidth, ywidth) ranges = [[int(current_center['x'] - width), int(current_center['x'] + width)], [int(current_center['y'] - width), int(current_center['y'] + width)]] fig, axarr = plt.subplots(2,1, figsize = (3,6)) plt.subplots_adjust(hspace = 0.01, wspace = 0.01, left = 0.05, right = 0.95, top = 0.95, bottom = 0.05) axarr[0].imshow(np.clip(dat[ranges[1][0]:ranges[1][1],ranges[0][0]:ranges[0][1]],a_min = 0, a_max = None), origin = 'lower', cmap = 'Greys_r', norm = ImageNormalize(stretch=LogStretch()), extent = [ranges[0][0],ranges[0][1],ranges[1][0]-1,ranges[1][1]-1]) axarr[0].plot(track_centers[:,0], track_centers[:,1], color = 'y') axarr[0].scatter(track_centers[:,0], track_centers[:,1], c = range(len(track_centers)), cmap = 'Reds') axarr[0].set_xticks([]) axarr[0].set_yticks([]) width = 10. ranges = [[int(current_center['x'] - width), int(current_center['x'] + width)], [int(current_center['y'] - width), int(current_center['y'] + width)]] axarr[1].imshow(np.clip(dat[ranges[1][0]:ranges[1][1],ranges[0][0]:ranges[0][1]],a_min = 0, a_max = None), origin = 'lower', cmap = 'Greys_r', extent = [ranges[0][0],ranges[0][1],ranges[1][0]-1,ranges[1][1]-1]) axarr[1].plot(track_centers[:,0], track_centers[:,1], color = 'y') axarr[1].scatter(track_centers[:,0], track_centers[:,1], c = range(len(track_centers)), cmap = 'Reds') axarr[1].set_xlim(ranges[0]) axarr[1].set_ylim(np.array(ranges[1])-1) axarr[1].set_xticks([]) axarr[1].set_yticks([]) plt.savefig('%sCenter_path_%s.jpg' % (options['ap_plotpath'] if 'ap_plotpath' in options else '', options['ap_name']), dpi = options['ap_plotdpi'] if 'ap_plotdpi' in options else 300) plt.close() return IMG, {'center': current_center, 'auxfile center': 'center x: %.2f pix, y: %.2f pix' % (current_center['x'], current_center['y'])}
def Isophote_Initialize_mean(IMG, results, options): """Fit global elliptical isophote to a galaxy image using FFT coefficients. Same as the default isophote initialization routine, except uses mean/std measures for low S/N applications. Parameters ----------------- ap_fit_limit : float, default 2 noise level out to which to extend the fit in units of pixel background noise level. Default is 2, smaller values will end fitting further out in the galaxy image. Notes ---------- :References: - 'background' - 'background noise' - 'psf fwhm' - 'center' Returns ------- IMG : ndarray Unaltered galaxy image results : dict .. code-block:: python {'init ellip': , # Ellipticity of the global fit (float) 'init pa': ,# Position angle of the global fit (float) 'init R': ,# Semi-major axis length of global fit (float) 'auxfile initialize': # optional, message for aux file to record the global ellipticity and postition angle (string) } """ ###################################################################### # Initial attempt to find size of galaxy in image # based on when isophotes SB values start to get # close to the background noise level circ_ellipse_radii = [results["psf fwhm"]] allphase = [] dat = IMG - results["background"] while circ_ellipse_radii[-1] < (len(IMG) / 2): circ_ellipse_radii.append(circ_ellipse_radii[-1] * (1 + 0.2)) isovals = _iso_extract( dat, circ_ellipse_radii[-1], { "ellip": 0.0, "pa": 0.0 }, results["center"], more=True, ) coefs = fft(isovals[0]) allphase.append(coefs[2]) # Stop when at 3 times background noise if (np.mean(isovals[0]) < (3 * results["background noise"]) and len(circ_ellipse_radii) > 4): break logging.info("%s: init scale: %f pix" % (options["ap_name"], circ_ellipse_radii[-1])) # Find global position angle. phase = (-Angle_Median(np.angle(allphase[-5:])) / 2) % np.pi # (-np.angle(np.mean(allphase[-5:]))/2) % np.pi # Find global ellipticity test_ellip = np.linspace(0.05, 0.95, 15) test_f2 = [] for e in test_ellip: test_f2.append( sum( list( _fitEllip_mean_loss( e, dat, circ_ellipse_radii[-2] * m, phase, results["center"], results["background noise"], ) for m in np.linspace(0.8, 1.2, 5)))) ellip = test_ellip[np.argmin(test_f2)] res = minimize( lambda e, d, r, p, c, n: sum( list( _fitEllip_mean_loss(_x_to_eps(e[0]), d, r * m, p, c, n) for m in np.linspace(0.8, 1.2, 5))), x0=_inv_x_to_eps(ellip), args=( dat, circ_ellipse_radii[-2], phase, results["center"], results["background noise"], ), method="Nelder-Mead", options={ "initial_simplex": [ [_inv_x_to_eps(ellip) - 1 / 15], [_inv_x_to_eps(ellip) + 1 / 15], ] }, ) if res.success: logging.debug( "%s: using optimal ellipticity %.3f over grid ellipticity %.3f" % (options["ap_name"], _x_to_eps(res.x[0]), ellip)) ellip = _x_to_eps(res.x[0]) # Compute the error on the parameters ###################################################################### RR = np.linspace( circ_ellipse_radii[-2] - results["psf fwhm"], circ_ellipse_radii[-2] + results["psf fwhm"], 10, ) errallphase = [] for rr in RR: isovals = _iso_extract(dat, rr, { "ellip": 0.0, "pa": 0.0 }, results["center"], more=True) coefs = fft(isovals[0]) errallphase.append(coefs[2]) sample_pas = (-np.angle(1j * np.array(errallphase) / np.mean(errallphase)) / 2) % np.pi pa_err = np.std(sample_pas) res_multi = map( lambda rrp: minimize( lambda e, d, r, p, c, n: _fitEllip_mean_loss( _x_to_eps(e[0]), d, r, p, c, n), x0=_inv_x_to_eps(ellip), args=(dat, rrp[0], rrp[1], results["center"], results[ "background noise"]), method="Nelder-Mead", options={ "initial_simplex": [ [_inv_x_to_eps(ellip) - 1 / 15], [_inv_x_to_eps(ellip) + 1 / 15], ] }, ), zip(RR, sample_pas), ) ellip_err = np.std(list(_x_to_eps(rm.x[0]) for rm in res_multi)) circ_ellipse_radii = np.array(circ_ellipse_radii) if "ap_doplot" in options and options["ap_doplot"]: ranges = [ [ max(0, int(results["center"]["x"] - circ_ellipse_radii[-1] * 1.5)), min( dat.shape[1], int(results["center"]["x"] + circ_ellipse_radii[-1] * 1.5), ), ], [ max(0, int(results["center"]["y"] - circ_ellipse_radii[-1] * 1.5)), min( dat.shape[0], int(results["center"]["y"] + circ_ellipse_radii[-1] * 1.5), ), ], ] LSBImage( dat[ranges[1][0]:ranges[1][1], ranges[0][0]:ranges[0][1]], results["background noise"], ) # plt.imshow(np.clip(dat[ranges[1][0]: ranges[1][1], ranges[0][0]: ranges[0][1]],a_min = 0, a_max = None), # origin = 'lower', cmap = 'Greys_r', norm = ImageNormalize(stretch=LogStretch())) plt.gca().add_patch( Ellipse( ( results["center"]["x"] - ranges[0][0], results["center"]["y"] - ranges[1][0], ), 2 * circ_ellipse_radii[-1], 2 * circ_ellipse_radii[-1] * (1.0 - ellip), phase * 180 / np.pi, fill=False, linewidth=1, color="y", )) plt.plot( [results["center"]["x"] - ranges[0][0]], [results["center"]["y"] - ranges[1][0]], marker="x", markersize=3, color="r", ) plt.tight_layout() if not ("ap_nologo" in options and options["ap_nologo"]): AddLogo(plt.gcf()) plt.savefig( "%sinitialize_ellipse_%s.jpg" % ( options["ap_plotpath"] if "ap_plotpath" in options else "", options["ap_name"], ), dpi=options["ap_plotdpi"] if "ap_plotdpi" in options else 300, ) plt.close() fig, ax = plt.subplots(2, 1, figsize=(6, 6)) ax[0].plot( circ_ellipse_radii[:-1], ((-np.angle(allphase) / 2) % np.pi) * 180 / np.pi, color="k", ) ax[0].axhline(phase * 180 / np.pi, color="r") ax[0].axhline((phase + pa_err) * 180 / np.pi, color="r", linestyle="--") ax[0].axhline((phase - pa_err) * 180 / np.pi, color="r", linestyle="--") # ax[0].axvline(circ_ellipse_radii[-2], color = 'orange', linestyle = '--') ax[0].set_xlabel("Radius [pix]") ax[0].set_ylabel("FFT$_{1}$ phase [deg]") ax[1].plot(test_ellip, test_f2, color="k") ax[1].axvline(ellip, color="r") ax[1].axvline(ellip + ellip_err, color="r", linestyle="--") ax[1].axvline(ellip - ellip_err, color="r", linestyle="--") ax[1].set_xlabel("Ellipticity [1 - b/a]") ax[1].set_ylabel("Loss [FFT$_{2}$/med(flux)]") plt.tight_layout() if not ("ap_nologo" in options and options["ap_nologo"]): AddLogo(plt.gcf()) plt.savefig( "%sinitialize_ellipse_optimize_%s.jpg" % ( options["ap_plotpath"] if "ap_plotpath" in options else "", options["ap_name"], ), dpi=options["ap_plotdpi"] if "ap_plotdpi" in options else 300, ) plt.close() auxmessage = ( "global ellipticity: %.3f +- %.3f, pa: %.3f +- %.3f deg, size: %f pix" % ( ellip, ellip_err, PA_shift_convention(phase) * 180 / np.pi, pa_err * 180 / np.pi, circ_ellipse_radii[-2], )) return IMG, { "init ellip": ellip, "init ellip_err": ellip_err, "init pa": phase, "init pa_err": pa_err, "init R": circ_ellipse_radii[-2], "auxfile initialize": auxmessage, }
def Isophote_Initialize(IMG, results, options): """Fit global elliptical isophote to a galaxy image using FFT coefficients. A global position angle and ellipticity are fit in a two step process. First, a series of circular isophotes are geometrically sampled until they approach the background level of the image. An FFT is taken for the flux values around each isophote and the phase of the second coefficient is used to determine a direction. The average direction for the outer isophotes is taken as the position angle of the galaxy. Second, with fixed position angle the ellipticity is optimized to minimize the amplitude of the second FFT coefficient relative to the median flux in an isophote. To compute the error on position angle we use the standard deviation of the outer values from step one. For ellipticity the error is computed by optimizing the ellipticity for multiple isophotes within 1 PSF length of each other. Parameters ----------------- ap_fit_limit : float, default 2 noise level out to which to extend the fit in units of pixel background noise level. Default is 2, smaller values will end fitting further out in the galaxy image. ap_isoinit_pa_set : float, default None User set initial position angle in degrees, will override the calculation. ap_isoinit_ellip_set : float, default None User set initial ellipticity (1 - b/a), will override the calculation. Notes ---------- :References: - 'background' - 'background noise' - 'psf fwhm' - 'center' Returns ------- IMG : ndarray Unaltered galaxy image results : dict .. code-block:: python {'init ellip': , # Ellipticity of the global fit (float) 'init pa': ,# Position angle of the global fit (float) 'init R': ,# Semi-major axis length of global fit (float) 'auxfile initialize': # optional, message for aux file to record the global ellipticity and postition angle (string) } """ ###################################################################### # Initial attempt to find size of galaxy in image # based on when isophotes SB values start to get # close to the background noise level circ_ellipse_radii = [1.0] allphase = [] dat = IMG - results["background"] mask = results["mask"] if "mask" in results else None if not np.any(mask): mask = None while circ_ellipse_radii[-1] < (len(IMG) / 2): circ_ellipse_radii.append(circ_ellipse_radii[-1] * (1 + 0.2)) isovals = _iso_extract( dat, circ_ellipse_radii[-1], { "ellip": 0.0, "pa": 0.0 }, results["center"], more=True, mask=mask, sigmaclip=True, sclip_nsigma=3, interp_mask=True, ) coefs = fft(isovals[0]) allphase.append(coefs[2]) # Stop when at 3 time background noise if (np.quantile(isovals[0], 0.8) < ( (options["ap_fit_limit"] + 1 if "ap_fit_limit" in options else 3) * results["background noise"]) and len(circ_ellipse_radii) > 4): break logging.info("%s: init scale: %f pix" % (options["ap_name"], circ_ellipse_radii[-1])) # Find global position angle. phase = (-Angle_Median(np.angle(allphase[-5:])) / 2) % np.pi if "ap_isoinit_pa_set" in options: phase = PA_shift_convention(options["ap_isoinit_pa_set"] * np.pi / 180) # Find global ellipticity test_ellip = np.linspace(0.05, 0.95, 15) test_f2 = [] for e in test_ellip: test_f2.append( sum( list( _fitEllip_loss( e, dat, circ_ellipse_radii[-2] * m, phase, results["center"], results["background noise"], mask, ) for m in np.linspace(0.8, 1.2, 5)))) ellip = test_ellip[np.argmin(test_f2)] res = minimize( lambda e, d, r, p, c, n, msk: sum( list( _fitEllip_loss(_x_to_eps(e[0]), d, r * m, p, c, n, msk) for m in np.linspace(0.8, 1.2, 5))), x0=_inv_x_to_eps(ellip), args=( dat, circ_ellipse_radii[-2], phase, results["center"], results["background noise"], mask, ), method="Nelder-Mead", options={ "initial_simplex": [ [_inv_x_to_eps(ellip) - 1 / 15], [_inv_x_to_eps(ellip) + 1 / 15], ] }, ) if res.success: logging.debug( "%s: using optimal ellipticity %.3f over grid ellipticity %.3f" % (options["ap_name"], _x_to_eps(res.x[0]), ellip)) ellip = _x_to_eps(res.x[0]) if "ap_isoinit_ellip_set" in options: ellip = options["ap_isoinit_ellip_set"] # Compute the error on the parameters ###################################################################### RR = np.linspace( circ_ellipse_radii[-2] - results["psf fwhm"], circ_ellipse_radii[-2] + results["psf fwhm"], 10, ) errallphase = [] for rr in RR: isovals = _iso_extract( dat, rr, { "ellip": 0.0, "pa": 0.0 }, results["center"], more=True, sigmaclip=True, sclip_nsigma=3, interp_mask=True, ) coefs = fft(isovals[0]) errallphase.append(coefs[2]) sample_pas = (-np.angle(1j * np.array(errallphase) / np.mean(errallphase)) / 2) % np.pi pa_err = iqr(sample_pas, rng=[16, 84]) / 2 res_multi = map( lambda rrp: minimize( lambda e, d, r, p, c, n, m: _fitEllip_loss(_x_to_eps(e[0]), d, r, p, c, n, m), x0=_inv_x_to_eps(ellip), args=( dat, rrp[0], rrp[1], results["center"], results["background noise"], mask, ), method="Nelder-Mead", options={ "initial_simplex": [ [_inv_x_to_eps(ellip) - 1 / 15], [_inv_x_to_eps(ellip) + 1 / 15], ] }, ), zip(RR, sample_pas), ) ellip_err = iqr(list(_x_to_eps(rm.x[0]) for rm in res_multi), rng=[16, 84]) / 2 circ_ellipse_radii = np.array(circ_ellipse_radii) if "ap_doplot" in options and options["ap_doplot"]: Plot_Isophote_Init_Ellipse(dat, circ_ellipse_radii, ellip, phase, results, options) Plot_Isophote_Init_Optimize( circ_ellipse_radii, allphase, phase, pa_err, test_ellip, test_f2, ellip, ellip_err, results, options, ) auxmessage = ( "global ellipticity: %.3f +- %.3f, pa: %.3f +- %.3f deg, size: %f pix" % ( ellip, ellip_err, PA_shift_convention(phase) * 180 / np.pi, pa_err * 180 / np.pi, circ_ellipse_radii[-2], )) return IMG, { "init ellip": ellip, "init ellip_err": ellip_err, "init pa": phase, "init pa_err": pa_err, "init R": circ_ellipse_radii[-2], "auxfile initialize": auxmessage, }
def Check_Fit(IMG, results, options): """ Check for failed fit with various measures. 1) compare iqr of isophotes with that of a simple global fit 2) compare iqr of isophotes with median flux to check for high variability 3) measure signal in 2nd and 4th FFT coefficient which should be minimal 4) measure signal in 1st FFT coefficient which should be minimal 5) Compare integrated SB profile with simple flux summing for total magnitude """ tests = {} # subtract background from image during processing dat = IMG - results['background'] # Compare variability of flux values along isophotes ###################################################################### use_center = results['center'] count_variable = 0 count_initrelative = 0 f2_compare = [] f1_compare = [] for i in range(len(results['fit R'])): init_isovals = _iso_extract(dat, results['fit R'][i], results['init ellip'], results['init pa'], use_center) isovals = _iso_extract(dat, results['fit R'][i], results['fit ellip'][i], results['fit pa'][i], use_center) coefs = fft( np.clip(isovals, a_max=np.quantile(isovals, 0.85), a_min=None)) if np.median(isovals) < (iqr(isovals) - results['background noise']): count_variable += 1 if ((iqr(isovals) - results['background noise']) / (np.median(isovals) + results['background noise'])) > ( iqr(init_isovals) / (np.median(init_isovals) + results['background noise'])): count_initrelative += 1 f2_compare.append(np.sum(np.abs(coefs[[2, 4]])) / np.abs(coefs[0])) f1_compare.append(np.abs(coefs[1]) / np.abs(coefs[0])) f1_compare = np.array(f1_compare) f2_compare = np.array(f2_compare) if count_variable > (0.2 * len(results['fit R'])): logging.warning( '%s: Possible failed fit! flux values highly variable along isophotes' % options['ap_name']) tests['isophote variability'] = False else: tests['isophote variability'] = True if count_initrelative > (0.5 * len(results['fit R'])): logging.warning( '%s: Possible failed fit! flux values highly variable relative to initialization' % options['ap_name']) tests['initial fit compare'] = False else: tests['initial fit compare'] = True if np.sum(f2_compare > 0.3) > 2 or np.sum( f2_compare > 0.2) > (0.3 * len(results['fit R'])) or np.sum( f2_compare > 0.1) > (0.8 * len(results['fit R'])): logging.warning( '%s: Possible failed fit! poor convergence of FFT coefficients' % options['ap_name']) tests['FFT coefficients'] = False else: tests['FFT coefficients'] = True if np.sum(f1_compare > 0.3) > 2 or np.sum( f1_compare > 0.2) > (0.3 * len(results['fit R'])) or np.sum( f1_compare > 0.1) > (0.8 * len(results['fit R'])): logging.warning( '%s: Possible failed fit! possible failed center or lopsided galaxy' % options['ap_name']) tests['Light symmetry'] = False else: tests['Light symmetry'] = True # Compare integrated total magnitude with summed total magnitude try: SB = np.array(results['prof data']['SB']) SBe = np.array(results['prof data']['SB_e']) Mint = np.array(results['prof data']['totmag']) Msum = np.array(results['prof data']['totmag_direct']) CHOOSE = np.logical_and(SB < 99, SBe < 0.1) if np.sum(np.abs(Mint[CHOOSE][-4:] - Msum[CHOOSE][-4:]) > 0.2) > 2: logging.warning( '%s: Possible failed fit! Inconsistent results for curve of growth, bad fit or foreground star' % options['ap_name']) tests['curve of growth consistency'] = False else: tests['curve of growth consistency'] = True except: logging.info('%s: Check fit could not check SB profile consistency') res = {'checkfit': tests} for t in tests: res['auxfile checkfit %s' % t] = 'checkfit %s: %s' % (t, 'pass' if tests[t] else 'fail') return IMG, res
def Isophote_Fit_FFT_Robust(IMG, results, options): """Fit elliptical isophotes to a galaxy image using FFT coefficients and regularization. The isophotal fitting routine simultaneously optimizes a collection of elliptical isophotes by minimizing the 2nd FFT coefficient power, regularized for robustness. A series of isophotes are constructed which grow geometrically until they begin to reach the background level. Then the algorithm iteratively updates the position angle and ellipticity of each isophote individually for many rounds. Each round updates every isophote in a random order. Each round cycles between three options: optimizing position angle, ellipticity, or both. To optimize the parameters, 5 values (pa, ellip, or both) are randomly sampled and the "loss" is computed. The loss is a combination of the relative amplitude of the second FFT coefficient (compared to the median flux), and a regularization term. The regularization term penalizes adjacent isophotes for having different position angle or ellipticity (using the l1 norm). Thus, all the isophotes are coupled and tend to fit smoothly varying isophotes. When the optimization has completed three rounds without any isophotes updating, the profile is assumed to have converged. An uncertainty for each ellipticity and position angle value is determined by repeatedly re-optimizing each ellipse with slight adjustments to it's semi-major axis length (+- 5%). The standard deviation of the PA/ellipticity after repeated fitting gives the uncertainty. Parameters ----------------- ap_scale : float, default 0.2 growth scale when fitting isophotes, not the same as *ap_sample---scale*. ap_fit_limit : float, default 2 noise level out to which to extend the fit in units of pixel background noise level. Default is 2, smaller values will end fitting further out in the galaxy image. ap_regularize_scale : float, default 1 scale factor to apply to regularization coupling factor between isophotes. Default of 1, larger values make smoother fits, smaller values give more chaotic fits. ap_isofit_robustclip : float, default 0.15 quantile of flux values at which to clip when extracting values along an isophote. Clipping outlier values (such as very bright stars) while fitting isophotes allows for robust computation of FFT coefficients along an isophote. ap_isofit_losscoefs : tuple, default (2,) Tuple of FFT coefficients to use in optimization procedure. AutoProf will attemp to minimize the power in all listed FFT coefficients. Must be a tuple, not a list. ap_isofit_superellipse : bool, default False If True, AutoProf will fit superellipses instead of regular ellipses. A superellipse is typically used to represent boxy/disky isophotes. The variable controlling the transition from a rectangle to an ellipse to a four-armed-star like shape is C. A value of C = 2 represents an ellipse and is the starting point of the optimization. ap_isofit_fitcoefs : tuple, default None Tuple of FFT coefficients to use in fitting procedure. AutoProf will attemp to fit ellipses with these Fourier mode perturbations. Such perturbations allow for lopsided, boxy, disky, and other types of isophotes beyond straightforward ellipses. Must be a tuple, not a list. Note that AutoProf will first fit ellipses, then turn on the Fourier mode perturbations, thus the fitting time will always be longer. ap_isofit_fitcoefs_FFTinit : bool, default False If True, the coefficients for the Fourier modes fitted from ap_isofit_fitcoefs will be initialized using an FFT decomposition along fitted elliptical isophotes. This can improve the fit result, though it is less stable and so users should examine the results after fitting. ap_isofit_perturbscale_ellip : float, default 0.03 Sampling scale for random adjustments to ellipticity made while optimizing isophotes. Smaller values will converge faster, but get stuck in local minima; larger values will escape local minima, but takes longer to converge. ap_isofit_perturbscale_pa : float, default 0.06 Sampling scale for random adjustments to position angle made while optimizing isophotes. Smaller values will converge faster, but get stuck in local minima; larger values will escape local minima, but takes longer to converge. ap_isofit_iterlimitmax : int, default 300 Maximum number of iterations (each iteration adjusts every isophote once) before automatically stopping optimization. For galaxies with lots of structure (ie detailed spiral arms) more iterations may be needed to fully fit the light distribution, but runtime will be longer. ap_isofit_iterlimitmin : int, default 0 Minimum number of iterations before optimization is allowed to stop. ap_isofit_iterstopnochange : float, default 3 Number of iterations with no updates to parameters before optimization procedure stops. Lower values will process galaxies faster, but may still be stuck in local minima, higher values are more likely to converge on the global minimum but can take a long time to run. Fractional values are allowed though not recomended. Notes ---------- :References: - 'background' - 'background noise' - 'psf fwhm' - 'center' - 'mask' (optional) - 'init ellip' - 'init pa' Returns ------- IMG : ndarray Unaltered galaxy image results : dict .. code-block:: python {'fit ellip': , # array of ellipticity values (ndarray) 'fit pa': , # array of PA values (ndarray) 'fit R': , # array of semi-major axis values (ndarray) 'fit ellip_err': , # optional, array of ellipticity error values (ndarray) 'fit pa_err': , # optional, array of PA error values (ndarray) 'fit C': , # optional, superellipse scale parameter (ndarray) 'fit Fmodes': , # optional, fitted Fourier mode indices (tuple) 'fit Fmode A*': , # optional, fitted Fourier mode amplitudes, * for each index (ndarray) 'fit Fmode Phi*': , # optional, fitted Fourier mode phases, * for each index (ndarray) 'auxfile fitlimit': # optional, auxfile message (string) } """ if "ap_scale" in options: scale = options["ap_scale"] else: scale = 0.2 # subtract background from image during processing dat = IMG - results["background"] mask = results["mask"] if "mask" in results else None if not np.any(mask): mask = None # Determine sampling radii ###################################################################### shrink = 0 while shrink < 5: sample_radii = [max(1.0, results["psf fwhm"] / 2)] while sample_radii[-1] < (max(IMG.shape) / 2): isovals = _iso_extract( dat, sample_radii[-1], { "ellip": results["init ellip"], "pa": results["init pa"] }, results["center"], more=False, mask=mask, ) if (np.median(isovals) < (options["ap_fit_limit"] if "ap_fit_limit" in options else 2) * results["background noise"]): break sample_radii.append(sample_radii[-1] * (1.0 + scale / (1.0 + shrink))) if len(sample_radii) < 15: shrink += 1 else: break if shrink >= 5: raise Exception( "Unable to initialize ellipse fit, check diagnostic plots. Possible missed center." ) ellip = np.ones(len(sample_radii)) * results["init ellip"] pa = np.ones(len(sample_radii)) * results["init pa"] logging.debug("%s: sample radii: %s" % (options["ap_name"], str(sample_radii))) # Fit isophotes ###################################################################### perturb_scale = 0.03 regularize_scale = (options["ap_regularize_scale"] if "ap_regularize_scale" in options else 1.0) robust_clip = (options["ap_isofit_robustclip"] if "ap_isofit_robustclip" in options else 0.15) N_perturb = 5 fit_coefs = (options["ap_isofit_losscoefs"] if "ap_isofit_losscoefs" in options else None) fit_params = (options["ap_isofit_fitcoefs"] if "ap_isofit_fitcoefs" in options else None) fit_superellipse = (options["ap_isofit_superellipse"] if "ap_isofit_superellipse" in options else False) parameters = list( { "ellip": ellip[i], "pa": pa[i], "m": fit_params, "C": 2 if fit_superellipse else None, "Am": None if fit_params is None else np.zeros(len(fit_params)), "Phim": None if fit_params is None else np.zeros(len(fit_params)), } for i in range(len(ellip))) count = 0 iterlimitmax = (options["ap_isofit_iterlimitmax"] if "ap_isofit_iterlimitmax" in options else 1000) iterlimitmin = (options["ap_isofit_iterlimitmin"] if "ap_isofit_iterlimitmin" in options else 0) iterstopnochange = (options["ap_isofit_iterstopnochange"] if "ap_isofit_iterstopnochange" in options else 3) count_nochange = 0 use_center = copy(results["center"]) I = np.array(range(len(sample_radii))) param_cycle = 2 base_params = 2 + int(fit_superellipse) while count < iterlimitmax: # Periodically include logging message if count % 10 == 0: logging.debug("%s: count: %i" % (options["ap_name"], count)) count += 1 np.random.shuffle(I) N_perturb = int(1 + (10 / np.sqrt(count))) for i in I: perturbations = [] perturbations.append(deepcopy(parameters)) perturbations[-1][i]["loss"] = _FFT_Robust_loss( dat, sample_radii, perturbations[-1], i, use_center, results["background noise"], mask=mask, reg_scale=regularize_scale if count > 4 else 0, robust_clip=robust_clip, fit_coefs=fit_coefs, name=options["ap_name"], ) for n in range(N_perturb): perturbations.append(deepcopy(parameters)) if count % param_cycle == 0: perturbations[-1][i]["ellip"] = _x_to_eps( _inv_x_to_eps(perturbations[-1][i]["ellip"]) + np.random.normal(loc=0, scale=perturb_scale)) elif count % param_cycle == 1: perturbations[-1][i]["pa"] = ( perturbations[-1][i]["pa"] + np.random.normal( loc=0, scale=np.pi * perturb_scale)) % np.pi elif (count % param_cycle) == 2 and not parameters[i]["C"] is None: perturbations[-1][i]["C"] = 10**( np.log10(perturbations[-1][i]["C"]) + np.random.normal( loc=0, scale=np.log10(1.0 + perturb_scale))) elif count % param_cycle < (base_params + len(parameters[i]["m"])): perturbations[-1][i]["Am"][ (count % param_cycle) - base_params] += np.random.normal(loc=0, scale=perturb_scale) elif count % param_cycle < (base_params + 2 * len(parameters[i]["m"])): phim_index = ((count % param_cycle) - base_params - len(parameters[i]["m"])) perturbations[-1][i]["Phim"][phim_index] = ( perturbations[-1][i]["Phim"][phim_index] + np.random.normal( loc=0, scale=2 * np.pi * perturb_scale / parameters[i]["m"][phim_index], )) % (2 * np.pi / parameters[i]["m"][phim_index]) else: raise Exception( "Unrecognized optimization parameter id: %i" % (count % param_cycle)) perturbations[-1][i]["loss"] = _FFT_Robust_loss( dat, sample_radii, perturbations[-1], i, use_center, results["background noise"], mask=mask, reg_scale=regularize_scale if count > 4 else 0, robust_clip=robust_clip, fit_coefs=fit_coefs, name=options["ap_name"], ) best = np.argmin(list(p[i]["loss"] for p in perturbations)) if best > 0: parameters = deepcopy(perturbations[best]) del parameters[i]["loss"] count_nochange = 0 else: count_nochange += 1 if not (count_nochange < (iterstopnochange * (len(sample_radii) - 1)) or count < iterlimitmin): if param_cycle > 2 or (parameters[i]["m"] is None and not fit_superellipse): break elif parameters[i]["m"] is None and fit_superellipse: logging.info("%s: Started C fitting at iteration %i" % (options["ap_name"], count)) param_cycle = 3 iterstopnochange = max(iterstopnochange, param_cycle) count_nochange = 0 count = 0 if fit_coefs is None: fit_coefs = (2, 4) else: logging.info("%s: Started Fmode fitting at iteration %i" % (options["ap_name"], count)) if fit_superellipse: logging.info("%s: Started C fitting at iteration %i" % (options["ap_name"], count)) param_cycle = base_params + 2 * len(parameters[i]["m"]) iterstopnochange = max(iterstopnochange, param_cycle) count_nochange = 0 count = 0 if fit_coefs is None and not fit_params is None: fit_coefs = fit_params if not 2 in fit_coefs: fit_coefs = tuple( sorted(set([2] + list(fit_coefs)))) if not parameters[i]["C"] is None and ( not "ap_isofit_losscoefs" in options or options["ap_isofit_losscoefs"] is None): fit_coefs = tuple(sorted(set([4] + list(fit_coefs)))) if ("ap_isofit_fitcoefs_FFTinit" in options and options["ap_isofit_fitcoefs_FFTinit"]): for ii in I: isovals = _iso_extract( dat, sample_radii[ii], parameters[ii], use_center, mask=mask, interp_mask=False if mask is None else True, interp_method="bicubic", ) if mask is None: coefs = fft( np.clip( isovals, a_max=np.quantile(isovals, 0.85), a_min=None, )) else: coefs = fft( np.clip( isovals, a_max=np.quantile(isovals, 0.9), a_min=None, )) for m in range(len(parameters[ii]["m"])): parameters[ii]["Am"][m] = np.abs( coefs[parameters[ii]["m"][m]] / coefs[0]) * np.sign( np.angle( coefs[parameters[ii]["m"][m]])) parameters[ii]["Phim"][m] = np.angle( coefs[parameters[ii]["m"][m]]) % (2 * np.pi) if not (count_nochange < (iterstopnochange * (len(sample_radii) - 1)) or count < iterlimitmin): break logging.info("%s: Completed isohpote fit in %i itterations" % (options["ap_name"], count)) # Compute errors ###################################################################### ellip_err, pa_err = _FFT_Robust_Errors( dat, sample_radii, parameters, use_center, results["background noise"], mask=mask, reg_scale=regularize_scale, robust_clip=robust_clip, fit_coefs=fit_coefs, name=options["ap_name"], ) for i in range(len(ellip)): parameters[i]["ellip err"] = ellip_err[i] parameters[i]["pa err"] = pa_err[i] # Plot fitting results ###################################################################### if "ap_doplot" in options and options["ap_doplot"]: Plot_Isophote_Fit(dat, sample_radii, parameters, results, options) res = { "fit ellip": np.array(list(parameters[i]["ellip"] for i in range(len(parameters)))), "fit pa": np.array(list(parameters[i]["pa"] for i in range(len(parameters)))), "fit R": sample_radii, "fit ellip_err": ellip_err, "fit pa_err": pa_err, "auxfile fitlimit": "fit limit semi-major axis: %.2f pix" % sample_radii[-1], } if not fit_params is None: res.update({"fit Fmodes": fit_params}) for m in range(len(fit_params)): res.update({ "fit Fmode A%i" % fit_params[m]: np.array( list(parameters[i]["Am"][m] for i in range(len(parameters)))), "fit Fmode Phi%i" % fit_params[m]: np.array( list(parameters[i]["Phim"][m] for i in range(len(parameters)))), }) if fit_superellipse: res.update({ "fit C": np.array(list(parameters[i]["C"] for i in range(len(parameters)))) }) return IMG, res
def Isophote_Fit_FFT_mean(IMG, results, options): """ Fit isophotes by minimizing the amplitude of the second FFT coefficient, relative to the local median flux. Included is a regularization term which penalizes isophotes for having large differences between parameters of adjacent isophotes. """ if 'ap_scale' in options: scale = options['ap_scale'] else: scale = 0.2 # subtract background from image during processing dat = IMG - results['background'] mask = results['mask'] if 'mask' in results else None if not np.any(mask): mask = None # Determine sampling radii ###################################################################### shrink = 0 while shrink < 5: sample_radii = [3 * results['psf fwhm'] / 2] while sample_radii[-1] < (max(IMG.shape) / 2): isovals = _iso_extract(dat, sample_radii[-1], results['init ellip'], results['init pa'], results['center'], more=False, mask=mask) if np.mean(isovals) < (options['ap_fit_limit'] if 'ap_fit_limit' in options else 1) * results['background noise']: break sample_radii.append(sample_radii[-1] * (1. + scale / (1. + shrink))) if len(sample_radii) < 15: shrink += 1 else: break if shrink >= 5: raise Exception( 'Unable to initialize ellipse fit, check diagnostic plots. Possible missed center.' ) ellip = np.ones(len(sample_radii)) * results['init ellip'] pa = np.ones(len(sample_radii)) * results['init pa'] logging.debug('%s: sample radii: %s' % (options['ap_name'], str(sample_radii))) # Fit isophotes ###################################################################### perturb_scale = np.array([0.03, 0.06]) regularize_scale = options[ 'ap_regularize_scale'] if 'ap_regularize_scale' in options else 1. N_perturb = 5 count = 0 count_nochange = 0 use_center = copy(results['center']) I = np.array(range(len(sample_radii))) while count < 300 and count_nochange < (3 * len(sample_radii)): # Periodically include logging message if count % 10 == 0: logging.debug('%s: count: %i' % (options['ap_name'], count)) count += 1 np.random.shuffle(I) for i in I: perturbations = [] perturbations.append({'ellip': copy(ellip), 'pa': copy(pa)}) perturbations[-1]['loss'] = _FFT_mean_loss( dat, sample_radii, perturbations[-1]['ellip'], perturbations[-1]['pa'], i, use_center, results['background noise'], mask=mask, reg_scale=regularize_scale if count > 4 else 0, name=options['ap_name']) for n in range(N_perturb): perturbations.append({'ellip': copy(ellip), 'pa': copy(pa)}) if count % 3 in [0, 1]: perturbations[-1]['ellip'][i] = _x_to_eps( _inv_x_to_eps(perturbations[-1]['ellip'][i]) + np.random.normal(loc=0, scale=perturb_scale[0])) if count % 3 in [1, 2]: perturbations[-1]['pa'][i] = ( perturbations[-1]['pa'][i] + np.random.normal( loc=0, scale=perturb_scale[1])) % np.pi perturbations[-1]['loss'] = _FFT_mean_loss( dat, sample_radii, perturbations[-1]['ellip'], perturbations[-1]['pa'], i, use_center, results['background noise'], mask=mask, reg_scale=regularize_scale if count > 4 else 0, name=options['ap_name']) best = np.argmin(list(p['loss'] for p in perturbations)) if best > 0: ellip = copy(perturbations[best]['ellip']) pa = copy(perturbations[best]['pa']) count_nochange = 0 else: count_nochange += 1 logging.info('%s: Completed isohpote fit in %i itterations' % (options['ap_name'], count)) # detect collapsed center ###################################################################### for i in range(5): if (_inv_x_to_eps(ellip[i]) - _inv_x_to_eps(ellip[i + 1])) > 0.5: ellip[:i + 1] = ellip[i + 1] pa[:i + 1] = pa[i + 1] # Smooth ellip and pa profile ###################################################################### smooth_ellip = copy(ellip) smooth_pa = copy(pa) ellip[:3] = min(ellip[:3]) smooth_ellip = _ellip_smooth(sample_radii, smooth_ellip, 5) smooth_pa = _pa_smooth(sample_radii, smooth_pa, 5) if 'ap_doplot' in options and options['ap_doplot']: ranges = [[ max(0, int(use_center['x'] - sample_radii[-1] * 1.2)), min(dat.shape[1], int(use_center['x'] + sample_radii[-1] * 1.2)) ], [ max(0, int(use_center['y'] - sample_radii[-1] * 1.2)), min(dat.shape[0], int(use_center['y'] + sample_radii[-1] * 1.2)) ]] LSBImage(dat[ranges[1][0]:ranges[1][1], ranges[0][0]:ranges[0][1]], results['background noise']) # plt.imshow(np.clip(dat[ranges[1][0]: ranges[1][1], ranges[0][0]: ranges[0][1]], # a_min = 0,a_max = None), origin = 'lower', cmap = 'Greys', norm = ImageNormalize(stretch=LogStretch())) for i in range(len(sample_radii)): plt.gca().add_patch( Ellipse((use_center['x'] - ranges[0][0], use_center['y'] - ranges[1][0]), 2 * sample_radii[i], 2 * sample_radii[i] * (1. - ellip[i]), pa[i] * 180 / np.pi, fill=False, linewidth=((i + 1) / len(sample_radii))**2, color='r')) if not ('ap_nologo' in options and options['ap_nologo']): AddLogo(plt.gcf()) plt.savefig( '%sfit_ellipse_%s.jpg' % (options['ap_plotpath'] if 'ap_plotpath' in options else '', options['ap_name']), dpi=options['ap_plotdpi'] if 'ap_plotdpi' in options else 300) plt.close() plt.scatter(sample_radii, ellip, color='r', label='ellip') plt.scatter(sample_radii, pa / np.pi, color='b', label='pa/$np.pi$') show_ellip = _ellip_smooth(sample_radii, ellip, deg=5) show_pa = _pa_smooth(sample_radii, pa, deg=5) plt.plot(sample_radii, show_ellip, color='orange', linewidth=2, linestyle='--', label='smooth ellip') plt.plot(sample_radii, show_pa / np.pi, color='purple', linewidth=2, linestyle='--', label='smooth pa/$np.pi$') #plt.xscale('log') plt.legend() if not ('ap_nologo' in options and options['ap_nologo']): AddLogo(plt.gcf()) plt.savefig( '%sphaseprofile_%s.jpg' % (options['ap_plotpath'] if 'ap_plotpath' in options else '', options['ap_name']), dpi=options['ap_plotdpi'] if 'ap_plotdpi' in options else 300) plt.close() # Compute errors ###################################################################### ellip_err = np.zeros(len(ellip)) ellip_err[:2] = np.sqrt(np.sum((ellip[:4] - smooth_ellip[:4])**2) / 4) ellip_err[-1] = np.sqrt(np.sum((ellip[-4:] - smooth_ellip[-4:])**2) / 4) pa_err = np.zeros(len(pa)) pa_err[:2] = np.sqrt(np.sum((pa[:4] - smooth_pa[:4])**2) / 4) pa_err[-1] = np.sqrt(np.sum((pa[-4:] - smooth_pa[-4:])**2) / 4) for i in range(2, len(pa) - 1): ellip_err[i] = np.sqrt( np.sum((ellip[i - 2:i + 2] - smooth_ellip[i - 2:i + 2])**2) / 4) pa_err[i] = np.sqrt( np.sum((pa[i - 2:i + 2] - smooth_pa[i - 2:i + 2])**2) / 4) res = { 'fit ellip': ellip, 'fit pa': pa, 'fit R': sample_radii, 'fit ellip_err': ellip_err, 'fit pa_err': pa_err, 'auxfile fitlimit': 'fit limit semi-major axis: %.2f pix' % sample_radii[-1] } return IMG, res
def _fitEllip_mean_loss(e, dat, r, p, c, n): isovals = _iso_extract(dat, r, e, p, c) coefs = fft(isovals) return np.abs(coefs[2]) / (len(isovals) * (max(0, np.mean(isovals)) + n))
def Center_Peak(IMG, results, options): current_center = {"x": IMG.shape[1] / 2, "y": IMG.shape[0] / 2} dat = IMG - results["background"] if "ap_guess_center" in options: current_center = deepcopy(options["ap_guess_center"]) logging.info("%s: Center initialized by user: %s" % (options["ap_name"], str(current_center))) if "ap_set_center" in options: logging.info("%s: Center set by user: %s" % (options["ap_name"], str(options["ap_set_center"]))) sb0 = flux_to_sb( _iso_extract(dat, 0.0, { "ellip": 0.0, "pa": 0.0 }, options["ap_set_center"])[0], options["ap_pixscale"], options["ap_zeropoint"] if "zeropoint" in options else 22.5, ) return IMG, { "center": deepcopy(options["ap_set_center"]), "auxfile central sb": "central surface brightness: %.4f mag arcsec^-2" % sb0, } searchring = int( (options["ap_centeringring"] if "ap_centeringring" in options else 10) * results["psf fwhm"]) xx, yy = np.meshgrid(np.arange(searchring), np.arange(searchring)) xx = xx.flatten() yy = yy.flatten() A = np.array([ np.ones(xx.shape), xx, yy, xx**2, yy**2, xx * yy, xx * yy**2, yy * xx**2, xx**2 * yy**2, ]).T ranges = [ [ max(0, int(current_center["x"] - searchring / 2)), min(IMG.shape[1], int(current_center["x"] + searchring / 2)), ], [ max(0, int(current_center["y"] - searchring / 2)), min(IMG.shape[0], int(current_center["y"] + searchring / 2)), ], ] chunk = np.clip( dat[ranges[1][0]:ranges[1][1], ranges[0][0]:ranges[0][1]].T, a_min=results["background noise"] / 5, a_max=None, ) poly2dfit = np.linalg.lstsq(A, np.log10(chunk.flatten()), rcond=None) current_center = { "x": -poly2dfit[0][2] / (2 * poly2dfit[0][4]) + ranges[0][0], "y": -poly2dfit[0][1] / (2 * poly2dfit[0][3]) + ranges[1][0], } sb0 = flux_to_sb( _iso_extract(dat, 0.0, { "ellip": 0.0, "pa": 0.0 }, current_center)[0], options["ap_pixscale"], options["ap_zeropoint"] if "zeropoint" in options else 22.5, ) return IMG, { "center": current_center, "auxfile center": "center x: %.2f pix, y: %.2f pix" % (current_center["x"], current_center["y"]), "auxfile central sb": "central surface brightness: %.4f mag arcsec^-2" % sb0, }
def Center_OfMass(IMG, results, options): """Find the light weighted galaxy center. Iteratively computes the light weighted centroid within a window, moves to the new center and computes the light weighted centroid again. The size of the search area is 10PSF by default. The iterative process will continue until the center is updated by less than 1/10th of the PSF size or when too mny iterations have been reached. Parameters ----------------- ap_guess_center : dict, default None user provided starting point for center fitting. Center should be formatted as: .. code-block:: python {'x':float, 'y': float} , where the floats are the center coordinates in pixels. If not given, Autoprof will default to a guess of the image center. ap_set_center : dict, default None user provided fixed center for rest of calculations. Center should be formatted as: .. code-block:: python {'x':float, 'y': float} , where the floats are the center coordinates in pixels. If not given, Autoprof will default to a guess of the image center. ap_centeringring : int, default 10 Size of ring to use when finding galaxy center, in units of PSF. Larger rings will allow for the starting position to be further from the true galaxy center. Smaller rings will include fewer spurious objects, and can stop the centroid from being distracted by larger nearby objects/galaxies. Notes ---------- :References: - 'background' - 'psf fwhm' Returns ------- IMG : ndarray Unaltered galaxy image results : dict .. code-block:: python {'center': {'x': , # x coordinate of the center (pix) 'y': }, # y coordinate of the center (pix) 'auxfile center': # optional, message for aux file to record galaxy center (string) 'auxfile centeral sb': # optional, central surface brightness value (float) } """ current_center = {"x": IMG.shape[1] / 2, "y": IMG.shape[0] / 2} dat = IMG - results["background"] if "ap_guess_center" in options: current_center = deepcopy(options["ap_guess_center"]) logging.info("%s: Center initialized by user: %s" % (options["ap_name"], str(current_center))) if "ap_set_center" in options: logging.info("%s: Center set by user: %s" % (options["ap_name"], str(options["ap_set_center"]))) sb0 = flux_to_sb( _iso_extract(dat, 0.0, { "ellip": 0.0, "pa": 0.0 }, options["ap_set_center"])[0], options["ap_pixscale"], options["ap_zeropoint"] if "zeropoint" in options else 22.5, ) return IMG, { "center": deepcopy(options["ap_set_center"]), "auxfile central sb": "central surface brightness: %.4f mag arcsec^-2" % sb0, } searchring = int( (options["ap_centeringring"] if "ap_centeringring" in options else 10) * results["psf fwhm"]) xx, yy = np.meshgrid(np.arange(searchring), np.arange(searchring)) N_updates = 0 while N_updates < 100: N_updates += 1 old_center = deepcopy(current_center) ranges = [ [ max(0, int(current_center["x"] - searchring / 2)), min(IMG.shape[1], int(current_center["x"] + searchring / 2)), ], [ max(0, int(current_center["y"] - searchring / 2)), min(IMG.shape[0], int(current_center["y"] + searchring / 2)), ], ] current_center = { "x": ranges[0][0] + np.sum( (dat[ranges[1][0]:ranges[1][1], ranges[0][0]:ranges[0][1]] * xx)) / np.sum(dat[ranges[1][0]:ranges[1][1], ranges[0][0]:ranges[0][1]]), "y": ranges[1][0] + np.sum( (dat[ranges[1][0]:ranges[1][1], ranges[0][0]:ranges[0][1]] * yy)) / np.sum(dat[ranges[1][0]:ranges[1][1], ranges[0][0]:ranges[0][1]]), } if (abs(current_center["x"] - old_center["x"]) < 0.1 * results["psf fwhm"] and abs(current_center["y"] - old_center["y"]) < 0.1 * results["psf fwhm"]): break sb0 = flux_to_sb( _iso_extract(dat, 0.0, { "ellip": 0.0, "pa": 0.0 }, current_center)[0], options["ap_pixscale"], options["ap_zeropoint"] if "zeropoint" in options else 22.5, ) return IMG, { "center": current_center, "auxfile center": "center x: %.2f pix, y: %.2f pix" % (current_center["x"], current_center["y"]), "auxfile central sb": "central surface brightness: %.4f mag arcsec^-2" % sb0, }
def Center_Forced(IMG, results, options): """Extracts previously fit center coordinates. Extracts the center coordinates from an aux file for a previous AutoProf fit. Can instead simply be given a set center value, just like other centering methods. A given center will override teh fitted aux file center. Parameters ----------------- ap_guess_center : dict, default None user provided starting point for center fitting. Center should be formatted as: .. code-block:: python {'x':float, 'y': float} , where the floats are the center coordinates in pixels. If not given, Autoprof will default to a guess of the image center. ap_set_center : dict, default None user provided fixed center for rest of calculations. Center should be formatted as: .. code-block:: python {'x':float, 'y': float} , where the floats are the center coordinates in pixels. If not given, Autoprof will default to a guess of the image center. ap_forcing_profile : string, default None (required for forced photometry) file path to .prof file providing forced photometry PA and ellip values to apply to *ap_image_file*. Notes ---------- :References: - 'background' - 'background noise' Returns ------- IMG : ndarray Unaltered galaxy image results : dict .. code-block:: python {'center': {'x': , # x coordinate of the center (pix) 'y': }, # y coordinate of the center (pix) 'auxfile center': # optional, message for aux file to record galaxy center (string) 'auxfile centeral sb': # optional, central surface brightness value (float) } """ current_center = {"x": IMG.shape[1] / 2, "y": IMG.shape[0] / 2} if "ap_guess_center" in options: current_center = deepcopy(options["ap_guess_center"]) logging.info("%s: Center initialized by user: %s" % (options["ap_name"], str(current_center))) if "ap_set_center" in options: logging.info("%s: Center set by user: %s" % (options["ap_name"], str(options["ap_set_center"]))) sb0 = flux_to_sb( _iso_extract( IMG - results["background"], 0.0, { "ellip": 0.0, "pa": 0.0 }, options["ap_set_center"], )[0], options["ap_pixscale"], options["ap_zeropoint"] if "zeropoint" in options else 22.5, ) return IMG, { "center": deepcopy(options["ap_set_center"]), "auxfile central sb": "central surface brightness: %.4f mag arcsec^-2" % sb0, } try: with open(options["ap_forcing_profile"][:-4] + "aux", "r") as f: for line in f.readlines(): if line[:6] == "center": x_loc = line.find("x:") y_loc = line.find("y:") current_center = { "x": float(line[x_loc + 3:line.find("pix")]), "y": float(line[y_loc + 3:line.rfind("pix")]), } break else: logging.warning( "%s: Forced center failed! Using image center (or guess)." % options["ap_name"]) except: logging.warning( "%s: Forced center failed! Using image center (or guess)." % options["ap_name"]) sb0 = flux_to_sb( _iso_extract(IMG - results["background"], 0.0, { "ellip": 0.0, "pa": 0.0 }, current_center)[0], options["ap_pixscale"], options["ap_zeropoint"] if "zeropoint" in options else 22.5, ) return IMG, { "center": current_center, "auxfile center": "center x: %.2f pix, y: %.2f pix" % (current_center["x"], current_center["y"]), "auxfile central sb": "central surface brightness: %.4f mag arcsec^-2" % sb0, }
def Radial_Profiles(IMG, results, options): mask = results['mask'] if 'mask' in results else None nwedges = options[ 'ap_radialprofiles_nwedges'] if 'ap_radialprofiles_nwedges' in options else 4 wedgeangles = np.linspace(0, 2 * np.pi * (1 - 1. / nwedges), nwedges) zeropoint = options['ap_zeropoint'] if 'ap_zeropoint' in options else 22.5 R = np.array(results['prof data']['R']) / options['ap_pixscale'] SBE = np.array(results['prof data']['SB_e']) if 'ap_radialprofiles_variable_pa' in options and options[ 'ap_radialprofiles_variable_pa']: pa = np.array(results['prof data']['pa']) * np.pi / 180 else: pa = np.ones(len(R)) * ( (options['ap_radialprofiles_pa'] * np.pi / 180) if 'ap_radialprofiles_pa' in options else results['init pa']) dat = IMG - results['background'] maxwedgewidth = options[ 'ap_radialprofiles_width'] if 'ap_radialprofiles_width' in options else 15. maxwedgewidth *= np.pi / 180 if 'ap_radialprofiles_expwidth' in options and options[ 'ap_radialprofiles_expwidth']: wedgewidth = maxwedgewidth * np.exp(R / R[-1] - 1) else: wedgewidth = np.ones(len(R)) * maxwedgewidth if wedgewidth[-1] * nwedges > 2 * np.pi: logging.warning( '%s: Radial sampling wedges are overlapping! %i wedges with a maximum width of %.3f rad' % (nwedges, wedgewidth[-1])) sb = list([] for i in wedgeangles) sbE = list([] for i in wedgeangles) for i in range(len(R)): if R[i] < 100: isovals = list( _iso_extract(dat, R[i], 0, 0, results['center'], more=True, minN=int(5 * 2 * np.pi / wedgewidth[i]), mask=mask)) else: isobandwidth = R[i] * (options['ap_isoband_width'] if 'ap_isoband_width' in options else 0.025) isovals = list( _iso_between(dat, R[i] - isobandwidth, R[i] + isobandwidth, 0, 0, results['center'], more=True, mask=mask)) isovals[1] -= pa[i] for sa_i in range(len(wedgeangles)): aselect = np.abs(Angle_TwoAngles(wedgeangles[sa_i], isovals[1])) < (wedgewidth[i] / 2) if np.sum(aselect) == 0: sb[sa_i].append(99.999) sbE[sa_i].append(99.999) continue medflux = _average( isovals[0][aselect], options['ap_isoaverage_method'] if 'ap_isoaverage_method' in options else 'median') scatflux = _scatter( isovals[0][aselect], options['ap_isoaverage_method'] if 'ap_isoaverage_method' in options else 'median') sb[sa_i].append( flux_to_sb(medflux, options['ap_pixscale'], zeropoint ) if medflux > 0 else 99.999) sbE[sa_i].append((2.5 * scatflux / (np.sqrt(np.sum(aselect)) * medflux * np.log(10))) if medflux > 0 else 99.999) newprofheader = results['prof header'] newprofunits = results['prof units'] newprofformat = results['prof format'] newprofdata = results['prof data'] for sa_i in range(len(wedgeangles)): p1, p2 = ('SB_rad[%.1f]' % (wedgeangles[sa_i] * 180 / np.pi), 'SB_rad_e[%.1f]' % (wedgeangles[sa_i] * 180 / np.pi)) newprofheader.append(p1) newprofheader.append(p2) newprofunits[p1] = 'mag*arcsec^-2' newprofunits[p2] = 'mag*arcsec^-2' newprofformat[p1] = '%.4f' newprofformat[p2] = '%.4f' newprofdata[p1] = sb[sa_i] newprofdata[p2] = sbE[sa_i] if 'ap_doplot' in options and options['ap_doplot']: CHOOSE = SBE < 0.2 firstbad = np.argmax(np.logical_not(CHOOSE)) if firstbad > 3: CHOOSE[firstbad:] = False ranges = [ [ max(0, int(results['center']['x'] - 1.5 * R[CHOOSE][-1] - 2)), min(IMG.shape[1], int(results['center']['x'] + 1.5 * R[CHOOSE][-1] + 2)) ], [ max(0, int(results['center']['y'] - 1.5 * R[CHOOSE][-1] - 2)), min(IMG.shape[0], int(results['center']['y'] + 1.5 * R[CHOOSE][-1] + 2)) ] ] # cmap = matplotlib.cm.get_cmap('tab10' if nwedges <= 10 else 'viridis') # colorind = np.arange(nwedges)/10 cmap = matplotlib.cm.get_cmap('hsv') colorind = (np.linspace(0, 1 - 1 / nwedges, nwedges) + 0.1) % 1 for sa_i in range(len(wedgeangles)): CHOOSE = np.logical_and( np.array(sb[sa_i]) < 99, np.array(sbE[sa_i]) < 1) plt.errorbar(np.array(R)[CHOOSE] * options['ap_pixscale'], np.array(sb[sa_i])[CHOOSE], yerr=np.array(sbE[sa_i])[CHOOSE], elinewidth=1, linewidth=0, marker='.', markersize=5, color=cmap(colorind[sa_i]), label='Wedge %.2f' % (wedgeangles[sa_i] * 180 / np.pi)) plt.xlabel('Radius [arcsec]', fontsize=16) plt.ylabel('Surface Brightness [mag arcsec$^{-2}$]', fontsize=16) bkgrdnoise = -2.5 * np.log10( results['background noise']) + zeropoint + 2.5 * np.log10( options['ap_pixscale']**2) plt.axhline(bkgrdnoise, color='purple', linewidth=0.5, linestyle='--', label='1$\\sigma$ noise/pixel:\n%.1f mag arcsec$^{-2}$' % bkgrdnoise) plt.gca().invert_yaxis() plt.legend(fontsize=15) plt.tick_params(labelsize=14) plt.tight_layout() if not ('ap_nologo' in options and options['ap_nologo']): AddLogo(plt.gcf()) plt.savefig( '%sradial_profiles_%s.jpg' % (options['ap_plotpath'] if 'ap_plotpath' in options else '', options['ap_name']), dpi=options['ap_plotdpi'] if 'ap_plotdpi' in options else 300) plt.close() LSBImage(dat[ranges[1][0]:ranges[1][1], ranges[0][0]:ranges[0][1]], results['background noise']) # plt.imshow(np.clip(dat[ranges[1][0]: ranges[1][1], ranges[0][0]: ranges[0][1]], # a_min = 0,a_max = None), origin = 'lower', cmap = 'Greys_r', norm = ImageNormalize(stretch=LogStretch())) cx, cy = (results['center']['x'] - ranges[0][0], results['center']['y'] - ranges[1][0]) for sa_i in range(len(wedgeangles)): endx, endy = (R * np.cos(wedgeangles[sa_i] + pa), R * np.sin(wedgeangles[sa_i] + pa)) plt.plot(endx + cx, endy + cy, color='w', linewidth=1.1) plt.plot(endx + cx, endy + cy, color=cmap(colorind[sa_i]), linewidth=0.7) endx, endy = (R * np.cos(wedgeangles[sa_i] + pa + wedgewidth / 2), R * np.sin(wedgeangles[sa_i] + pa + wedgewidth / 2)) plt.plot(endx + cx, endy + cy, color='w', linewidth=0.7) plt.plot(endx + cx, endy + cy, color=cmap(colorind[sa_i]), linestyle='--', linewidth=0.5) endx, endy = (R * np.cos(wedgeangles[sa_i] + pa - wedgewidth / 2), R * np.sin(wedgeangles[sa_i] + pa - wedgewidth / 2)) plt.plot(endx + cx, endy + cy, color='w', linewidth=0.7) plt.plot(endx + cx, endy + cy, color=cmap(colorind[sa_i]), linestyle='--', linewidth=0.5) plt.xlim([0, ranges[0][1] - ranges[0][0]]) plt.ylim([0, ranges[1][1] - ranges[1][0]]) if not ('ap_nologo' in options and options['ap_nologo']): AddLogo(plt.gcf()) plt.savefig( '%sradial_profiles_wedges_%s.jpg' % (options['ap_plotpath'] if 'ap_plotpath' in options else '', options['ap_name']), dpi=options['ap_plotdpi'] if 'ap_plotdpi' in options else 300) plt.close() return IMG, { 'prof header': newprofheader, 'prof units': newprofunits, 'prof data': newprofdata, 'prof format': newprofformat }
def Isophote_Fit_FFT_mean(IMG, results, options): """Fit elliptical isophotes to a galaxy image using FFT coefficients and regularization. Same as the standard isophote fitting routine, except uses less robust mean/std measures. This is only intended for low S/N data where pixels have low integer counts. Parameters ----------------- ap_scale : float, default 0.2 growth scale when fitting isophotes, not the same as *ap_sample---scale*. ap_fit_limit : float, default 2 noise level out to which to extend the fit in units of pixel background noise level. Default is 2, smaller values will end fitting further out in the galaxy image. ap_regularize_scale : float, default 1 scale factor to apply to regularization coupling factor between isophotes. Default of 1, larger values make smoother fits, smaller values give more chaotic fits. Notes ---------- :References: - 'background' - 'background noise' - 'center' - 'psf fwhm' - 'init ellip' - 'init pa' Returns ------- IMG : ndarray Unaltered galaxy image results : dict .. code-block:: python {'fit ellip': , # array of ellipticity values (ndarray) 'fit pa': , # array of PA values (ndarray) 'fit R': , # array of semi-major axis values (ndarray) 'fit ellip_err': , # optional, array of ellipticity error values (ndarray) 'fit pa_err': , # optional, array of PA error values (ndarray) 'auxfile fitlimit': # optional, auxfile message (string) } """ if "ap_scale" in options: scale = options["ap_scale"] else: scale = 0.2 # subtract background from image during processing dat = IMG - results["background"] mask = results["mask"] if "mask" in results else None if not np.any(mask): mask = None # Determine sampling radii ###################################################################### shrink = 0 while shrink < 5: sample_radii = [3 * results["psf fwhm"] / 2] while sample_radii[-1] < (max(IMG.shape) / 2): isovals = _iso_extract( dat, sample_radii[-1], { "ellip": results["init ellip"], "pa": results["init pa"] }, results["center"], more=False, mask=mask, ) if (np.mean(isovals) < (options["ap_fit_limit"] if "ap_fit_limit" in options else 1) * results["background noise"]): break sample_radii.append(sample_radii[-1] * (1.0 + scale / (1.0 + shrink))) if len(sample_radii) < 15: shrink += 1 else: break if shrink >= 5: raise Exception( "Unable to initialize ellipse fit, check diagnostic plots. Possible missed center." ) ellip = np.ones(len(sample_radii)) * results["init ellip"] pa = np.ones(len(sample_radii)) * results["init pa"] logging.debug("%s: sample radii: %s" % (options["ap_name"], str(sample_radii))) # Fit isophotes ###################################################################### perturb_scale = np.array([0.03, 0.06]) regularize_scale = (options["ap_regularize_scale"] if "ap_regularize_scale" in options else 1.0) N_perturb = 5 count = 0 count_nochange = 0 use_center = copy(results["center"]) I = np.array(range(len(sample_radii))) while count < 300 and count_nochange < (3 * len(sample_radii)): # Periodically include logging message if count % 10 == 0: logging.debug("%s: count: %i" % (options["ap_name"], count)) count += 1 np.random.shuffle(I) for i in I: perturbations = [] perturbations.append({"ellip": copy(ellip), "pa": copy(pa)}) perturbations[-1]["loss"] = _FFT_mean_loss( dat, sample_radii, perturbations[-1]["ellip"], perturbations[-1]["pa"], i, use_center, results["background noise"], mask=mask, reg_scale=regularize_scale if count > 4 else 0, name=options["ap_name"], ) for n in range(N_perturb): perturbations.append({"ellip": copy(ellip), "pa": copy(pa)}) if count % 3 in [0, 1]: perturbations[-1]["ellip"][i] = _x_to_eps( _inv_x_to_eps(perturbations[-1]["ellip"][i]) + np.random.normal(loc=0, scale=perturb_scale[0])) if count % 3 in [1, 2]: perturbations[-1]["pa"][i] = ( perturbations[-1]["pa"][i] + np.random.normal( loc=0, scale=perturb_scale[1])) % np.pi perturbations[-1]["loss"] = _FFT_mean_loss( dat, sample_radii, perturbations[-1]["ellip"], perturbations[-1]["pa"], i, use_center, results["background noise"], mask=mask, reg_scale=regularize_scale if count > 4 else 0, name=options["ap_name"], ) best = np.argmin(list(p["loss"] for p in perturbations)) if best > 0: ellip = copy(perturbations[best]["ellip"]) pa = copy(perturbations[best]["pa"]) count_nochange = 0 else: count_nochange += 1 logging.info("%s: Completed isohpote fit in %i itterations" % (options["ap_name"], count)) # detect collapsed center ###################################################################### for i in range(5): if (_inv_x_to_eps(ellip[i]) - _inv_x_to_eps(ellip[i + 1])) > 0.5: ellip[:i + 1] = ellip[i + 1] pa[:i + 1] = pa[i + 1] # Smooth ellip and pa profile ###################################################################### smooth_ellip = copy(ellip) smooth_pa = copy(pa) ellip[:3] = min(ellip[:3]) smooth_ellip = _ellip_smooth(sample_radii, smooth_ellip, 5) smooth_pa = _pa_smooth(sample_radii, smooth_pa, 5) if "ap_doplot" in options and options["ap_doplot"]: ranges = [ [ max(0, int(use_center["x"] - sample_radii[-1] * 1.2)), min(dat.shape[1], int(use_center["x"] + sample_radii[-1] * 1.2)), ], [ max(0, int(use_center["y"] - sample_radii[-1] * 1.2)), min(dat.shape[0], int(use_center["y"] + sample_radii[-1] * 1.2)), ], ] LSBImage( dat[ranges[1][0]:ranges[1][1], ranges[0][0]:ranges[0][1]], results["background noise"], ) # plt.imshow(np.clip(dat[ranges[1][0]: ranges[1][1], ranges[0][0]: ranges[0][1]], # a_min = 0,a_max = None), origin = 'lower', cmap = 'Greys', norm = ImageNormalize(stretch=LogStretch())) for i in range(len(sample_radii)): plt.gca().add_patch( Ellipse( (use_center["x"] - ranges[0][0], use_center["y"] - ranges[1][0]), 2 * sample_radii[i], 2 * sample_radii[i] * (1.0 - ellip[i]), pa[i] * 180 / np.pi, fill=False, linewidth=((i + 1) / len(sample_radii))**2, color="r", )) if not ("ap_nologo" in options and options["ap_nologo"]): AddLogo(plt.gcf()) plt.savefig( "%sfit_ellipse_%s.jpg" % ( options["ap_plotpath"] if "ap_plotpath" in options else "", options["ap_name"], ), dpi=options["ap_plotdpi"] if "ap_plotdpi" in options else 300, ) plt.close() plt.scatter(sample_radii, ellip, color="r", label="ellip") plt.scatter(sample_radii, pa / np.pi, color="b", label="pa/$np.pi$") show_ellip = _ellip_smooth(sample_radii, ellip, deg=5) show_pa = _pa_smooth(sample_radii, pa, deg=5) plt.plot( sample_radii, show_ellip, color="orange", linewidth=2, linestyle="--", label="smooth ellip", ) plt.plot( sample_radii, show_pa / np.pi, color="purple", linewidth=2, linestyle="--", label="smooth pa/$np.pi$", ) # plt.xscale('log') plt.legend() if not ("ap_nologo" in options and options["ap_nologo"]): AddLogo(plt.gcf()) plt.savefig( "%sphaseprofile_%s.jpg" % ( options["ap_plotpath"] if "ap_plotpath" in options else "", options["ap_name"], ), dpi=options["ap_plotdpi"] if "ap_plotdpi" in options else 300, ) plt.close() # Compute errors ###################################################################### ellip_err = np.zeros(len(ellip)) ellip_err[:2] = np.sqrt(np.sum((ellip[:4] - smooth_ellip[:4])**2) / 4) ellip_err[-1] = np.sqrt(np.sum((ellip[-4:] - smooth_ellip[-4:])**2) / 4) pa_err = np.zeros(len(pa)) pa_err[:2] = np.sqrt(np.sum((pa[:4] - smooth_pa[:4])**2) / 4) pa_err[-1] = np.sqrt(np.sum((pa[-4:] - smooth_pa[-4:])**2) / 4) for i in range(2, len(pa) - 1): ellip_err[i] = np.sqrt( np.sum((ellip[i - 2:i + 2] - smooth_ellip[i - 2:i + 2])**2) / 4) pa_err[i] = np.sqrt( np.sum((pa[i - 2:i + 2] - smooth_pa[i - 2:i + 2])**2) / 4) res = { "fit ellip": ellip, "fit pa": pa, "fit R": sample_radii, "fit ellip_err": ellip_err, "fit pa_err": pa_err, "auxfile fitlimit": "fit limit semi-major axis: %.2f pix" % sample_radii[-1], } return IMG, res
def Check_Fit(IMG, results, options): """Check for cases of failed isophote fits. A variety of check methods are applied to ensure that the fit has converged to a reasonable solution. If a fit passes all of these checks then it is typically an acceptable fit. However if it fails one or more of the checks then the fit likely either failed or the galaxy has strong non-axisymmetric features (and the fit itself may be acceptable). One check samples the fitted isophotes and looks for cases with high variability of flux values along the isophote. This is done by comparing the interquartile range to the median flux, if the interquartile range is larger then that isophote is flagged. If enough isophotes are flagged then the fit may have failed. A second check operates similarly, checking the second and fourth FFT coefficient amplitudes relative to the median flux. If many of the isophotes have large FFT coefficients, or if a few of the isophotes have very large FFT coefficients then the fit is flagged as potentially failed. A third check is similar to the first, except that it compares the interquartile range from the fitted isophotes to those using just the global position angle and ellipticity values. A fourth check uses the first FFT coefficient to detect if the light is biased to one side of the galaxy. Typically this indicated either a failed center, or the galaxy has been disturbed and is not lopsided. Notes ---------- :References: - 'background' - 'background noise' - 'center' - 'init ellip' - 'init pa' - 'fit R' (optional) - 'fit ellip' (optional) - 'fit pa' (optional) - 'prof data' (optional) Returns ------- IMG : ndarray Unaltered galaxy image results : dict .. code-block:: python {'checkfit': {'isophote variability': , # True if the test was passed, False if the test failed (bool) 'FFT coefficients': , # True if the test was passed, False if the test failed (bool) 'initial fit compare': , # True if the test was passed, False if the test failed (bool) 'Light symmetry': }, # True if the test was passed, False if the test failed (bool) 'auxfile checkfit isophote variability': ,# optional aux file message for pass/fail of test (string) 'auxfile checkfit FFT coefficients': ,# optional aux file message for pass/fail of test (string) 'auxfile checkfit initial fit compare': ,# optional aux file message for pass/fail of test (string) 'auxfile checkfit Light symmetry': ,# optional aux file message for pass/fail of test (string) } """ tests = {} # subtract background from image during processing dat = IMG - results["background"] # Compare variability of flux values along isophotes ###################################################################### use_center = results["center"] count_variable = 0 count_initrelative = 0 f2_compare = [] f1_compare = [] if "fit R" in results: checkson = { "R": results["fit R"], "pa": results["fit pa"], "ellip": results["fit ellip"], } else: checkson = { "R": results["prof data"]["R"], "pa": results["prof data"]["pa"], "ellip": results["prof data"]["ellip"], } for i in range(len(checkson["R"])): init_isovals = _iso_extract( dat, checkson["R"][i], { "ellip": results["init ellip"], # fixme, use mask "pa": results["init pa"], }, use_center, ) isovals = _iso_extract( dat, checkson["R"][i], { "ellip": checkson["ellip"][i], "pa": checkson["pa"][i] }, use_center, ) coefs = fft( np.clip(isovals, a_max=np.quantile(isovals, 0.85), a_min=None)) if np.median(isovals) < (iqr(isovals) - results["background noise"]): count_variable += 1 if ((iqr(isovals) - results["background noise"]) / (np.median(isovals) + results["background noise"])) > ( iqr(init_isovals) / (np.median(init_isovals) + results["background noise"])): count_initrelative += 1 f2_compare.append( np.sum(np.abs(coefs[2])) / (len(isovals) * (max(0, np.median(isovals)) + results["background noise"]))) f1_compare.append( np.abs(coefs[1]) / (len(isovals) * (max(0, np.median(isovals)) + results["background noise"]))) f1_compare = np.array(f1_compare) f2_compare = np.array(f2_compare) if count_variable > (0.2 * len(checkson["R"])): logging.warning( "%s: Possible failed fit! flux values highly variable along isophotes" % options["ap_name"]) tests["isophote variability"] = False else: tests["isophote variability"] = True if count_initrelative > (0.5 * len(checkson["R"])): logging.warning( "%s: Possible failed fit! flux values highly variable relative to initialization" % options["ap_name"]) tests["initial fit compare"] = False else: tests["initial fit compare"] = True if (np.sum(f2_compare > 0.2) > (0.1 * len(checkson["R"])) or np.sum(f2_compare > 0.1) > (0.3 * len(checkson["R"])) or np.sum(f2_compare > 0.05) > (0.8 * len(checkson["R"]))): logging.warning( "%s: Possible failed fit! poor convergence of FFT coefficients" % options["ap_name"]) tests["FFT coefficients"] = False else: tests["FFT coefficients"] = True if (np.sum(f1_compare > 0.2) > (0.1 * len(checkson["R"])) or np.sum(f1_compare > 0.1) > (0.3 * len(checkson["R"])) or np.sum(f1_compare > 0.05) > (0.8 * len(checkson["R"]))): logging.warning( "%s: Possible failed fit! possible failed center or lopsided galaxy" % options["ap_name"]) tests["Light symmetry"] = False else: tests["Light symmetry"] = True res = {"checkfit": tests} for t in tests: res["auxfile checkfit %s" % t] = "checkfit %s: %s" % ( t, "pass" if tests[t] else "fail", ) return IMG, res
def _Generate_Profile(IMG, results, R, parameters, options): # Create image array with background and mask applied try: if np.any(results["mask"]): mask = results["mask"] else: mask = None except: mask = None dat = IMG - results["background"] zeropoint = options["ap_zeropoint"] if "ap_zeropoint" in options else 22.5 fluxunits = options["ap_fluxunits"] if "ap_fluxunits" in options else "mag" for p in range(len(parameters)): # Indicate no Fourier modes if supplied parameters does not include it if not "m" in parameters[p]: parameters[p]["m"] = None if not "C" in parameters[p]: parameters[p]["C"] = None # If no ellipticity error supplied, assume zero if not "ellip err" in parameters[p]: parameters[p]["ellip err"] = 0.0 # If no position angle error supplied, assume zero if not "pa err" in parameters[p]: parameters[p]["pa err"] = 0.0 sb = [] sbE = [] pixels = [] maskedpixels = [] cogdirect = [] sbfix = [] sbfixE = [] measFmodes = [] count_neg = 0 medflux = np.inf end_prof = len(R) compare_interp = [] for i in range(len(R)): if "ap_isoband_fixed" in options and options["ap_isoband_fixed"]: isobandwidth = ( options["ap_isoband_width"] if "ap_isoband_width" in options else 0.5 ) else: isobandwidth = R[i] * ( options["ap_isoband_width"] if "ap_isoband_width" in options else 0.025 ) isisophoteband = False if ( medflux > ( results["background noise"] * (options["ap_isoband_start"] if "ap_isoband_start" in options else 2) ) or isobandwidth < 0.5 ): isovals = _iso_extract( dat, R[i], parameters[i], results["center"], mask=mask, more=True, rad_interp=( options["ap_iso_interpolate_start"] if "ap_iso_interpolate_start" in options else 5 ) * results["psf fwhm"], interp_method=( options["ap_iso_interpolate_method"] if "ap_iso_interpolate_method" in options else "lanczos" ), interp_window=( int(options["ap_iso_interpolate_window"]) if "ap_iso_interpolate_window" in options else 5 ), sigmaclip=options["ap_isoclip"] if "ap_isoclip" in options else False, sclip_iterations=options["ap_isoclip_iterations"] if "ap_isoclip_iterations" in options else 10, sclip_nsigma=options["ap_isoclip_nsigma"] if "ap_isoclip_nsigma" in options else 5, ) else: isisophoteband = True isovals = _iso_between( dat, R[i] - isobandwidth, R[i] + isobandwidth, parameters[i], results["center"], mask=mask, more=True, sigmaclip=options["ap_isoclip"] if "ap_isoclip" in options else False, sclip_iterations=options["ap_isoclip_iterations"] if "ap_isoclip_iterations" in options else 10, sclip_nsigma=options["ap_isoclip_nsigma"] if "ap_isoclip_nsigma" in options else 5, ) isotot = np.sum( _iso_between(dat, 0, R[i], parameters[i], results["center"], mask=mask) ) medflux = _average( isovals[0], options["ap_isoaverage_method"] if "ap_isoaverage_method" in options else "median", ) scatflux = _scatter( isovals[0], options["ap_isoaverage_method"] if "ap_isoaverage_method" in options else "median", ) if ( "ap_iso_measurecoefs" in options and not options["ap_iso_measurecoefs"] is None ): if ( mask is None and (not "ap_isoclip" in options or not options["ap_isoclip"]) and not isisophoteband ): coefs = fft(isovals[0]) else: N = max(15, int(0.9 * 2 * np.pi * R[i])) theta = np.linspace(0, 2 * np.pi * (1.0 - 1.0 / N), N) coefs = fft(np.interp(theta, isovals[1], isovals[0], period=2 * np.pi)) measFmodes.append( { "a": [np.imag(coefs[0]) / len(coefs)] + list( np.imag(coefs[np.array(options["ap_iso_measurecoefs"])]) / (np.abs(coefs[0])) ), "b": [np.real(coefs[0]) / len(coefs)] + list( np.real(coefs[np.array(options["ap_iso_measurecoefs"])]) / (np.abs(coefs[0])) ), } ) pixels.append(len(isovals[0])) maskedpixels.append(isovals[2]) if fluxunits == "intensity": sb.append(medflux / options["ap_pixscale"] ** 2) sbE.append(scatflux / np.sqrt(len(isovals[0]))) cogdirect.append(isotot) else: sb.append( flux_to_sb(medflux, options["ap_pixscale"], zeropoint) if medflux > 0 else 99.999 ) sbE.append( (2.5 * scatflux / (np.sqrt(len(isovals[0])) * medflux * np.log(10))) if medflux > 0 else 99.999 ) cogdirect.append(flux_to_mag(isotot, zeropoint) if isotot > 0 else 99.999) if medflux <= 0: count_neg += 1 if ( "ap_truncate_evaluation" in options and options["ap_truncate_evaluation"] and count_neg >= 2 ): end_prof = i + 1 break # Compute Curve of Growth from SB profile if fluxunits == "intensity": cog, cogE = Fmode_fluxdens_to_fluxsum_errorprop( R[:end_prof] * options["ap_pixscale"], np.array(sb), np.array(sbE), parameters[:end_prof], N=100, symmetric_error=True, ) if cog is None: cog = -99.999 * np.ones(len(R)) cogE = -99.999 * np.ones(len(R)) else: cog[np.logical_not(np.isfinite(cog))] = -99.999 cogE[cog < 0] = -99.999 else: cog, cogE = SBprof_to_COG_errorprop( R[:end_prof] * options["ap_pixscale"], np.array(sb), np.array(sbE), parameters[:end_prof], N=100, symmetric_error=True, ) if cog is None: cog = 99.999 * np.ones(len(R)) cogE = 99.999 * np.ones(len(R)) else: cog[np.logical_not(np.isfinite(cog))] = 99.999 cogE[cog > 99] = 99.999 # For each radius evaluation, write the profile parameters if fluxunits == "intensity": params = [ "R", "I", "I_e", "totflux", "totflux_e", "ellip", "ellip_e", "pa", "pa_e", "pixels", "maskedpixels", "totflux_direct", ] SBprof_units = { "R": "arcsec", "I": "flux*arcsec^-2", "I_e": "flux*arcsec^-2", "totflux": "flux", "totflux_e": "flux", "ellip": "unitless", "ellip_e": "unitless", "pa": "deg", "pa_e": "deg", "pixels": "count", "maskedpixels": "count", "totflux_direct": "flux", } else: params = [ "R", "SB", "SB_e", "totmag", "totmag_e", "ellip", "ellip_e", "pa", "pa_e", "pixels", "maskedpixels", "totmag_direct", ] SBprof_units = { "R": "arcsec", "SB": "mag*arcsec^-2", "SB_e": "mag*arcsec^-2", "totmag": "mag", "totmag_e": "mag", "ellip": "unitless", "ellip_e": "unitless", "pa": "deg", "pa_e": "deg", "pixels": "count", "maskedpixels": "count", "totmag_direct": "mag", } SBprof_data = dict((h, None) for h in params) SBprof_data["R"] = list(R[:end_prof] * options["ap_pixscale"]) SBprof_data["I" if fluxunits == "intensity" else "SB"] = list(sb) SBprof_data["I_e" if fluxunits == "intensity" else "SB_e"] = list(sbE) SBprof_data["totflux" if fluxunits == "intensity" else "totmag"] = list(cog) SBprof_data["totflux_e" if fluxunits == "intensity" else "totmag_e"] = list(cogE) SBprof_data["ellip"] = list(parameters[p]["ellip"] for p in range(end_prof)) SBprof_data["ellip_e"] = list(parameters[p]["ellip err"] for p in range(end_prof)) SBprof_data["pa"] = list(parameters[p]["pa"] * 180 / np.pi for p in range(end_prof)) SBprof_data["pa_e"] = list( parameters[p]["pa err"] * 180 / np.pi for p in range(end_prof) ) SBprof_data["pixels"] = list(pixels) SBprof_data["maskedpixels"] = list(maskedpixels) SBprof_data[ "totflux_direct" if fluxunits == "intensity" else "totmag_direct" ] = list(cogdirect) if "ap_iso_measurecoefs" in options and not options["ap_iso_measurecoefs"] is None: whichcoefs = [0] + list(options["ap_iso_measurecoefs"]) for i in list(range(len(whichcoefs))): aa, bb = "a%i" % whichcoefs[i], "b%i" % whichcoefs[i] params += [aa, bb] SBprof_units.update( { aa: "flux" if whichcoefs[i] == 0 else "a%i/F0" % whichcoefs[i], bb: "flux" if whichcoefs[i] == 0 else "b%i/F0" % whichcoefs[i], } ) SBprof_data[aa] = list(F["a"][i] for F in measFmodes) SBprof_data[bb] = list(F["b"][i] for F in measFmodes) if any(not p["m"] is None for p in parameters): for m in range(len(parameters[0]["m"])): AA, PP = "A%i" % parameters[0]["m"][m], "Phi%i" % parameters[0]["m"][m] params += [AA, PP] SBprof_units.update({AA: "unitless", PP: "deg"}) SBprof_data[AA] = list(p["Am"][m] for p in parameters[:end_prof]) SBprof_data[PP] = list(p["Phim"][m] for p in parameters[:end_prof]) if any(not p["C"] is None for p in parameters): params += ["C"] SBprof_units["C"] = "unitless" SBprof_data["C"] = list(p["C"] for p in parameters[:end_prof]) if "ap_doplot" in options and options["ap_doplot"]: Plot_Phase_Profile( np.array(SBprof_data["R"]), parameters[:end_prof], results, options ) if fluxunits == "intensity": Plot_I_Profile( dat, np.array(SBprof_data["R"]), np.array(SBprof_data["I"]), np.array(SBprof_data["I_e"]), parameters[:end_prof], results, options, ) else: Plot_SB_Profile( dat, np.array(SBprof_data["R"]), np.array(SBprof_data["SB"]), np.array(SBprof_data["SB_e"]), parameters[:end_prof], results, options, ) return {"prof header": params, "prof units": SBprof_units, "prof data": SBprof_data}
def Center_HillClimb_mean(IMG, results, options): """Follow locally increasing brightness (robust to PSF size objects) to find peak. Using 10 circular isophotes out to 10 times the PSF length, the first FFT coefficient phases are averaged to find the direction of increasing flux. Flux values are sampled along this direction and a quadratic fit gives the maximum. This is iteratively repeated until the step size becomes very small. This function is identical to :func:`~autoprofutils.Center.Center_HillClimb` except that all averages/scatters are mean/std based instead of median/iqr based. Parameters ----------------- ap_guess_center : dict, default None user provided starting point for center fitting. Center should be formatted as: .. code-block:: python {'x':float, 'y': float} , where the floats are the center coordinates in pixels. If not given, Autoprof will default to a guess of the image center. ap_set_center : dict, default None user provided fixed center for rest of calculations. Center should be formatted as: .. code-block:: python {'x':float, 'y': float} , where the floats are the center coordinates in pixels. If not given, Autoprof will default to a guess of the image center. ap_centeringring : int, default 10 Size of ring to use when finding galaxy center, in units of PSF. Larger rings will be robust to features (i.e., foreground stars), while smaller rings may be needed for small galaxies. Notes ---------- :References: - 'background' - 'background noise' - 'psf fwhm' Returns ------- IMG : ndarray Unaltered galaxy image results : dict .. code-block:: python {'center': {'x': , # x coordinate of the center (pix) 'y': }, # y coordinate of the center (pix) 'auxfile center': # optional, message for aux file to record galaxy center (string) 'auxfile centeral sb': # optional, central surface brightness value (float) } """ current_center = {"x": IMG.shape[0] / 2, "y": IMG.shape[1] / 2} current_center = {"x": IMG.shape[1] / 2, "y": IMG.shape[0] / 2} if "ap_guess_center" in options: current_center = deepcopy(options["ap_guess_center"]) logging.info("%s: Center initialized by user: %s" % (options["ap_name"], str(current_center))) if "ap_set_center" in options: logging.info("%s: Center set by user: %s" % (options["ap_name"], str(options["ap_set_center"]))) return IMG, {"center": deepcopy(options["ap_set_center"])} dat = IMG - results["background"] searchring = (int(options["ap_centeringring"]) if "ap_centeringring" in options else 10) sampleradii = np.linspace(1, searchring, searchring) * results["psf fwhm"] track_centers = [] small_update_count = 0 total_count = 0 while small_update_count <= 5 and total_count <= 100: total_count += 1 phases = [] isovals = [] coefs = [] for r in sampleradii: isovals.append( _iso_extract(dat, r, { "ellip": 0.0, "pa": 0.0 }, current_center, more=True)) coefs.append(fft(isovals[-1][0])) phases.append((-np.angle(coefs[-1][1])) % (2 * np.pi)) direction = Angle_Median(phases) % (2 * np.pi) levels = [] level_locs = [] for i, r in enumerate(sampleradii): floc = np.argmin(np.abs(isovals[i][1] - direction)) rloc = np.argmin( np.abs(isovals[i][1] - ((direction + np.pi) % (2 * np.pi)))) smooth = np.abs( ifft(coefs[i][:min(10, len(coefs[i]))], n=len(coefs[i]))) levels.append(smooth[floc]) level_locs.append(r) levels.insert(0, smooth[rloc]) level_locs.insert(0, -r) try: p = np.polyfit(level_locs, levels, deg=2) if p[0] < 0 and len(levels) > 3: dist = np.clip(-p[1] / (2 * p[0]), a_min=min(level_locs), a_max=max(level_locs)) else: dist = level_locs[np.argmax(levels)] except: dist = results["psf fwhm"] current_center["x"] += dist * np.cos(direction) current_center["y"] += dist * np.sin(direction) if abs(dist) < (0.25 * results["psf fwhm"]): small_update_count += 1 else: small_update_count = 0 track_centers.append([current_center["x"], current_center["y"]]) # refine center res = minimize( _hillclimb_mean_loss, x0=[current_center["x"], current_center["y"]], args=(dat, results["psf fwhm"], results["background noise"]), method="Nelder-Mead", ) if res.success: current_center["x"] = res.x[0] current_center["y"] = res.x[1] return IMG, { "center": current_center, "auxfile center": "center x: %.2f pix, y: %.2f pix" % (current_center["x"], current_center["y"]), }
def _FFT_Robust_loss(dat, R, PARAMS, i, C, noise, mask=None, reg_scale=1.0, robust_clip=0.15, fit_coefs=None, name=""): isovals = _iso_extract( dat, R[i], PARAMS[i], C, mask=mask, interp_mask=False if mask is None else True, interp_method="bicubic", ) coefs = fft( np.clip(isovals, a_max=np.quantile(isovals, 1. - robust_clip), a_min=None)) if fit_coefs is None: f2_loss = np.abs(coefs[2]) / ( len(isovals) * (max(0, np.median(isovals)) + noise / np.sqrt(len(isovals)))) else: f2_loss = np.sum(np.abs(coefs[np.array(fit_coefs)])) / ( len(fit_coefs) * len(isovals) * (max(0, np.median(isovals)) + noise / np.sqrt(len(isovals)))) reg_loss = 0 if not PARAMS[i]["m"] is None: fmode_scale = 1.0 / len(PARAMS[i]["m"]) if i < (len(R) - 1): reg_loss += abs((PARAMS[i]["ellip"] - PARAMS[i + 1]["ellip"]) / (1 - PARAMS[i + 1]["ellip"])) reg_loss += abs( Angle_TwoAngles_sin(PARAMS[i]["pa"], PARAMS[i + 1]["pa"]) / (0.2)) if not PARAMS[i]["m"] is None: for m in range(len(PARAMS[i]["m"])): reg_loss += fmode_scale * abs( (PARAMS[i]["Am"][m] - PARAMS[i + 1]["Am"][m]) / 0.2) reg_loss += fmode_scale * abs( Angle_TwoAngles_cos( PARAMS[i]["m"][m] * PARAMS[i]["Phim"][m], PARAMS[i + 1]["m"][m] * PARAMS[i + 1]["Phim"][m], ) / (PARAMS[i]["m"][m] * 0.1)) if not PARAMS[i]["C"] is None: reg_loss += abs(np.log10( PARAMS[i]["C"] / PARAMS[i + 1]["C"])) / 0.1 if i > 0: reg_loss += abs((PARAMS[i]["ellip"] - PARAMS[i - 1]["ellip"]) / (1 - PARAMS[i - 1]["ellip"])) reg_loss += abs( Angle_TwoAngles_sin(PARAMS[i]["pa"], PARAMS[i - 1]["pa"]) / (0.2)) if not PARAMS[i]["m"] is None: for m in range(len(PARAMS[i]["m"])): reg_loss += fmode_scale * abs( (PARAMS[i]["Am"][m] - PARAMS[i - 1]["Am"][m]) / 0.2) reg_loss += fmode_scale * abs( Angle_TwoAngles_cos( PARAMS[i]["m"][m] * PARAMS[i]["Phim"][m], PARAMS[i - 1]["m"][m] * PARAMS[i - 1]["Phim"][m], ) / (PARAMS[i]["m"][m] * 0.1)) if not PARAMS[i]["C"] is None: reg_loss += abs(np.log10( PARAMS[i]["C"] / PARAMS[i - 1]["C"])) / 0.1 return f2_loss * (1 + reg_loss * reg_scale)
def Isophote_Initialize(IMG, results, options): """ Determine the global pa and ellipticity for a galaxy. First grow circular isophotes until reaching near the noise floor, then evaluate the phase of the second FFT coefficients and determine the average direction. Then fit an ellipticity for one of the outer isophotes. """ ###################################################################### # Initial attempt to find size of galaxy in image # based on when isophotes SB values start to get # close to the background noise level circ_ellipse_radii = [results['psf fwhm']] allphase = [] dat = IMG - results['background'] while circ_ellipse_radii[-1] < (len(IMG) / 2): circ_ellipse_radii.append(circ_ellipse_radii[-1] * (1 + 0.2)) isovals = _iso_extract(dat, circ_ellipse_radii[-1], 0., 0., results['center'], more=True, sigmaclip=True, sclip_nsigma=3, interp_mask=True) coefs = fft(isovals[0]) allphase.append(coefs[2]) # Stop when at 3 time background noise if np.quantile(isovals[0], 0.8) < (3 * results['background noise'] ) and len(circ_ellipse_radii) > 4: break logging.info('%s: init scale: %f pix' % (options['ap_name'], circ_ellipse_radii[-1])) # Find global position angle. phase = (-Angle_Median(np.angle(allphase[-5:])) / 2) % np.pi # Find global ellipticity test_ellip = np.linspace(0.05, 0.95, 15) test_f2 = [] for e in test_ellip: test_f2.append( sum( list( _fitEllip_loss(e, dat, circ_ellipse_radii[-2] * m, phase, results['center'], results['background noise']) for m in np.linspace(0.8, 1.2, 5)))) ellip = test_ellip[np.argmin(test_f2)] res = minimize(lambda e, d, r, p, c, n: sum( list( _fitEllip_loss(_x_to_eps(e[0]), d, r * m, p, c, n) for m in np.linspace(0.8, 1.2, 5))), x0=_inv_x_to_eps(ellip), args=(dat, circ_ellipse_radii[-2], phase, results['center'], results['background noise']), method='Nelder-Mead', options={ 'initial_simplex': [[_inv_x_to_eps(ellip) - 1 / 15], [_inv_x_to_eps(ellip) + 1 / 15]] }) if res.success: logging.debug( '%s: using optimal ellipticity %.3f over grid ellipticity %.3f' % (options['ap_name'], _x_to_eps(res.x[0]), ellip)) ellip = _x_to_eps(res.x[0]) # Compute the error on the parameters ###################################################################### RR = np.linspace(circ_ellipse_radii[-2] - results['psf fwhm'], circ_ellipse_radii[-2] + results['psf fwhm'], 10) errallphase = [] for rr in RR: isovals = _iso_extract(dat, rr, 0., 0., results['center'], more=True, sigmaclip=True, sclip_nsigma=3, interp_mask=True) coefs = fft(isovals[0]) errallphase.append(coefs[2]) sample_pas = (-np.angle(1j * np.array(errallphase) / np.mean(errallphase)) / 2) % np.pi pa_err = iqr(sample_pas, rng=[16, 84]) / 2 res_multi = map( lambda rrp: minimize(lambda e, d, r, p, c, n: _fitEllip_loss( _x_to_eps(e[0]), d, r, p, c, n), x0=_inv_x_to_eps(ellip), args=(dat, rrp[0], rrp[1], results['center'], results['background noise']), method='Nelder-Mead', options={ 'initial_simplex': [[ _inv_x_to_eps(ellip) - 1 / 15 ], [_inv_x_to_eps(ellip) + 1 / 15]] }), zip(RR, sample_pas)) ellip_err = iqr(list(_x_to_eps(rm.x[0]) for rm in res_multi), rng=[16, 84]) / 2 circ_ellipse_radii = np.array(circ_ellipse_radii) if 'ap_doplot' in options and options['ap_doplot']: ranges = [ [ max(0, int(results['center']['x'] - circ_ellipse_radii[-1] * 1.5)), min(dat.shape[1], int(results['center']['x'] + circ_ellipse_radii[-1] * 1.5)) ], [ max(0, int(results['center']['y'] - circ_ellipse_radii[-1] * 1.5)), min(dat.shape[0], int(results['center']['y'] + circ_ellipse_radii[-1] * 1.5)) ] ] LSBImage(dat[ranges[1][0]:ranges[1][1], ranges[0][0]:ranges[0][1]], results['background noise']) # plt.imshow(np.clip(dat[ranges[1][0]: ranges[1][1], ranges[0][0]: ranges[0][1]],a_min = 0, a_max = None), # origin = 'lower', cmap = 'Greys_r', norm = ImageNormalize(stretch=LogStretch())) plt.gca().add_patch( Ellipse((results['center']['x'] - ranges[0][0], results['center']['y'] - ranges[1][0]), 2 * circ_ellipse_radii[-1], 2 * circ_ellipse_radii[-1] * (1. - ellip), phase * 180 / np.pi, fill=False, linewidth=1, color='y')) plt.plot([results['center']['x'] - ranges[0][0]], [results['center']['y'] - ranges[1][0]], marker='x', markersize=3, color='r') if not ('ap_nologo' in options and options['ap_nologo']): AddLogo(plt.gcf()) plt.savefig( '%sinitialize_ellipse_%s.jpg' % (options['ap_plotpath'] if 'ap_plotpath' in options else '', options['ap_name']), dpi=options['ap_plotdpi'] if 'ap_plotdpi' in options else 300) plt.close() fig, ax = plt.subplots(2, 1, figsize=(6, 6)) plt.subplots_adjust(hspace=0.01, wspace=0.01) ax[0].plot(circ_ellipse_radii[:-1], ((-np.angle(allphase) / 2) % np.pi) * 180 / np.pi, color='k') ax[0].axhline(phase * 180 / np.pi, color='r') ax[0].axhline((phase + pa_err) * 180 / np.pi, color='r', linestyle='--') ax[0].axhline((phase - pa_err) * 180 / np.pi, color='r', linestyle='--') #ax[0].axvline(circ_ellipse_radii[-2], color = 'orange', linestyle = '--') ax[0].set_xlabel('Radius [pix]', fontsize=16) ax[0].set_ylabel('FFT$_{1}$ phase [deg]', fontsize=16) ax[0].tick_params(labelsize=12) ax[1].plot(test_ellip, test_f2, color='k') ax[1].axvline(ellip, color='r') ax[1].axvline(ellip + ellip_err, color='r', linestyle='--') ax[1].axvline(ellip - ellip_err, color='r', linestyle='--') ax[1].set_xlabel('Ellipticity [1 - b/a]', fontsize=16) ax[1].set_ylabel('Loss [FFT$_{2}$/med(flux)]', fontsize=16) ax[1].tick_params(labelsize=14) plt.tight_layout() if not ('ap_nologo' in options and options['ap_nologo']): AddLogo(plt.gcf()) plt.savefig( '%sinitialize_ellipse_optimize_%s.jpg' % (options['ap_plotpath'] if 'ap_plotpath' in options else '', options['ap_name']), dpi=options['ap_plotdpi'] if 'ap_plotdpi' in options else 300) plt.close() auxmessage = 'global ellipticity: %.3f +- %.3f, pa: %.3f +- %.3f deg, size: %f pix' % ( ellip, ellip_err, PA_shift_convention(phase) * 180 / np.pi, pa_err * 180 / np.pi, circ_ellipse_radii[-2]) return IMG, { 'init ellip': ellip, 'init ellip_err': ellip_err, 'init pa': phase, 'init pa_err': pa_err, 'init R': circ_ellipse_radii[-2], 'auxfile initialize': auxmessage }
def Radial_Profiles(IMG, results, options): """Extracts SB profiles along lines radiating from the galaxy center. For some applications, such as examining edge on galaxies, it is beneficial to observe the structure in a disk as well as (or instead of) the average isophotal profile. This can done with radial profiles which sample along lines radiating form the galaxy center. These lines are by default placed on the 4 semi-axes of the galaxy. The lines are actually wedges with increasing width as a function of radius. This helps keep roughly constant S/N in the bins, allowing the profile to extend far into the outskirts of a galaxy. The user may increase the number of wedgest to extract more stucture from the galaxy, however at some point the wedges will begin to cross each other. AutoProf will warn the user when this happens, but will carry on anyway. Parameters ----------------- ap_radialprofiles_nwedges : int, default 4 number of radial wedges to sample. Recommended choosing a power of 2. ap_radialprofiles_width : float, default 15 User set width of radial sampling wedges in degrees. ap_radialprofiles_pa : float, default 0 user set position angle at which to measure radial wedges relative to the global position angle, in degrees. ap_radialprofiles_expwidth : bool, default False Tell AutoProf to use exponentially increasing widths for radial samples. In this case *ap_radialprofiles_width* corresponds to the final width of the radial sampling. ap_radialprofiles_variable_pa : bool, default False Tell AutoProf to rotate radial sampling wedges with the position angle profile of the galaxy. Notes ---------- :References: - 'prof header' (optional) - 'prof units' (optional) - 'prof data' (optional) - 'mask' (optional) - 'background' - 'center' - 'init pa' (optional) Returns ------- IMG : ndarray Unaltered galaxy image results : dict No results provided as this method writes its own profile .. code-block:: python {'prof header': , # Previously extracted SB profile, with extra columns appended for radial profiles (list) 'prof units': , # Previously extracted SB profile, with extra units appended for radial profiles (dict) 'prof data': # Previously extracted SB profile, with extra columns appended for radial profiles (dict) } """ mask = results["mask"] if "mask" in results else None nwedges = (options["ap_radialprofiles_nwedges"] if "ap_radialprofiles_nwedges" in options else 4) wedgeangles = np.linspace(0, 2 * np.pi * (1 - 1.0 / nwedges), nwedges) zeropoint = options["ap_zeropoint"] if "ap_zeropoint" in options else 22.5 if "prof data" in results: R = np.array(results["prof data"]["R"]) / options["ap_pixscale"] else: startR = (options["ap_sampleinitR"] if "ap_sampleinitR" in options else min(1.0, results["psf fwhm"] / 2)) endR = (options["ap_sampleendR"] if "ap_sampleendR" in options else min(max(IMG.shape) / np.sqrt(2), 3 * results["init R"])) R = np.logspace( np.log10(startR), np.log10(endR), int( np.log10(endR / startR) / np.log10(1 + (options["ap_samplegeometricscale"] if "ap_samplegeometricscale" in options else 0.1))), ) if ("ap_radialprofiles_variable_pa" in options and options["ap_radialprofiles_variable_pa"]): pa = np.array(results["prof data"]["pa"]) * np.pi / 180 else: pa = np.ones(len(R)) * ( (options["ap_radialprofiles_pa"] * np.pi / 180) if "ap_radialprofiles_pa" in options else results["init pa"]) dat = IMG - results["background"] maxwedgewidth = (options["ap_radialprofiles_width"] if "ap_radialprofiles_width" in options else 15.0) maxwedgewidth *= np.pi / 180 if ("ap_radialprofiles_expwidth" in options and options["ap_radialprofiles_expwidth"]): wedgewidth = maxwedgewidth * np.exp(R / R[-1] - 1) else: wedgewidth = np.ones(len(R)) * maxwedgewidth if wedgewidth[-1] * nwedges > 2 * np.pi: logging.warning( "%s: Radial sampling wedges are overlapping! %i wedges with a maximum width of %.3f rad" % (nwedges, wedgewidth[-1])) sb = list([] for i in wedgeangles) sbE = list([] for i in wedgeangles) avgmedflux = np.inf for i in range(len(R)): isobandwidth = R[i] * (options["ap_isoband_width"] if "ap_isoband_width" in options else 0.025) if (avgmedflux > (results["background noise"] * (options["ap_isoband_start"] if "ap_isoband_start" in options else 2)) or isobandwidth < 0.5): isovals = list( _iso_extract( dat, R[i], { "ellip": 0, "pa": 0 }, results["center"], more=True, minN=int(5 * 2 * np.pi / wedgewidth[i]), mask=mask, )) else: isovals = list( _iso_between( dat, R[i] - isobandwidth, R[i] + isobandwidth, { "ellip": 0, "pa": 0 }, results["center"], more=True, mask=mask, )) isovals[1] -= pa[i] avgmedflux = [] for sa_i in range(len(wedgeangles)): aselect = np.abs(Angle_TwoAngles_cos( wedgeangles[sa_i], isovals[1])) < (wedgewidth[i] / 2) if np.sum(aselect) == 0: sb[sa_i].append(99.999) sbE[sa_i].append(99.999) continue medflux = _average( isovals[0][aselect], options["ap_isoaverage_method"] if "ap_isoaverage_method" in options else "median", ) avgmedflux.append(medflux) scatflux = _scatter( isovals[0][aselect], options["ap_isoaverage_method"] if "ap_isoaverage_method" in options else "median", ) sb[sa_i].append( flux_to_sb(medflux, options["ap_pixscale"], zeropoint ) if medflux > 0 else 99.999) sbE[sa_i].append((2.5 * scatflux / (np.sqrt(np.sum(aselect)) * medflux * np.log(10))) if medflux > 0 else 99.999) avgmedflux = np.mean(avgmedflux) if "prof header" in results: newprofheader = results["prof header"] newprofunits = results["prof units"] newprofdata = results["prof data"] else: newprofheader = ["R"] newprofunits = {"R": "arcsec"} newprofdata = {"R": R * options["ap_pixscale"]} for sa_i in range(len(wedgeangles)): p1, p2 = ( "SB_rad[%.1f]" % (wedgeangles[sa_i] * 180 / np.pi), "SB_rad_e[%.1f]" % (wedgeangles[sa_i] * 180 / np.pi), ) newprofheader.append(p1) newprofheader.append(p2) newprofunits[p1] = "mag*arcsec^-2" newprofunits[p2] = "mag*arcsec^-2" newprofdata[p1] = sb[sa_i] newprofdata[p2] = sbE[sa_i] if "ap_doplot" in options and options["ap_doplot"]: Plot_Radial_Profiles(dat, R, sb, sbE, pa, nwedges, wedgeangles, wedgewidth, results, options) return IMG, { "prof header": newprofheader, "prof units": newprofunits, "prof data": newprofdata, }