import os
import numpy as np
from numba import njit,types,from_dtype
from numba.experimental import jitclass
from numba_kdtree import KDTree
#from . import models_numba as mnb, utils_numba as utils
import models_numba_static as mnb, utils_numba_static as utils

# Fit a PSF model to multiple stars in an image

from numba.pycc import CC
cc = CC('_getpsf_numba_static')

@njit
@cc.export('starcube', '(f8[:,:],f8[:,:],f8[:,:],i8,f8)')
def starcube(tab,image,error,npix=51,fillvalue=np.nan):
    """
    Produce a cube of cutouts of stars.

    Parameters
    ----------
    tab : numpy array
       The catalog of stars to use, [3,Nstars].  With the columns being PEAK, X, Y.
    image : CCDData object
       The image to use to generate the stellar images.
    fillvalue : float, optional
       The fill value to use for pixels that are bad are off the image.
            Default is np.nan.

    Returns
    -------
    cube : numpy array
       Two-dimensional cube (Npix,Npix,Nstars) of the star images.

    Example
    -------

    cube = starcube(tab,image)

    """

    # Get the residuals data
    nstars = len(tab)
    nhpix = npix//2
    cube = np.zeros((npix,npix,nstars),float)
    xx,yy = utils.meshgrid(np.arange(npix)-nhpix,np.arange(npix)-nhpix)
    rr = np.sqrt(xx**2+yy**2)        
    x = xx[0,:]
    y = yy[:,0]
    for i in range(nstars):
        #xcen = tab['x'][i]
        #ycen = tab['y'][i]
        xcen = tab[1][i]
        ycen = tab[2][i]
        bbox = mnb.starbbox((xcen,ycen),image.shape,nhpix)
        flux = bbox.slice(image)
        err = bbox.slice(error)
        #amp = tab['peak'][i]
        amp = tab[0][i]
        xim,yim = bbox.xy()
        xim = xim-xcen
        yim = yim-ycen
        # Need to interpolate onto a regular grid
        im2 = np.zeros((npix,npix),float)+np.nan
        xind, = np.where((x>=bbox.ixmin-xcen) & (x<=bbox.ixmax-1-xcen))
        xmin,xmax = np.min(xind),np.max(xind)
        yind, = np.where((y>=bbox.iymin-ycen) & (y<=bbox.iymax-1-ycen))
        ymin,ymax = np.min(yind),np.max(yind)
        # xx/yy are relative coordinates for the final interpolated image
        #       i.e. [-3, -2, -1, 0, 1, 2, 3]
        # xim/yim are relative coordinates for the original flux image
        # for the interpolation we need coordinates relative to to (0,0) of the
        #    flux image
        fnx = xmax-xmin+1
        fny = ymax-ymin+1
        fxx = xx[ymin:ymax+1,xmin:xmax+1]-xim[0,0]   # relative to flux origin
        fyy = yy[ymin:ymax+1,xmin:xmax+1]-yim[0,0]
        fim = utils.alinearinterp(flux,fxx,fyy)
        fim = fim.reshape((fny,fnx))
        im2[ymin:ymax+1,xmin:xmax+1] = fim
        # Stuff it into 3D array
        cube[:,:,i] = im2
    return cube


@njit
@cc.export('mkempirical', '(f8[:,:,:],i4,f8[:,:],UniTuple(i8,2),b1)')
def mkempirical(cube,order=0,coords=None,shape=None,lookup=False):
    """
    Take a star cube and collapse it to make an empirical PSF using median
    and outlier rejection.

    Parameters
    ----------
    cube : numpy array
      Three-dimensional cube of star images (or residual images) of shape
        (Npix,Npix,Nstars).
    order : int, optional
      The order of the variations. 0-constant, 1-linear terms.  If order=1,
        Then coords and shape must be input.
    coords : tuple, optional
      Two-element tuple of the X/Y coordinates of the stars.  This is needed
        to generate the linear empirical model (order=1).
    shape : tuple, optional
      Two-element tuple giving the shape (Ny,Nx) of the image.  This is
        needed to generate the linear empirical model (order=1).
    lookup : boolean, optional
      Parameter to indicate if this is a lookup table.  If lookup=False, then
      the constant term is constrained to be non-negative. Default is False.

    Returns
    -------
    epsf : numpy array
      The empirical PSF model  If order=0, then this is just a 2D image.  If
        order=1, then it will be a 3D cube (Npix,Npix,4) where the four terms
        are [constant, X-term, Y-term, X*Y-term].  If rect=True, then a list
        of RectBivariateSpline functions are returned.

    Example
    -------

    epsf = mkempirical(cube,order=0)

    or

    epsf = mkempirical(cube,order=1,coords=coords,shape=im.shape)

    """

    ny,nx,nstar = cube.shape
    npix = ny
    nhpix = ny//2
    
    # Do outlier rejection in each pixel
    med = utils.median3d(cube,axis=2,ignore_nan=True)
    bady,badx = np.where(np.isfinite(med)==False)
    if len(bady)>0:
        gdmed = utils.median(med,ignore_nan=True)
        for i in range(len(bady)):
            med[bady[i],badx[i]] = gdmed
    sig = utils.mad3d(cube,axis=2,ignore_nan=True)
    bady,badx = np.where(np.isfinite(sig)==False)
    if len(bady)>0:
        gdsig = utils.median(sig,ignore_nan=True)
        for i in range(len(bady)):
            sig[bady[i],badx[i]] = gdsig
    # Mask outlier points
    bady,badx,badk = np.where((np.abs(cube-med.reshape((med.shape)+(-1,)))>3*sig.reshape((med.shape)+(-1,)))
                              & np.isfinite(cube))
    if len(bady)>0:
        macube = cube.copy()
        for i in range(len(bady)):
            macube[bady[i],badx[i],badk[i]] = np.nan
        medim = utils.mean3d(macube,axis=2,ignore_nan=True)
        # Remove any final NaNs
        bady,badx = np.where(np.isfinite(medim)==False)
        if len(bady)>0:
            gdmed = utils.median(medim,ignore_nan=True)
            for i in range(len(bady)):
                medim[bady[i],badx[i]] = gdmed
    else:
        medim = med

    # Check how well each star fits the median
    goodpix = np.sum(np.sum(np.isfinite(macube),axis=0),axis=0)
    print('goodpix=',goodpix)
    #goodpix = macube.count(axis=(0,1))
    resid2 = (cube-medim.reshape((medim.shape)+(-1,)))**2
    mnresid2 = utils.sum2d(utils.sum3d(resid2,axis=0,ignore_nan=True),axis=0,ignore_nan=True)/goodpix
    rms = np.sqrt(mnresid2)
    print('rms=',rms)
    #print(utils.sum3d(dum,axis=0,ignore_nan=True))
    #rms = np.sqrt(np.nansum((cube-medim.reshape((medim.shape)+(-1,)))**2,axis=(0,1))/goodpix)

    xx,yy = utils.meshgrid(np.arange(npix)-nhpix,np.arange(npix)-nhpix)
    rr = np.sqrt(xx**2+yy**2)        
    x = xx[0,:]
    y = yy[:,0]
    mask = (rr<=nhpix)
    
    # Constant
    if order==0:
        # Make sure it goes to zero at large radius
        medim *= mask  # mask corners
        # Make sure values are positive
        if lookup==False:
            medim = np.maximum(medim,0.0)
            fpars = medim
        else:
            fpars = medim
        fpars = fpars.reshape((ny,nx,1))
            
    # Linear
    elif order==1:
        if coords is None or shape is None:
            raise ValueError('Need coords and shape with order=1')
        pars = np.zeros((ny,nx,4),float)
        # scale coordinates to -1 to +1
        xcen,ycen = coords[:,0],coords[:,1]
        relx,rely = mnb.relcoord(xcen,ycen,shape)
        # Loop over pixels and fit line to x/y
        for i in range(ny):
            for j in range(nx):
                data1 = cube[i,j,:]
                if np.sum(np.abs(data1)) != 0:
                    # maybe use a small maxiter
                    pars1,perror1,pcov1 = utils.poly2dfit(relx,rely,data1,data1*0+1)
                    pars[i,j,:] = pars1
        # Make sure it goes to zero at large radius
        fpars = pars.copy()
        for i in range(4):
            # Make sure edges are zero on average for higher-order terms
            gdouter, = np.where(rr.ravel()>nhpix*0.8)
            medouter = np.median(pars[:,:,i].ravel()[gdouter])
            fpars[:,:,i] -= medouter
            # Mask corners
            fpars[:,:,i] *= mask
            # Each higher-order term must have ZERO total volume
   
    return fpars,rms

