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 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 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 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"]), }