Example #1
0
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
Example #2
0
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
Example #3
0
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))
Example #4
0
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)
Example #5
0
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
Example #6
0
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)
Example #7
0
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
Example #8
0
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)
Example #9
0
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
    }
Example #10
0
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'])}
Example #11
0
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
Example #12
0
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'])}
Example #13
0
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,
    }
Example #14
0
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,
    }
Example #15
0
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
Example #16
0
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
Example #17
0
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
Example #18
0
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))
Example #19
0
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,
    }
Example #20
0
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,
    }
Example #21
0
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,
    }
Example #22
0
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
    }
Example #23
0
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
Example #24
0
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
Example #25
0
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}
Example #26
0
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"]),
    }
Example #27
0
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)
Example #28
0
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
    }
Example #29
0
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,
    }