# #@njit
# @cc.export('findpsfnei', '(f8[:,:],f8[:,:],i8)')
# def findpsfnei(alltab,psftab,npix):
#     """
#     Find stars near PSF stars.

#     Parameters
#     ----------
#     alltab : numpy array
#       Catalog of all sources in the image.
#     psftab : numpy array
#       Catalog of PSF stars.
#     npix : int
#       Search radius in pixels.

#     Returns
#     -------
#     indall : numpy array
#        List of indices into allcat that are neighbors to
#         the PSF stars (within radius of npix), and are not
#         PSF stars themselves.

#     Example
#     -------

#     indall = findpsfnei(alltab,psftab,npix)

#     """
#     # Returns distance and index of closest neighbor
    
#     nalltab = len(alltab)
#     npsftab = len(psftab)
    
#     # Use KD-tree
#     x1 = np.zeros((nalltab,2),float)
#     x1[:,0] = alltab[:,2]
#     x1[:,1] = alltab[:,3]
#     x2 = np.zeros((npsftab,2),float)
#     x2[:,0] = psftab[:,2]
#     x2[:,1] = psftab[:,3]


#     #data = np.random.random(3_000_000).reshape(-1, 3)
#     #kdtree = KDTree(data, leafsize=10)

#     ## query the nearest neighbors of the first 100 points
#     #distances, indices, neighbors = kdtree.query(data[:100], k=30)

#     ## query all points in a radius around the first 100 points
#     #indices = kdtree.query_radius(data[:100], r=0.5, return_sorted=True)

    
#     #kdt = cKDTree(x1)
#     kdt = KDTree(x1)
#     # Get distance for 2 closest neighbors
#     dist, ind = kdt.query(x2, k=50, distance_upper_bound=np.sqrt(2)*npix//2)
#     # closest neighbor is always itself, remove it 
#     dist = dist[:,1:]
#     ind = ind[:,1:]
#     # Add up all the stars
#     gdall, = np.where(np.isfinite(dist.ravel()))
#     indall = ind.ravel()[gdall]
#     # Get unique ones
#     indall = np.unique(indall)

#     # Make sure none of them are our psf stars
#     ind1,ind2,dist = utils.xmatch(alltab[indall,2],alltab[indall3],
#                                   psftab[:,2],psftab[:,3],5,sphere=False)
#     #    alltab['x'][indall],alltab['y'][indall],psftab['x'],psftab['y'],5)
#     # Remove any matches
#     if len(ind1)>0:
#         indall = np.delete(indall,ind1)
    
#     return indall
    
# #@njit
# @cc.export('subtracenei', '(f8[:,:],f8[:,:],f8[:,:],f8[:])')
# def subtractnei(image,error,alltab,psftab,psf):
#     """
#     Subtract neighboring stars to PSF stars from the image.

#     Parameters
#     ----------
#     image : CCDDdata object
#        The input image from which to subtract PSF neighbor stars.
#     alltab : numpy array
#       Catalog of all sources in the image.
#     psftab : numpy array
#       Catalog of PSF stars.
#     psf : PSF object
#       The PSF model.

#     Returns
#     -------
#     resid : CCDData object
#       The input images with the PSF neighbor stars subtracted.

#     Example
#     -------

#     subim = subtractnei(image,alltab,psftab,psf)

#     """

#     indnei = findpsfnei(alltab,psftab,psf.npix)
#     nnei = len(indnei)

#     flux = image
#     resid = image.copy()
#     fitradius = psf.fwhm()*0.5
    
#     # Loop over neighboring stars and fit just the core
#     for i in range(nnei):
#         x1 = alltab[indnei[i],2]
#         xp1 = int(np.minimum(np.maximum(np.round(x1),0),image.shape[1]-1))
#         y1 = alltab[indnei[i],3]
#         yp1 = int(np.minimum(np.maximum(np.round(y1),0),image.shape[0]-1))
#         h1 = alltab[indnei[i],1]
#         initpars = np.array([h1,x1,y1])
#         bbox = starbbox((initpars[1],initpars[2]),image.shape,psf.radius)
#         # Fit amp empirically with central pixels
#         flux1 = bbox.slice(flux)
#         err1 = bbox.slice(error)
#         model1 = psf(pars=initpars,bbox=bbox)
#         good = ((flux1/err1>2) & (flux1>0) & (model1/np.max(model1)>0.25))
#         amp = np.median(flux1[good]/model1[good]) * initpars[0]
#         pars = np.array([amp, x1, y1])
#         im1 = psf(pars=pars,bbox=bbox)
#         resid[bbox.slices].data -= im1
#     return resid

# @njit
# @cc.export('starbbox', '(UniTuple(f8[:],2),UniTuple(i8,2),f8)')
# def starbbox(coords,imshape,radius):
#     """                                                                                         
#     Return the boundary box for a star given radius and image size.                             
                                                                                                
#     Parameters                                                                                  
#     ----------                                                                                  
#     coords: list or tuple                                                                       
#        Central coordinates (xcen,ycen) of star (*absolute* values).                             
#     imshape: list or tuple                                                                      
#        Image shape (ny,nx) values.  Python images are (Y,X).                                    
#     radius: float                                                                               
#        Radius in pixels.                                                                        
                                                                                                
#     Returns                                                                                     
#     -------                                                                                     
#     bbox : BoundingBox object                                                                   
#        Bounding box of the x/y ranges.                                                          
#        Upper values are EXCLUSIVE following the python convention.                              
                                                                                                
#     """

#     # pixels span +/-0.5 pixels in each direction
#     # so pixel x=5 spans x=4.5-5.5
#     # Use round() to get the value of the pixel that is covers
#     # the coordinate
#     # to include pixels at the bottom but not the top we have to
#     # subtract a tiny bit before we round
#     eta = 1e-5

#     # Star coordinates
#     xcen,ycen = coords
#     ny,nx = imshape   # python images are (Y,X)
#     x0 = xcen-radius
#     x1 = xcen+radius
#     y0 = ycen-radius
#     y1 = ycen+radius
#     xlo = np.maximum(int(np.round(x0-eta)),0)
#     xhi = np.minimum(int(np.round(x1-eta))+1,nx)
#     ylo = np.maximum(int(np.round(y0-eta)),0)
#     yhi = np.minimum(int(np.round(y1-eta))+1,ny)
#     # add 1 at the upper end because those values are EXCLUDED
#     # by the standard python convention

#     # The old way of doing it
#     #xlo = np.maximum(int(np.floor(xcen-radius)),0)
#     #xhi = np.minimum(int(np.ceil(xcen+radius+1)),nx)
#     #ylo = np.maximum(int(np.floor(ycen-radius)),0)
#     #yhi = np.minimum(int(np.ceil(ycen+radius+1)),ny)
#     return np.array([xlo,xhi,ylo,yhi])

# # @njit
# # def sliceinsert(array,lo,insert):
# #     """ Insert array values."""
# #     n = insert.size
# #     for i in range(n):
# #         j = i + lo
# #         array[j] = insert[i]
# #     # array is updated in place  


# @njit
# @cc.export('getstar', '(f8[:,:],f8[:,:],f8,f8,f8)')
# def getstar(image,error,xcen,ycen,fitradius):
#     """ Return the entire footprint image/error/x/y arrays for one star."""
#     # always return the same size
#     # a distance of 6.2 pixels spans 6 full pixels but you could have
#     # 0.1 left on one side and 0.1 left on the other side
#     # that's why we have to add 2 pixels
#     npix = int(np.floor(2*fitradius))+2
#     bbox = starbbox((xcen,ycen),image.shape,fitradius)
#     nx = bbox[1]-bbox[0]
#     ny = bbox[3]-bbox[2]
#     # extra buffer is ALWAYS at the end of each dimension    
#     imdata = np.zeros(npix*npix,float)+np.nan
#     errdata = np.zeros(npix*npix,float)+np.nan
#     xdata = np.zeros(npix*npix,np.int32)-1
#     ydata = np.zeros(npix*npix,np.int32)-1
#     count = 0
#     for j in range(npix):
#         y = j + bbox[2]
#         for i in range(npix):
#             x = i + bbox[0]            
#             xdata[count] = x
#             ydata[count] = y
#             if x>=bbox[0] and x<=bbox[1]-1 and y>=bbox[2] and y<=bbox[3]-1:
#                 imdata[count] = image[y,x]
#                 errdata[count] = error[y,x]
#             count += 1
#     return imdata,errdata,xdata,ydata,bbox,nx,ny

# @njit
# @cc.export('collatestars', '(f8[:,:],f8[:,:],f8[:],f8[:],f8)')
# def collatestars(image,error,starx,stary,fitradius):
#     """ Get the entire footprint image/error/x/y for all of the stars."""
#     nstars = len(starx)
#     npix = int(np.floor(2*fitradius))+2
#     # Get xdata, ydata, error
#     imdata = np.zeros((nstars,npix*npix),float)
#     errdata = np.zeros((nstars,npix*npix),float)
#     xdata = np.zeros((nstars,npix*npix),np.int32)
#     ydata = np.zeros((nstars,npix*npix),np.int32)
#     bbox = np.zeros((nstars,4),np.int32)
#     shape = np.zeros((nstars,2),np.int32)
#     for i in range(nstars):
#         imdata1,errdata1,xdata1,ydata1,bbox1,nx1,ny1 = getstar(image,error,starx[i],stary[i],fitradius)
#         imdata[i,:] = imdata1
#         errdata[i,:] = errdata1
#         xdata[i,:] = xdata1
#         ydata[i,:] = ydata1
#         bbox[i,:] = bbox1
#         shape[i,0] = ny1
#         shape[i,1] = nx1
#     return imdata,errdata,xdata,ydata,bbox,shape

# @njit
# @cc.export('unpackstar', '(f8[:],f8[:,:],f8[:,:],i8,f8)')
# def unpackstar(imdata,errdata,xdata,ydata,bbox,shape,istar):
#     """ Return unpacked data for one star."""
#     imdata1 = imdata[istar,:].copy()
#     errdata1 = errdata[istar,:].copy()
#     xdata1 = xdata[istar,:].copy()
#     ydata1 = ydata[istar,:].copy()
#     bbox1 = bbox[istar,:]
#     shape1 = shape[istar,:]
#     n = len(imdata1)
#     npix = int(np.sqrt(n))
#     # Convert to 2D arrays
#     imdata1 = imdata1.reshape(npix,npix)
#     errdata1 = errdata1.reshape(npix,npix)
#     xdata1 = xdata1.reshape(npix,npix)
#     ydata1 = ydata1.reshape(npix,npix)
#     # Trim values
#     if shape1[0] < npix or shape1[1] < npix:
#         imdata1 = imdata1[:shape1[0],:shape1[1]]
#         errdata1 = errdata1[:shape1[0],:shape1[1]]
#         xdata1 = xdata1[:shape1[0],:shape1[1]]
#         ydata1 = ydata1[:shape1[0],:shape1[1]]
#     return imdata1,errdata1,xdata1,ydata1,bbox1,shape1

# @njit
# @cc.export('getfitstar', '(f8[:,:],f8[:,:],f8,f8,f8)')
# def getfitstar(image,error,xcen,ycen,fitradius):
#     """ Get the fitting pixels for a single star."""
#     npix = int(np.floor(2*fitradius))+2
#     bbox = starbbox((xcen,ycen),image.shape,fitradius)
#     flux = image[bbox[2]:bbox[3],bbox[0]:bbox[1]].copy()
#     err = error[bbox[2]:bbox[3],bbox[0]:bbox[1]].copy()
#     nflux = flux.size
#     nx = bbox[1]-bbox[0]
#     ny = bbox[3]-bbox[2]
#     imdata = np.zeros(npix*npix,float)
#     errdata = np.ones(npix*npix,float)
#     xdata = np.zeros(npix*npix,np.int32)-1
#     ydata = np.zeros(npix*npix,np.int32)-1
#     mask = np.zeros(npix*npix,np.int32)
#     count = 0
#     for j in range(ny):
#         y = j + bbox[2]
#         for i in range(nx):
#             x = i + bbox[0]
#             r = np.sqrt((x-xcen)**2 + (y-ycen)**2)
#             if r <= fitradius:
#                 imdata[count] = flux[j,i]
#                 errdata[count] = error[j,i]
#                 xdata[count] = x
#                 ydata[count] = y
#                 mask[count] = 1
#                 count += 1
#     return imdata,errdata,xdata,ydata,count,mask
        
# @njit
# @cc.export('collatefitstars', '(f8[:,:],f8[:,:],f8[:],f8[:],f8)')
# def collatefitstars(image,error,starx,stary,fitradius):
#     nstars = len(starx)
#     npix = int(np.floor(2*fitradius))+2
#     # Get xdata, ydata, error
#     maxpix = nstars*(npix)**2
#     imdata = np.zeros((nstars,npix*npix),float)
#     errdata = np.zeros((nstars,npix*npix),float)
#     xdata = np.zeros((nstars,npix*npix),np.int32)
#     ydata = np.zeros((nstars,npix*npix),np.int32)
#     mask = np.zeros((nstars,npix*npix),np.int32)
#     ndata = np.zeros(nstars,np.int32)
#     bbox = np.zeros((nstars,4),np.int32)
#     for i in range(nstars):
#         xcen = starx[i]
#         ycen = stary[i]
#         bb = starbbox((xcen,ycen),image.shape,fitradius)
#         imdata1,errdata1,xdata1,ydata1,n1,mask1 = getfitstar(image,error,xcen,ycen,fitradius)
#         imdata[i,:] = imdata1
#         errdata[i,:] = errdata1
#         xdata[i,:] = xdata1
#         ydata[i,:] = ydata1
#         mask[i,:] = mask1
#         ndata[i] = n1
#         bbox[i,:] = bb
#     return imdata,errdata,xdata,ydata,bbox,ndata,mask

# @njit
# @cc.export('unpackfitstar', '(f8[:,:],f8[:,:],f8[:,:],i8,f8)')
# def unpackfitstar(imdata,errdata,xdata,ydata,bbox,ndata,istar):
#     """ Return unpacked fitting data for one star."""
#     imdata1 = imdata[istar,:]
#     errdata1 = errdata[istar,:]
#     xdata1 = xdata[istar,:]
#     ydata1 = ydata[istar,:]
#     bbox1 = bbox[istar,:]
#     n1 = ndata[istar]
#     # Trim to the values we want
#     imdata1 = imdata1[:n1]
#     errdata1 = errdata1[:n1]
#     xdata1 = xdata1[:n1]
#     ydata1 = ydata1[:n1]
#     return imdata1,errdata1,xdata1,ydata1,bbox1,n1

    
# kv_ty = (types.int64, types.unicode_type)
# spec = [
#     ('psftype', types.int32),
#     ('params', types.float64[:]),
#     #('lookup', types.float64[:,:,:]),
#     #('order', types.int32),
#     #('fwhm', types.float64),
#     ('image', types.float64[:,:]),
#     ('error', types.float64[:,:]),
#     ('starinit', types.float64[:,:]),
#     ('nstars', types.int32),
#     ('niter', types.int32),
#     ('npsfpix', types.int32),
#     ('nx', types.int32),
#     ('ny', types.int32),
#     ('fitradius', types.float64),
#     ('nfitpix', types.int32),
#     ('staramp', types.float64[:]),
#     ('starxcen', types.float64[:]),
#     ('starycen', types.float64[:]),
#     ('starchisq', types.float64[:]),
#     ('starrms', types.float64[:]),
#     ('starnpix', types.int32[:]),
#     ('star_imdata', types.float64[:,:]),
#     ('star_errdata', types.float64[:,:]),
#     ('star_xdata', types.int32[:,:]),
#     ('star_ydata', types.int32[:,:]),
#     ('star_bbox', types.int32[:,:]),
#     ('star_shape', types.int32[:,:]),
#     ('starfit_imdata', types.float64[:,:]),
#     ('starfit_errdata', types.float64[:,:]),
#     ('starfit_xdata', types.int32[:,:]),
#     ('starfit_ydata', types.int32[:,:]),
#     ('starfit_mask', types.int32[:,:]),
#     ('starfit_bbox', types.int32[:,:]),
#     ('starfit_ndata', types.int32[:]),
#     ('npix', types.int32),
#     ('radius', types.int32),
#     ('verbose', types.boolean),
#     ('niter', types.int32),
#     ('_unitfootflux', types.float64),
#     #('lookup', types.float64[:,:,:]),
#     #('_bounds', types.float64[:,:]),
#     #('_steps', types.float64[:]),
#     ('coords', types.float64[:]),
#     ('imshape', types.int32[:]),
#     #('order', types.int32),
# ]

# @jitclass(spec)
# class PSFFitter(object):

#     def __init__(self,psftype,psfparams,image,error,starx,stary,starflux,fitradius,verbose):
#         self.verbose = verbose
#         self.psftype = psftype
#         self.params = psfparams
#         #self.lookup = psf.lookup
#         #self.order = psf.order
#         #self.fwhm = psf.fwhm()
#         self.image = image.astype(np.float64)
#         self.error = error.astype(np.float64)
#         self.imshape = np.zeros(2,np.int32)
#         self.imshape[:] = image.shape
#         nstars = len(starx)
#         self.starinit = np.zeros((nstars,3),float)
#         self.starinit[:,0] = starx
#         self.starinit[:,1] = stary
#         self.starinit[:,2] = starflux
#         self.nstars = nstars
#         self.niter = 0
#         #self.npsfpix = psf.npix
#         ny,nx = image.shape
#         self.nx = nx
#         self.ny = ny
#         #if np.isfinite(fitradius)==False:
#         #    fitradius = self.fwhm*1.5
#         self.fitradius = fitradius
#         self.nfitpix = int(np.floor(2*fitradius))+2  # max pixels 
#         self.staramp = np.zeros(self.nstars,float)
#         self.staramp[:] = starflux/(2*fitradius+1)**2
#         # current best-fit values
#         self.starxcen = np.zeros(self.nstars,float)
#         self.starxcen[:] = self.starinit[:,0].copy()
#         self.starycen = np.zeros(self.nstars,float)
#         self.starycen[:] = self.starinit[:,1].copy()
#         self.starchisq = np.zeros(self.nstars,float)
#         self.starrms = np.zeros(self.nstars,float)
#         self.starnpix = np.zeros(self.nstars,np.int32)

#         # Get information for all the stars
#         imdata,errdata,xdata,ydata,bbox,shape = collatestars(image,error,self.starxcen,
#                                                              self.starycen,fitradius)
#         self.star_imdata = imdata
#         self.star_errdata = errdata
#         self.star_xdata = xdata
#         self.star_ydata = ydata
#         self.star_bbox = bbox
#         self.star_shape = shape
        
#         # Get fitting information for all the stars
#         fimdata,ferrdata,fxdata,fydata,fbbox,fndata,fmask = collatefitstars(image,error,self.starxcen,
#                                                                             self.starycen,fitradius)
#         self.starfit_imdata = fimdata
#         self.starfit_errdata = ferrdata
#         self.starfit_xdata = fxdata
#         self.starfit_ydata = fydata
#         self.starfit_mask = fmask
#         self.starfit_bbox = fbbox
#         self.starfit_ndata = fndata

#     def unpackstar(self,istar):
#         return unpackstar(self.star_imdata,self.star_errdata,self.star_xdata,
#                           self.star_ydata,self.star_bbox,self.star_shape,istar)

#     def unpackfitstar(self,istar):
#         return unpackfitstar(self.starfit_imdata,self.starfit_errdata,self.starfit_xdata,
#                              self.starfit_ydata,self.starfit_bbox,self.starfit_ndata,istar)

#     def psf(self,x,y,pars,psfparams,lookup,deriv):
#         """ Get a PSF model for a single star."""
#         return mnb.psf(x,y,pars,self.psftype,psfparams,lookup,
#                        self.imshape,deriv=deriv,verbose=self.verbose)

#     def model(self,x,args):
#         """ model function."""
#         # input the model parameters
        
#         if self.verbose:
#             print('model: '+str(self.niter)+' ',args)
#         nparsarr = np.array([6,7,8,8,7])
#         npars = nparsarr[self.psftype-1]
#         # Loop over the stars and generate the model image
#         allmodel = np.zeros(self.star_imdata.shape,float)
#         maxpix = self.star_imdata.shape[1]
#         for i in range(self.nstars):
#             im1,err1,xdata1,ydata1,bbox1,n1 = self.unpackfitstar(i)
#             allpars = np.zeros(npars,float)
#             allpars[0] = self.staramp[i]
#             allpars[1] = self.starxcen[i]
#             allpars[2] = self.starycen[i]
#             allpars[3:] = args
#             g,_ = mnb.amodel2d(xdata1,ydata1,self.psftype,allpars,0)
#             allmodel[i,:len(g)] = g
#         return allmodel

#     def chisq(self,pars):
#         """ Create the model and return chisq for this set of PSF parameters."""
#         m = self.model(np.arange(10),pars)
#         chisq = np.sum((self.starfit_imdata-m)**2/self.starfit_errdata**2)        
#         return chisq
        
    
#             #image = self.imdata[i]
#             #amp = self.staramp[i]
#             #xcenorig = self.starxcenorig[i]   
#             #ycenorig = self.starycenorig[i]
#             #xcen = self.starxcen[i]   
#             #ycen = self.starycen[i]            
#             #bbox = self.bboxdata[i]
#             #x = self.xlist[i]
#             #y = self.ylist[i]
#             #pixstart = self.pixstart[i]
#             #npix = self.npix[i]
#             #flux = self.imflatten[pixstart:pixstart+npix]
#             #err = self.errflatten[pixstart:pixstart+npix]

#             #x0orig = xcenorig - bbox.ixmin
#             #y0orig = ycenorig - bbox.iymin
#             #x0 = xcen - bbox.ixmin
#             #y0 = ycen - bbox.iymin            
            
#             # # Fit amp/xcen/ycen if niter=1
#             # if refit:
#             #     #if (self.niter<=1): # or self.niter%3==0):
#             #     if self.niter>-1:
#             #         # force the positions to stay within +/-2 pixels of the original values
#             #         bounds = (np.array([0,np.maximum(x0orig-2,0),np.maximum(y0orig-2,0),-np.inf]),
#             #                   np.array([np.inf,np.minimum(x0orig+2,bbox.shape[1]-1),np.minimum(y0orig+2,bbox.shape[0]-1),np.inf]))
#             #         # the image still has sky in it, use sky (nosky=False)
#             #         if np.isfinite(psf.fwhm())==False:
#             #             print('nan fwhm')
#             #             #import pdb; pdb.set_trace()
#             #         pars,perror,model = psf.fit(image,[amp,x0,y0],nosky=False,retpararray=True,niter=5,bounds=bounds)
#             #         xcen += (pars[1]-x0)
#             #         ycen += (pars[2]-y0)
#             #         amp = pars[0]                    
#             #         self.staramp[i] = amp
#             #         self.starxcen[i] = xcen
#             #         self.starycen[i] = ycen
#             #         model = psf(x,y,pars=[amp,xcen,ycen])
#             #         if verbose:
#             #             print('Star '+str(i)+' Refitting all parameters')
#             #             print(str([amp,xcen,ycen]))

#             #         #pars2,model2,mpars2 = psf.fit(image,[amp,x0,y0],nosky=False,niter=5,allpars=True)
#             #         #import pdb; pdb.set_trace()
                        
#     #             # Only fit amp if niter>1
#     #             #   do it empirically
#     #             else:
#     #                 #im1 = psf(pars=[1.0,xcen,ycen],bbox=bbox)
#     #                 #wt = 1/image.error**2
#     #                 #amp = np.median(image.data[mask]/im1[mask])                
#     #                 model1 = psf(x,y,pars=[1.0,xcen,ycen])
#     #                 wt = 1/err**2
#     #                 amp = np.median(flux/model1)
#     #                 #amp = np.median(wt*flux/model1)/np.median(wt)

#     #                 self.staramp[i] = amp
#     #                 model = model1*amp
#     #                 #self.starxcen[i] = pars2[1]+xy[0][0]
#     #                 #self.starycen[i] = pars2[2]+xy[1][0]       
#     #                 #print(count,self.starxcen[i],self.starycen[i])
#     #                 # updating the X/Y values after the first iteration
#     #                 #  causes problems.  bounces around too much

#     #                 if verbose:
#     #                     print('Star '+str(i)+' Refitting amp empirically')
#     #                     print(str(amp))
                        
#     #                 #if i==1: print(amp)
#     #                 #if self.niter==2:
#     #                 #    import pdb; pdb.set_trace()

#     #         # No refit of stellar parameters
#     #         else:
#     #             model = psf(x,y,pars=[amp,xcen,ycen])

#     #         #if self.niter>1:
#     #         #    import pdb; pdb.set_trace()
                
#     #         # Relculate reduced chi squared
#     #         chisq = np.sum((flux-model.ravel())**2/err**2)/npix
#     #         self.starchisq[i] = chisq
#     #         # chi value, RMS of the residuals as a fraction of the amp
#     #         rms = np.sqrt(np.mean(((flux-model.ravel())/self.staramp[i])**2))
#     #         self.starrms[i] = rms
            
#     #         #model = psf(x,y,pars=[amp,xcen,ycen])
#     #         # Zero-out anything beyond the fitting radius
#     #         #im[mask] = 0.0
#     #         #npix = im.size
#     #         #npix = len(x)
#     #         allim[pixcnt:pixcnt+npix] = model.flatten()
#     #         pixcnt += npix

#     #         #import pdb; pdb.set_trace()
            
#     #     self.niter += 1
            
#     #    return allim


#     def fitstars(self,x,psfparams):
#         """ Fit amp/xcen/ycen for all the stars using the current PSF."""

#         # Loop over the stars and generate the model image
#         lookup = np.zeros((1,1,1),float)
#         allmodel = np.zeros(self.star_imdata.shape,float)
#         maxpix = self.star_imdata.shape[1]
#         npix = int(np.sqrt(maxpix))
#         for i in range(self.nstars):
#             im1,err1,xdata1,ydata1,bbox1,n1 = self.unpackfitstar(i)
#             pars = np.zeros(3,float)
#             pars[0] = self.staramp[i]
#             pars[1] = self.starxcen[i]
#             pars[2] = self.starycen[i]
#             pout = mnb.psffit(im1,err1,xdata1,ydata1,pars,self.psftype,psfparams,
#                               lookup,self.imshape,self.verbose)
#             bestpar,perror,cov,flux,fluxerr,chisq = pout
#             rchisq = chisq/len(xdata1)
#             model,der = mnb.psf(xdata1,ydata1,pars,self.psftype,psfparams,
#                                 lookup,self.imshape)
#             rms = np.sqrt(np.mean(((im1-model)/bestpar[0])**2))
#             #print(i,bestpar,rchisq,rms)
            
#             # Update stellar parameters
#             self.staramp[i] = bestpar[0]
#             self.starxcen[i] = bestpar[1]
#             self.starycen[i] = bestpar[2]
#             self.starchisq[i] = rchisq
#             self.starrms[i] = rms
            
    
#     def jac(self,x,args):
#         """ jacobian."""
#         # input the model parameters        
#         if self.verbose:
#             print('model: '+str(self.niter)+' ',args)
#         # Loop over the stars and generate the model image
#         nparsarr = np.array([6,7,8,8,7])
#         npars = nparsarr[self.psftype-1]
#         allmodel = np.zeros(self.star_imdata.shape,float)
#         maxpix = self.star_imdata.shape[1]
#         allderiv = np.zeros((self.nstars,maxpix,npars),float)
#         for i in range(self.nstars):
#             im1,err1,xdata1,ydata1,bbox1,n1 = self.unpackfitstar(i)
#             allpars = np.zeros(npars,float)
#             allpars[0] = self.staramp[i]
#             allpars[1] = self.starxcen[i]
#             allpars[2] = self.starycen[i]
#             allpars[3:] = args
#             g,d = mnb.amodel2d(xdata1,ydata1,self.psftype,allpars,npars)
#             allmodel[i,:len(g)] = g
#             allderiv[i,:len(g),:] = d
#         # Trim off the stellar parameter (amp/xc/yc) derivatives
#         allderiv = allderiv[:,:,3:]
#         return allmodel,allderiv

#     def linesearch(self,bestpar,dbeta):
#         # Perform line search along search gradient

#         # Inside model() the parameters are limited to the PSF bounds()
#         f0 = self.chisq(bestpar)
#         f1 = self.chisq(bestpar+0.5*dbeta)
#         f2 = self.chisq(bestpar+dbeta)
#         alpha = quadratic_bisector(np.array([0.0,0.5,1.0]),np.array([f0,f1,f2]))
#         alpha = np.minimum(np.maximum(alpha,0.0),1.0)  # 0<alpha<1
#         if np.isfinite(alpha)==False:
#             alpha = 1.0

#         pars_new = bestpar + alpha * dbeta
#         new_dbeta = alpha * dbeta
#         return alpha,new_dbeta
    
#     # def mklookup(self,order=0):
#     #     """ Make an empirical look-up table for the residuals."""

#     #     # Make the empirical EPSF
#     #     cube = self.psf.resid(self.tab,self.image,fillvalue=np.nan)
#     #     coords = (self.tab['x'].data,self.tab['y'].data)
#     #     epsf,nbadstar,rms = mkempirical(cube,order=order,coords=coords,shape=self.image.shape,lookup=True)
#     #     lookup = models.PSFEmpirical(epsf,imshape=self.image.shape,order=order,lookup=True)

#     #     # DAOPHOT does some extra analysis to make sure the flux
#     #     # in the residual component is okay

#     #     # -make sure
#     #     #  -to take the total flux into account (not varying across image)
#     #     #  -make sure the amp=1 at center
#     #     #  -make sure all PSF values are >=0
                             
#     #     # Add the lookup table to the PSF model
#     #     self.psf.lookup = lookup

#     #     #import pdb; pdb.set_trace()
        
        
#     def starmodel(self,istar,params):
#         """ Generate a 2D star model image that can be compared to the original cutout."""
#         im,err,xdata,ydata,bbox,shape = self.unpackstar(istar)
#         xdata1d = xdata.ravel()
#         ydata1d = ydata.ravel()
#         amp = self.staramp[istar]
#         xcen = self.starxcen[istar]   
#         ycen = self.starycen[istar]
#         nparsarr = np.array([6,7,8,8,7])
#         npars = nparsarr[self.psftype-1]
#         allpars = np.zeros(npars,float)
#         allpars[0] = amp
#         allpars[1] = xcen
#         allpars[2] = ycen
#         allpars[3:] = params
#         g,_ = mnb.amodel2d(xdata1d,ydata1d,self.psftype,allpars,0)
#         g = g.reshape(im.shape)
#         return g

# @njit
# @cc.export('fitpsf', '(i4,f8[:],f8[:,:],f8[:,:],table,f8,unicode_type,i8,f8,b1)')
# def fitpsf(psftype,psfparams,image,error,starx,stary,starflux,fitradius,method='qr',maxiter=10,
#            minpercdiff=1.0,verbose=False):
#     """
#     Fit PSF model to stars in an image.

#     Parameters
#     ----------
#     psftype : PSF object
#        PSF object with initial parameters to use.
#     psfparams : numpy array
#        PSF object with initial parameters to use.
#     image : numpy array
#        Image to use to fit PSF model to stars.
#     error : numpy array
#        Uncertainties in image.
#     tab : numpy structured array
#        Table with initial amp/x/y values for the stars to use to fit the PSF.
#     fitradius : float, table
#        The fitting radius.  If none is input then the initial PSF FWHM will be used.
#     method : str, optional
#        Method to use for solving the non-linear least squares problem: "qr",
#        "svd", "cholesky", and "curve_fit".  Default is "qr".
#     maxiter : int, optional
#        Maximum number of iterations to allow.  Only for methods "qr", "svd", and "cholesky".
#        Default is 10.
#     minpercdiff : float, optional
#        Minimum percent change in the parameters to allow until the solution is
#        considered converged and the iteration loop is stopped.  Only for methods
#        "qr" and "svd".  Default is 1.0.
#     verbose : boolean, optional
#        Verbose output.

#     Returns
#     -------
#     pars : numpy array
#        Array of best-fit model parameters
#     perror : numpy array
#        Uncertainties in "pars".
#     pcov : numpy array
#        Covariances of the best-ft parameters.
#     psftab : table
#        Table of best-fitting amp/xcen/ycen values for the PSF stars.
#     rchisq : float
#        Reduced chi-squared of the best-fit.
#     pf : PSFFitter object
#        PSFFitter object.
    
#     Example
#     -------

#     pars,perror,pcov,psftab,rchisq,pf = fitpsf(psftype,psfparams,image,error,tab,fitradius)

#     """

#     # Fitting the PSF to the stars
#     #-----------------------------

#     # Empirical PSF - done differently
#     if psf.psftype==6:
#         cube1 = starcube(tab,image,npix=psf.npix,fillvalue=np.nan)
#     #     coords = (tab['x'].data,tab['y'].data)
#     #     epsf1,nbadstar1,rms1 = mkempirical(cube1,order=psf.order,coords=coords,shape=psf._shape)
#     #     initpsf = mnb.PSFEmpirical(epsf1,imshape=image.shape,order=psf.order)
#     #     pf = PSFFitter(initpsf,image,tab,fitradius=fitradius,verbose=False)
#     #     # Fit the amp, xcen, ycen properly
#     #     xdata = np.arange(pf.ntotpix)
#     #     out = pf.model(xdata,[])
#     #     # Put information into the psftab table
#     #     psftab['amp'] = pf.staramp
#     #     psftab['x'] = pf.starxcen
#     #     psftab['y'] = pf.starycen
#     #     psftab['chisq'] = pf.starchisq
#     #     psftab['rms'] = pf.starrms
#     #     psftab['npix'] = pf.starnpix    
#     #     for i in range(len(tab)):
#     #         bbox = pf.bboxdata[i]
#     #         psftab['ixmin'][i] = bbox.ixmin
#     #         psftab['ixmax'][i] = bbox.ixmax
#     #         psftab['iymin'][i] = bbox.iymin
#     #         psftab['iymax'][i] = bbox.iymax        
#     #     psftab = Table(psftab)
#     #     # Remake the empirical EPSF    
#     #     cube = starcube(psftab,image,npix=psf.npix,fillvalue=np.nan)
#     #     epsf,nbadstar,rms = mkempirical(cube,order=psf.order,coords=coords,shape=psf._shape)
#     #     newpsf = mnb.PSFEmpirical(epsf,imshape=image.shape,order=psf.order)
#     #     if verbose:
#     #         print('Median RMS: '+str(np.median(pf.starrms)))
#     #         print('dt = %.2f sec' % (time.time()-t0))
#     #     return newpsf, None, None, psftab, pf

#     nstars = len(starx)
#     pf = PSFFitter(psftype,psfparams,image,error,starx,stary,starflux,fitradius,verbose)
#     initpar = psfparams.copy()
#     xdata = np.arange(10)  # dummy

#     # Iterate
#     niter = 0
#     percdiff = 1e10
#     bestpar = initpar.copy()
    
#     dchisq = -1
#     oldchisq = 1e30
#     bounds = mnb.model2d_bounds(psftype)
#     bounds = bounds[3:,:]
#     while (niter<maxiter and percdiff>minpercdiff and dchisq<0):
#         # Fit the stellar parameters first
#         pf.fitstars(xdata,bestpar)
#         # Get the Jacobian and model
#         m,jac = pf.jac(xdata,bestpar)
#         chisq = np.sum((pf.starfit_imdata-m)**2/pf.starfit_errdata**2)
#         dy = (pf.starfit_imdata-m).ravel()
#         # Weights
#         wt = 1/pf.starfit_errdata**2
#         wt = wt.ravel()
#         jac = jac.copy().reshape((jac.shape[0]*jac.shape[1],jac.shape[2]))
#         m = m.ravel()
#         # Solve Jacobian
#         dbeta = mnb.qr_jac_solve(jac,dy,weight=wt)

#         # Perform line search
#         alpha,new_dbeta = pf.linesearch(bestpar,dbeta)
            
#         if verbose:
#             print('  pars = '+str(bestpar))
#             print('  dbeta = '+str(dbeta))

#         # Update parameters
#         oldpar = bestpar.copy()
#         # limit the steps to the maximum step sizes and boundaries
#         maxsteps = mnb.model2d_maxsteps(psftype,bestpar)
#         maxsteps = maxsteps[3:]
#         bestpar = mnb.newlsqpars(bestpar,new_dbeta,bounds,maxsteps)
        
#         # Update the parameters 
#         diff = np.abs(bestpar-oldpar)
#         denom = np.abs(oldpar.copy())
#         bad, = np.where(denom==0)
#         if len(bad)>0:
#             denom[bad] = 1.0  # deal with zeros
#         percdiff = np.max(diff/denom*100)
#         dchisq = chisq-oldchisq
#         percdiffchisq = dchisq/oldchisq*100
#         oldchisq = chisq
#         niter += 1
            
#         if verbose:
#             print('  '+str(niter+1)+' ',bestpar,' '+str(percdiff)+' '+str(chisq))

#     # Fit the stellar parameters first
#     pf.fitstars(xdata,bestpar)
#     # Get the Jacobian and model
#     m,jac = pf.jac(xdata,bestpar)
#     chisq = np.sum((pf.starfit_imdata-m)**2/pf.starfit_errdata**2)
#     dy = (pf.starfit_imdata-m).ravel()
#     # Weights
#     wt = 1/pf.starfit_errdata**2
#     wt = wt.ravel()
#     jac = jac.copy().reshape((jac.shape[0]*jac.shape[1],jac.shape[2]))
#     rchisq = chisq/np.sum(pf.starfit_ndata)
    
#     # Estimate uncertainties
#     # Calculate covariance matrix
#     pcov = mnb.jac_covariance(jac,dy,wt=wt)
#     perror = np.sqrt(np.diag(pcov))

#     pars = bestpar
#     if verbose:
#         print('Best-fitting parameters: '+str(pars))
#         print('Errors: '+str(perror))
#         print('Median RMS: '+str(np.median(pf.starrms)))

#     # Output best-fitting values for the PSF stars as well
#     psftab = np.zeros((nstars,11),float)
#     psftab[:,0] = np.arange(nstars)+1.0  # id
#     psftab[:,1] = pf.staramp             # amp
#     psftab[:,2] = pf.starxcen            # x
#     psftab[:,3] = pf.starycen            # y
#     psftab[:,4] = pf.starfit_ndata       # npix
#     psftab[:,5] = pf.starrms             # rms
#     psftab[:,6] = pf.starchisq           # chisq
#     psftab[:,7] = pf.star_bbox[:,0]      # ixmin
#     psftab[:,8] = pf.star_bbox[:,1]      # ixmax
#     psftab[:,9] = pf.star_bbox[:,2]      # iymin
#     psftab[:,10] = pf.star_bbox[:,3]     # iymax
    
#     return pars, perror, pcov, psftab, rchisq
    

# #@njit
# @cc.export('getpsf', '(i4,f8[:],f8[:,:],f8[:,:],table,f8,b1,i4,unicode_type,b1,table,i8,b1)')
# def getpsf(psftype,psfparams,image,error,starx,stary,starflux,fitradius,
#            lookup=False,lorder=0,subnei=False,
#            alltab=None,maxiter=10,minpercdiff=1.0,reject=False,maxrejiter=3,verbose=False):
#     """
#     Fit PSF model to stars in an image with outlier rejection of badly-fit stars.

#     Parameters
#     ----------
#     psftype : PSF object
#        PSF object with initial parameters to use.
#     psfparams : numpy array
#        PSF object with initial parameters to use.
#     image : numpy array
#        Image to use to fit PSF model to stars.
#     error : numpy array
#        Uncertainties in image.
#     tab : table
#        Catalog with initial amp/x/y values for the stars to use to fit the PSF.
#     fitradius : float, table
#        The fitting radius.  If none is input then the initial PSF FWHM will be used.
#     lookup : boolean, optional
#        Use an empirical lookup table.  Default is False.
#     lorder : int, optional
#        The order of the spatial variations (0=constant, 1=linear).  Default is 0.
#     method : str, optional
#        Method to use for solving the non-linear least squares problem: "qr",
#        "svd", "cholesky", and "curve_fit".  Default is "qr".
#     subnei : boolean, optional
#        Subtract stars neighboring the PSF stars.  Default is False.
#     alltab : table, optional
#        Catalog of all objects in the image.  This is needed for bad PSF star
#        rejection.
#     maxiter : int, optional
#        Maximum number of iterations to allow.  Only for methods "qr", "svd", and "cholesky".
#        Default is 10.
#     minpercdiff : float, optional
#        Minimum percent change in the parameters to allow until the solution is
#        considered converged and the iteration loop is stopped.  Only for methods
#        "qr" and "svd".  Default is 1.0.
#     reject : boolean, optional
#        Reject PSF stars with high RMS values.  Default is False.
#     maxrejiter : int, boolean
#        Maximum number of PSF star rejection iterations.  Default is 3.
#     verbose : boolean, optional
#        Verbose output.

#     Returns
#     -------
#     newpsf : PSF object
#        New PSF object with the best-fit model parameters.
#     pars : numpy array
#        Array of best-fit model parameters
#     perror : numpy array
#        Uncertainties in "pars".
#     psftab : table
#        Table of best-fitting amp/xcen/ycen values for the PSF stars.

#     Example
#     -------

#     newpsf,pars,perror,psftab = getpsf(psf,image,tab)

#     """

#     #t0 = time.time()
#     #print = utils.getprintfunc() # Get print function to be used locally, allows for easy logging   

#     #psftype,psfpars,_,_ = mnb.unpackpsf(psf)
    
#     # # Fitting radius
#     # if fitradius is None:
#     #     tpars = np.zeros(len(psfpars)+3,float)
#     #     tpars[0] = 1.0
#     #     tpars[3:] = psfpars
#     #     if psftype == 3:  # Penny
#     #         fitradius = mnb.penny2d_fwhm(tpars)*1.5
#     #     else:
#     #         fitradius = mnb.model2d_fwhm(psftype,tpars)
        
#     # # subnei but no alltab input
#     # if subnei and alltab is None:
#     #     raise ValueError('alltab is needed for PSF neighbor star subtraction')
        
#     # if 'id' not in tab.dtype.names:
#     #     tab['id'] = np.arange(len(tab))+1
#     # psftab = tab.copy()

#     # id, amp, x, y, npix, rms, chisq, ixmin, ixmax, iymin, iymax, reject
#     psftab = np.zeros((nstars,12),float)
#     psftab[:,0] = np.arange(nstars)+1
#     psftab[:,1] = starflux
#     psftab[:,2] = starx
#     psftab[:,3] = stary
    
#     # Initializing output PSF star catalog
#     #dt = np.dtype([('id',int),('amp',float),('x',float),('y',float),('npix',int),
#     #               ('rms',float),('chisq',float),('ixmin',int),('ixmax',int),
#     #               ('iymin',int),('iymax',int),('reject',int)])
#     #outtab = np.zeros(len(tab),dtype=dt)
#     #outtab = Table(outtab)
#     #for n in ['id','x','y']:
#     #    outtab[n] = tab[n]
#     nstars = len(starx)
#     # id, amp, x, y, npix, rms, chisq, ixmin, ixmax, iymin, iymax, reject
#     outtab = np.zeros((nstars,12),float)
    
#     # Remove stars that are too close to the edge
#     ny,nx = image.shape
#     gd, = np.where((starx>fitradius) & (starx<(nx-1-fitradius)) &
#                    (stary>fitradius) & (stary<(ny-1-fitradius)))
#     ngd = len(gd)
#     nbd = nstars-ngd
#     if nbd > 0:
#         if verbose:
#             print('Removing '+str(nbd)+' stars near the edge')
#         psftab = psftab[gd,:]

#     # # Generate an empirical image of the stars
#     # # and fit a model to it to get initial estimates
#     # if psftype != 6:
#     #     cube = starcube(psftab,image,npix=psf.npix,fillvalue=np.nan)
#     #     epsf,nbadstar,rms = mkempirical(cube,order=0)
#     #     #epsfim = CCDData(epsf,error=epsf.copy()*0+1,mask=~np.isfinite(epsf))
#     #     epsfim = epsf.copy()
#     #     epsferr = np.ones(epsf.shape,float)
#     #     ny,nx = epsf.shape
#     #     xx,yy = np.meshgrid(np.arange(nx),np.arange(ny))
#     #     out = model2dfit(epsfim,epsferr,xx,yy,psftype,1.0,nx//2,ny//2,verbose=False)
#     #     pars,perror,cov,flux,fluxerr,chisq = out
#     #     mparams = pars[3:]  # model parameters
#     #     #pars,perror,mparams = mnb.model2d_fit(epsfim,pars=[1.0,psf.npix/2,psf.npix//2])
#     #     initpar = mparams.copy()
#     #     curpsf = mnb.packpsf(psftype,mparams,0,0)
#     #     #curpsf = psf.copy()
#     #     #curpsf.params = initpar
#     #     if verbose:
#     #         print('Initial estimate from empirical PSF fit = '+str(mparams))
#     # else:
#     #     curpsf = psf.copy()
#     #     _,initpar,_,_ = mnb.unpackpsf(psf)
#     #     #initpar = psf.params.copy()
#     curparams = psfparams.copy()
    
#     # Outlier rejection iterations
#     nrejiter = 0
#     flag = 0
#     nrejstar = 100
#     fitrad = fitradius
#     useimage = image.copy()
#     while (flag==0):
#         if verbose:
#             print('--- Iteration '+str(nrejiter+1)+' ---')                

#         # Update the fitting radius
#         #if nrejiter>0:
#         #    fitrad = curpsf.fwhm()
#         if verbose:
#             print('  Fitting radius = %5.3f' % (fitrad))
                    
#         # Reject outliers
#         if reject and nrejiter>0:
#             medrms = np.median(ptab[:,5]) # rms
#             sigrms = utils.mad(ptab[:,5])
#             gd, = np.where(ptab[:,5] < medrms+3*sigrms)
#             nrejstar = len(psftab)-len(gd)
#             if verbose:
#                 print('  RMS = %6.4f +/- %6.4f' % (medrms,sigrms))
#                 print('  Threshold RMS = '+str(medrms+3*sigrms))
#                 print('  Rejecting '+str(nrejstar)+' stars')
#             if nrejstar>0:
#                 psftab = psftab[gd,:]

#         # Subtract neighbors
#         if nrejiter>0 and subnei:
#             if verbose:
#                 print('Subtracting neighbors')
#                 # Find the neighbors in alltab
#                 # Fit the neighbors and PSF stars
#                 # Subtract neighbors from the image
#                 useimage = image.copy()  # start with original image
#                 print('subtractnei')
#                 #useimage = subtractnei(useimage,alltab,tab,curpsf)
                
#         # Fitting the PSF to the stars
#         #-----------------------------
#         newpars,perror,pcov,ptab,rchisq = fitpsf(psftype,curparams,image,error,psftab[:,2],psftab[:,3],
#                                                  psftab[:,1],fitradius,method=method,
#                                                  maxiter=maxiter,minpercdiff=minpercdiff,verbose=verbose)
#         #newpsf,pars,perror,ptab,pf = fitpsf(curpsf,useimage,psftab,fitradius=fitrad,method=method,
#         #                                    maxiter=maxiter,minpercdiff=minpercdiff,verbose=verbose)
        
#         # Add information into the output catalog
#         _,ind1,ind2 = np.intersect1d(outtab[:,0],ptab[:,0],return_indices=True)
#         #ind1,ind2 = dln.match(outtab['id'],ptab['id'])
#         outtab[:,11] = 1    # reject
#         #outtab['reject'] = 1
#         #for n in ptab.columns:
#         #    outtab[n][ind1] = ptab[n][ind2]
#         #outtab['reject'][ind1] = 0
#         # id, amp, x, y, npix, rms, chisq, ixmin, ixmax, iymin, iymax, reject
#         outtab[ind1,1:10] = ptab[ind2,1:10]
        
#         # Compare PSF parameters
#         # if type(newpsf)!=mnb.PSFEmpirical:
#         #     pardiff = newpsf.params-curpsf.params
#         # else:
#         #     pardiff = newpsf._data-curpsf._data
#         pardiff = newpars - pars
#         sumpardiff = np.sum(np.abs(pardiff))
#         curpsf = newpsf.copy()
        
#         # Stopping criteria
#         if reject is False or sumpardiff<0.05 or nrejiter>=maxrejiter or nrejstar==0: flag=1
#         if subnei is True and nrejiter==0: flag=0   # iterate at least once with neighbor subtraction
        
#         nrejiter += 1
        
#     # # Generate an empirical look-up table of corrections
#     # if lookup:
#     #     if verbose:
#     #         print('Making empirical lookup table with order='+str(lorder))

#     #     pf.mklookup(lorder)
#     #     # Fit the stars again and get new RMS values
#     #     xdata = np.arange(pf.ntotpix)
#     #     out = pf.model(xdata,*pf.psf.params)
#     #     newpsf = pf.psf.copy()
#     #     # Update information in the output catalog
#     #     ind1,ind2 = dln.match(outtab['id'],ptab['id'])
#     #     outtab['reject'] = 1
#     #     outtab['reject'][ind1] = 0
#     #     outtab['amp'][ind1] = pf.staramp[ind2]
#     #     outtab['x'][ind1] = pf.starxcen[ind2]
#     #     outtab['y'][ind1] = pf.starycen[ind2]
#     #     outtab['rms'][ind1] = pf.starrms[ind2]
#     #     outtab['chisq'][ind1] = pf.starchisq[ind2]                
#     #     if verbose:
#     #         print('Median RMS: '+str(np.median(pf.starrms)))    
            
    
#     return newpsf, pars, perror, outtab
        
        
if __name__ == "__main__":
    cc.compile()
