예제 #1
0
def createMTFatm(D, m, k, wlum, zen, r0inmRef, model="vonK"):
    """
    
    Generate the modulation transfer function (MTF) for atmosphere.
    
    Arguments:
        D {[float]} -- Side length of optical path difference (OPD) image in m.
        m {[int]} -- Dimension of OPD image in pixel. The the number of pixel we want to have 
                     to cover the length of D.
        k {[int]} -- Use a k-times bigger array to pad the MTF. Use k=1 for the same size.
        wlum {[float]} -- Wavelength in um.
        zen {[float]} -- Telescope zenith angle in degree.
        r0inmRef {[float]} -- Reference r0 in meter at the wavelength of 0.5 um.
    
    Keyword Arguments:
        model {str} -- Kolmogorov power spectrum ("Kolm") or van Karman power spectrum ("vonK"). 
                       (default: {"vonK"})
    
    Returns:
        [ndarray] -- MTF at specific atmosphere model.
    """

    # Get the atmosphere phase structure function
    sfa = atmSF(D, m, wlum, zen, r0inmRef, model)

    # Get the modular transfer function for atmosphere
    mtfa = np.exp(-0.5 * sfa)

    # Add even number
    N = int(m + np.rint((m * (k - 1) + 1e-5) / 2) * 2)

    # Pad the matrix if necessary
    mtfa = padArray(mtfa, N)

    return mtfa
예제 #2
0
def createMTFatm(D, m, k, wlum, zen, r0inmRef):
    """
    m is the number of pixel we want to have to cover the length of D.
    If we want a k-times bigger array, we pad the mtf generated using k=1.
    """

    sfa = atmSF('vonK', D, m, wlum, zen, r0inmRef)
    mtfa = np.exp(-0.5 * sfa)

    N = int(m + np.rint((m * (k - 1) + 1e-5) / 2) * 2)  # add even number
    mtfa = padArray(mtfa, N)

    return mtfa
예제 #3
0
파일: aosMetric.py 프로젝트: bxin/IM
def createMTFatm(D, m, k, wlum, zen, r0inmRef):
    """
    m is the number of pixel we want to have to cover the length of D/wl.
    If we want a k-times bigger array, we pad the mtf generated using k=1.
    """

    sfa = atmSF('vonK', D, m, wlum, zen, r0inmRef)
    mtfa = np.exp(-0.5 * sfa)

    N = np.rint(m * k + 1e-5)
    mtfa = padArray(mtfa, N)

    return mtfa
예제 #4
0
def calc_pssn(array,
              wlum,
              aType="opd",
              D=8.36,
              r0inmRef=0.1382,
              zen=0,
              pmask=0,
              imagedelta=0,
              fno=1.2335,
              debugLevel=0):
    """
    
    Calculate the normalized point source sensitivity (PSSN).
    
    Arguments:
        array {[ndarray]} -- Array that contains either opd or pdf. opd need to be in microns.
        wlum {[float]} -- Wavelength in microns.
    
    Keyword Arguments:
        aType {str} -- What is used to calculate pssn - either opd or psf. (default: {"opd"})
        D {float} -- Side length of OPD image in meter. (default: {8.36})
        r0inmRef {float} -- Fidicial atmosphere r0 @ 500nm in meter, Konstantinos uses 0.20. 
                            (default: {0.1382})
        zen {float} -- Telescope zenith angle in degree. (default: {0})
        pmask {float/ ndarray} -- Pupil mask. when opd is used, it can be generated using opd 
                                  image, we can put 0 or -1 or whatever here. When psf is used, 
                                  this needs to be provided separately with same size as array. 
                                  (default: {0})
        imagedelta {float} -- Only needed when psf is used. use 0 for opd. (default: {0})
        fno {float} -- Only needed when psf is used. use 0 for opd. (default: {1.2335})
        debugLevel {int} -- Debug level. The higher value gives more information. (default: {0})
    
    Returns:
        [float] -- PSSN value.
    """

    # Only needed for psf: pmask, imagedelta, fno

    # THE INTERNAL RESOLUTION THAT FFTS OPERATE ON IS VERY IMPORTANT
    # TO THE ACCUARCY OF PSSN.
    # WHEN TYPE='OPD', NRESO=SIZE(ARRAY,1)
    # WHEN TYPE='PSF', NRESO=SIZE(PMASK,1)
    #    for the psf option, we can not first convert psf back to opd then
    #    start over,
    #    because psf=|exp(-2*OPD)|^2. information has been lost in the | |^2.
    #    we need to go forward with psf->mtf,
    #    and take care of the coordinates properly.

    # PSSN = (n_eff)_atm / (n_eff)_atm+sys
    # (n_eff))_atm = 1 / (int (PSF^2)_atm dOmega)
    # (n_eff))_atm+sys = 1 / (int (PSF^2)_atm+sys dOmega)

    # Check the type is "OPD" or "PSF"
    if aType not in ("opd", "psf"):
        raise ValueError("The type of %s is not allowed." % aType)

    # Squeeze the array if necessary
    if (array.ndim == 3):
        array2D = array[0, :, :].squeeze()

    # Get the k value (magnification ratio used in creating MTF)
    if (aType == "opd"):
        try:
            m = max(array2D.shape)
        except NameError:
            m = max(array.shape)
        k = 1
    elif (aType == "psf"):
        m = max(pmask.shape)
        # Pupil needs to be padded k times larger to get imagedelta
        # Do not know where to find this formular. Check with Bo.
        k = fno * wlum / imagedelta

    # Get the modulation transfer function with the van Karman power spectrum
    mtfa = createMTFatm(D, m, k, wlum, zen, r0inmRef, model="vonK")

    # Get the pupil function
    if (aType == "opd"):
        try:
            iad = (array2D != 0)
        except NameError:
            iad = (array != 0)
    elif (aType == "psf"):
        # Add even number
        mk = int(m + np.rint((m * (k - 1) + 1e-5) / 2) * 2)
        # padArray(pmask, m)
        iad = pmask

    # OPD --> PSF --> OTF --> OTF' (OTF + atmosphere) --> PSF'
    # Check with Bo that we could get OTF' or PSF' from PhoSim or not directly.
    # The above question might not be a concern in the simulation.
    # However, for the real image, it loooks like this is hard to do
    # What should be the standard way to judge the PSSN in the real telescope?

    # OPD is zero for perfect telescope
    opdt = np.zeros((m, m))

    # OPD to PSF
    psft = opd2psf(opdt,
                   iad,
                   wlum,
                   imagedelta=imagedelta,
                   sensorFactor=1,
                   fno=fno,
                   debugLevel=debugLevel)

    # PSF to optical transfer function (OTF)
    otft = psf2otf(psft)

    # Add atmosphere to perfect telescope
    otfa = otft * mtfa

    # OTF to PSF
    psfa = otf2psf(otfa)

    # Atmospheric PSS (point spread sensitivity) = 1/neff_atm
    pssa = np.sum(psfa**2)

    # Calculate PSF with error (atmosphere + system)
    if (aType == "opd"):

        if (array.ndim == 2):
            ninst = 1
        else:
            ninst = array.shape[0]

        for ii in range(ninst):

            if (array.ndim == 2):
                array2D = array
            else:
                array2D = array[ii, :, :].squeeze()

            psfei = opd2psf(array2D, iad, wlum, debugLevel=debugLevel)

            if (ii == 0):
                psfe = psfei
            else:
                psfe += psfei

        # Do the normalization based on the number of instrument
        psfe = psfe / ninst

    elif (aType == "psf"):

        if (array.shape[0] == mk):
            psfe = array

        elif (array.shape[0] > mk):
            psfe = extractArray(array, mk)

        else:
            print("calc_pssn: image provided too small, %d < %d x %6.4f." %
                  (array.shape[0], m, k))
            print("IQ is over-estimated !!!")
            psfe = padArray(array, mk)

        # Do the normalization of PSF
        psfe = psfe / np.sum(psfe) * np.sum(psft)

    # OTF with system error
    otfe = psf2otf(psfe)

    # Add the atmosphere error
    # OTF with system and atmosphere errors
    otftot = otfe * mtfa

    # PSF with system and atmosphere errors
    psftot = otf2psf(otftot)

    # atmospheric + error PSS
    pss = np.sum(psftot**2)

    # normalized PSS
    pssn = pss / pssa

    if (debugLevel >= 3):
        print("pssn = %10.8e/%10.8e = %6.4f." % (pss, pssa, pssn))

    return pssn
예제 #5
0
def opd2psf(opd,
            pupil,
            wavelength,
            imagedelta=0,
            sensorFactor=1,
            fno=1.2335,
            debugLevel=0):
    """
    
    Optical path difference (OPD) to point spread function (PSF).
    
    Arguments:
        opd {[ndarray]} -- Optical path difference.
        pupil {[ndarray/ float/ int]} -- Pupil function. If pupil is a number, not an array, we will 
                                         get pupil geometry from OPD.
        wavelength {[float]} -- Wavelength in um.
    
    Keyword Arguments:
        imagedelta {float} -- Pixel size in um. Use 0 if pixel size is not specified. (default: {0})
        sensorFactor {float} -- Factor of sensor (check with Bo for this). Only need this if 
                                imagedelta != 0. (default: {1})
        fno {float} -- ? Check with Bo. Only need this if imagedelta=0. (default: {1.2335})
        debugLevel {int} -- Debug level. The higher value gives more information. (default: {0})
    
    Returns:
        [ndarray] -- Normalized PSF.
    
    Raises:
        ValueError -- Shapes of OPD and pupil are different.
        ValueError -- OPD shape is not square.
        ValueError -- Padding value is less than 1.
    """

    # Make sure all NaN in OPD to be 0
    opd[np.isnan(opd)] = 0

    # Get the pupil function from OPD if necessary
    if (not isinstance(pupil, np.ndarray)):
        pupil = (opd != 0)

    # Check the dimension of pupil and OPD should be the same
    if (opd.shape != pupil.shape):
        raise ValueError("Shapes of OPD and pupil are different.")

    # For the PSF
    if (imagedelta != 0):

        # Check the dimension of OPD
        if (opd.shape[0] != opd.shape[1]):
            raise ValueError("Error (opd2psf): OPD image size = (%d, %d)." %
                             (opd.shape[0], opd.shape[1]))

        # Get the k value and the padding
        k = fno * wavelength / imagedelta
        padding = k / sensorFactor

        # Check the padding
        if (padding < 1):

            errorMes = "opd2psf: Sampling too low, data inaccurate.\n"
            errorMes += "Imagedelta needs to be smaller than fno * wlum = %4.2f um.\n" % (
                fno * wavelength)
            errorMes += "So that the padding factor > 1.\n"
            errorMes += "Otherwise we have to cut pupil to be < D."

            raise ValueError(errorMes)

        # Size of sensor
        sensorSamples = opd.shape[0]

        # Add even number for padding
        N = int(sensorSamples +
                np.rint(((padding - 1) * sensorSamples + 1e-5) / 2) * 2)
        pupil = padArray(pupil, N)
        opd = padArray(opd, N)

        # Show the padding information or not
        if (debugLevel >= 3):
            print("padding = %8.6f." % padding)

    # If imagedelta = 0, we don't do any padding, and go with below
    z = pupil * np.exp(-2j * np.pi * opd / wavelength)
    z = np.fft.fftshift(np.fft.fft2(np.fft.fftshift(z), s=z.shape))
    z = np.absolute(z**2)

    # Normalize the PSF
    z = z / np.sum(z)

    # Show the information of PSF from OPD
    if (debugLevel >= 3):
        print("opd2psf(): imagedelta = %8.6f." % imagedelta, end="")

        if (imagedelta == 0):
            print("0 means using OPD with padding as provided.")

        print("Verify psf has been normalized: %4.1f." % np.sum(z))

    return z
예제 #6
0
def psf2FWHMring(array,
                 wlum,
                 type='opd',
                 D=8.36,
                 r0inmRef=0.1382,
                 zen=0,
                 pmask=0,
                 imagedelta=0,
                 fno=1.2335,
                 fwhm_thresh=0.01,
                 power=2,
                 debugLevel=0):
    '''
    wavefront OPD in micron
'''
    wl = wlum * 1.e-6
    if array.ndim == 3:
        array2D = array[0, :, :].squeeze()

    if type == 'opd':
        try:
            m = max(array2D.shape)
        except NameError:
            m = max(array.shape)
        k = 1
        imagedelta = fno * wlum
    else:
        m = max(pmask.shape)
        k = fno * wlum / imagedelta
        m = int(np.round(m * k))
        D = D * k

    mtfa = createMTFatm(D, m, k, wlum, zen, r0inmRef)

    if type == 'opd':
        try:
            iad = (array2D != 0)
        except NameError:
            iad = (array != 0)
    elif type == 'psf':
        mk = int(m + np.rint((m * (k - 1) + 1e-5) / 2) * 2)  # add even number
        iad = pmask  # padArray(pmask, m)

    # coordinates of the PSF in mas
    conv = 206265000.  #=3600*180/pi*1000; const. for converting radian to mas
    da = conv * wl / D  #in arcsec; if type==psf, D includes the padding, so this is still valid
    ha = da * (m - 1) / 2
    ha1d = np.linspace(-ha, ha, m)
    xxr, yyr = np.meshgrid(ha1d, ha1d)

    # Perfect telescope
    opdt = np.zeros((m, m))
    psft = opd2psf(opdt, iad, wlum, imagedelta, 1, fno, debugLevel)
    otft = psf2otf(psft)  # OTF of perfect telescope
    otfa = otft * mtfa  # add atmosphere to perfect telescope
    psfa = otf2psf(otfa)

    dm = np.max(psfa)
    idxmax = (psfa == dm)
    idx = np.abs(psfa - 0.5 * dm) < fwhm_thresh * dm
    r = np.sqrt((xxr[idx] - xxr[idxmax])**2 + (yyr[idx] - yyr[idxmax])**2)
    fwhmatm = 2 * np.mean(r)

    # Error;
    if type == 'opd':
        if array.ndim == 2:
            ninst = 1
        else:
            ninst = array.shape[0]
        for i in range(ninst):
            if array.ndim == 2:
                array2D = array
            else:
                array2D = array[i, :, :].squeeze()
            psfei = opd2psf(array2D, iad, wlum, 0, 0, 0, debugLevel)
            if i == 0:
                psfe = psfei
            else:
                psfe += psfei
        psfe = psfe / ninst
    else:
        if array.shape[0] == mk:
            psfe = array
        elif array.shape[0] > mk:
            psfe = extractArray(array, mk)
        else:
            print('calc_pssn: image provided too small, %d < %d x %6.4f' %
                  (array.shape[0], m, k))
            print('IQ is over-estimated !!!')
            psfe = padArray(array, mk)

        psfe = psfe / np.sum(psfe) * np.sum(psft)

    otfe = psf2otf(psfe)  # OTF of error
    otftot = otfe * mtfa  # add atmosphere to error
    psftot = otf2psf(otftot)

    dm = np.max(psftot)
    idxmax = (psftot == dm)
    idx = np.abs(psftot - 0.5 * dm) < fwhm_thresh * dm
    r = np.sqrt((xxr[idx] - xxr[idxmax])**2 + (yyr[idx] - yyr[idxmax])**2)
    fwhmtot = 2 * np.mean(r)

    fwhm_mas = np.max(
        (0,
         (fwhmtot**power - fwhmatm**power)**(1 / power)))  #cannot be negative

    return fwhm_mas
예제 #7
0
def opd2psf(opd, pupil, wavelength, imagedelta, sensorFactor, fno, debugLevel):
    """
    wavefront OPD in micron
    imagedelta in micron, use 0 if pixel size is not specified
    wavelength in micron

    if pupil is a number, not an array, we will get pupil geometry from opd
    The following are not needed if imagedelta=0,
    sensorFactor, fno
    """

    opd[np.isnan(opd)] = 0
    try:
        if (pupil.shape == opd.shape):
            pass
        else:
            raise AttributeError
    except AttributeError:
        pupil = (opd != 0)

    if imagedelta != 0:
        try:
            if opd.shape[0] != opd.shape[1]:
                raise (nonSquareImageError)
        except nonSquareImageError:
            print('Error (opd2psf): Only square images are accepted.')
            print('image size = (%d, %d)' % (opd.shape[0], opd.shape[1]))
            sys.exit()

        k = fno * wavelength / imagedelta
        padding = k / sensorFactor
        try:
            if padding < 1:
                raise (psfSamplingTooLowError)
        except psfSamplingTooLowError:
            print('opd2psf: sampling too low, data inaccurate')
            print('imagedelta needs to be smaller than fno*wlum=%4.2f um' %
                  (fno * wavelength))
            print('         so that the padding factor > 1')
            print('         otherwise we have to cut pupil to be < D')
            sys.exit()

        sensorSamples = opd.shape[0]
        # add even number for padding
        N = int(sensorSamples + \
            np.rint(((padding - 1) * sensorSamples + 1e-5) / 2) * 2)
        pupil = padArray(pupil, N)
        opd = padArray(opd, N)
        if debugLevel >= 3:
            print('padding=%8.6f' % padding)
    # if imagedelta = 0, we don't do any padding, and go with below
    z = pupil * np.exp(-2j * np.pi * opd / wavelength)
    z = np.fft.fftshift(np.fft.fft2(np.fft.fftshift(z),
                                    s=z.shape))  # /sqrt(miad2/m^2)
    z = np.absolute(z**2)
    z = z / np.sum(z)

    if debugLevel >= 3:
        print('opd2psf(): imagedelta=%8.6f' % imagedelta, end='')
        if imagedelta == 0:
            print('0 means using OPD with padding as provided')
        else:
            print('')
        print('verify psf has been normalized: %4.1f' % np.sum(z))

    return z
예제 #8
0
def calc_pssn(array,
              wlum,
              type='opd',
              D=8.36,
              r0inmRef=0.1382,
              zen=0,
              pmask=0,
              imagedelta=0,
              fno=1.2335,
              debugLevel=0):
    """
    array: the array that contains either opd or pdf
           opd need to be in microns
    wlum: wavelength in microns
    type: what is used to calculate pssn - either opd or psf
    psf doesn't matter, will be normalized anyway
    D: side length of OPD image in meter
    r0inmRef: fidicial atmosphere r0@500nm in meter, Konstantinos uses 0.20
    Now that we use vonK atmosphere, r0in=0.1382 -> fwhm=0.6"
    earlier, we used Kolm atmosphere, r0in=0.1679 -> fwhm=0.6"
    zen: telescope zenith angle

    The following are only needed when the input array is psf -
    pmask: pupil mask. when opd is used, it can be generated using opd image,
    we can put 0 or -1 or whatever here.
    when psf is used, this needs to be provided separately with same
    size as array.
    imagedelta and fno are only needed when psf is used. use 0,0 for opd

    THE INTERNAL RESOLUTION THAT FFTS OPERATE ON IS VERY IMPORTANT
    TO THE ACCUARCY OF PSSN.
    WHEN TYPE='OPD', NRESO=SIZE(ARRAY,1)
    WHEN TYPE='PSF', NRESO=SIZE(PMASK,1)
       for the psf option, we can not first convert psf back to opd then
       start over,
       because psf=|exp(-2*OPD)|^2. information has been lost in the | |^2.
       we need to go forward with psf->mtf,
       and take care of the coordinates properly.
    """

    if array.ndim == 3:
        array2D = array[0, :, :].squeeze()

    if type == 'opd':
        try:
            m = max(array2D.shape)
        except NameError:
            m = max(array.shape)
        k = 1
        imagedelta = fno * wlum
    else:
        m = max(pmask.shape)
        # pupil needs to be padded k times larger to get imagedelta
        k = fno * wlum / imagedelta

    mtfa = createMTFatm(D, m, k, wlum, zen, r0inmRef)

    if type == 'opd':
        try:
            iad = (array2D != 0)
        except NameError:
            iad = (array != 0)
    elif type == 'psf':
        mk = int(m + np.rint((m * (k - 1) + 1e-5) / 2) * 2)  # add even number
        iad = pmask  # padArray(pmask, m)

    # number of non-zero elements, used for normalization later
    # miad2 = np.count_nonzero(iad)

    # Perfect telescope
    opdt = np.zeros((m, m))
    psft = opd2psf(opdt, iad, wlum, imagedelta, 1, fno, debugLevel)
    otft = psf2otf(psft)  # OTF of perfect telescope
    otfa = otft * mtfa  # add atmosphere to perfect telescope
    psfa = otf2psf(otfa)
    pssa = np.sum(psfa**2)  # atmospheric PSS = 1/neff_atm

    # Error;
    if type == 'opd':
        if array.ndim == 2:
            ninst = 1
        else:
            ninst = array.shape[0]
        for i in range(ninst):
            if array.ndim == 2:
                array2D = array
            else:
                array2D = array[i, :, :].squeeze()
            psfei = opd2psf(array2D, iad, wlum, 0, 0, 0, debugLevel)
            if i == 0:
                psfe = psfei
            else:
                psfe += psfei
        psfe = psfe / ninst
    else:
        if array.shape[0] == mk:
            psfe = array
        elif array.shape[0] > mk:
            psfe = extractArray(array, mk)
        else:
            print('calc_pssn: image provided too small, %d < %d x %6.4f' %
                  (array.shape[0], m, k))
            print('IQ is over-estimated !!!')
            psfe = padArray(array, mk)

        psfe = psfe / np.sum(psfe) * np.sum(psft)

    pixmas = imagedelta * 20
    aa = psfe / np.sum(psfe)
    neff = 1 / np.sum(aa**2)
    fwhmeff = 0.664 * pixmas * np.sqrt(neff)

    otfe = psf2otf(psfe)  # OTF of error
    otftot = otfe * mtfa  # add atmosphere to error
    psftot = otf2psf(otftot)
    pss = np.sum(psftot**2)  # atmospheric + error PSS

    pssn = pss / pssa  # normalized PSS
    if debugLevel >= 3:
        print('pssn = %10.8e/%10.8e = %6.4f' % (pss, pssa, pssn))

    return pssn, fwhmeff
예제 #9
0
파일: aosMetric.py 프로젝트: bxin/IM
def opd2psf(opd, pupil, wavelength, imagedelta, sensorFactor, fno, debugLevel):
    """
    wavefront OPD in micron
    imagedelta in micron, use 0 if pixel size is not specified
    wavelength in micron

    if pupil is a number, not an array, we will get pupil geometry from opd
    The following are not needed if imagedelta=0,
    sensorFactor, fno
    """

    opd[np.isnan(opd)] = 0
    try:
        if (pupil.shape == opd.shape):
            pass
        else:
            raise AttributeError
    except AttributeError:
        pupil = (opd != 0)

    if imagedelta != 0:
        try:
            if opd.shape[0] != opd.shape[1]:
                raise(nonSquareImageError)
        except nonSquareImageError:
            print('Error (opd2psf): Only square images are accepted.')
            print('image size = (%d, %d)' % (
                opd.shape[0], opd.shape[1]))
            sys.exit()

        k = fno * wavelength / imagedelta
        padding = k / sensorFactor
        try:
            if padding < 1:
                raise(psfSamplingTooLowError)
        except psfSamplingTooLowError:
            print('opd2psf: sampling too low, data inaccurate')
            print('imagedelta needs to be smaller than fno*wlum=%4.2f um' % (
                fno * wavelength))
            print('         so that the padding factor > 1')
            print('         otherwise we have to cut pupil to be < D')
            sys.exit()

        sensorSamples = opd.shape[0]
        N = np.rint(padding * sensorSamples)
        pupil = padArray(pupil, N)
        opd = padArray(opd, N)
        if debugLevel >= 3:
            print('padding=%8.6f' % padding)

    z = pupil * np.exp(-2j * np.pi * opd / wavelength)
    z = np.fft.fftshift(np.fft.fft2(np.fft.fftshift(z),
                                    s=z.shape))  # /sqrt(miad2/m^2)
    z = np.absolute(z**2)
    z = z / np.sum(z)

    if debugLevel >= 3:
        print('opd2psf(): imagedelta=%8.6f' % imagedelta)
        print('verify psf has been normalized: %4.1f' % np.sum(z))

    return z
예제 #10
0
파일: aosMetric.py 프로젝트: bxin/IM
def calc_pssn(array, wlum, type='opd', D=8.36, r0inmRef=0.1382, zen=0,
              pmask=0, imagedelta=0.2, fno=1.2335, debugLevel=0):
    """
    array: the array that contains eitehr opd or pdf
    opd need to be in microns
    wlum: wavelength in microns
    type: what is used to calculate pssn - either opd or psf
    psf doesn't matter, will be normalized anyway
    D: side length of OPD image in meter
    r0inmRef: fidicial atmosphere r0@500nm in meter, Konstantinos uses 0.20
    Now that we use vonK atmosphere, r0in=0.1382 -> fwhm=0.6"
    earlier, we used Kolm atmosphere, r0in=0.1679 -> fwhm=0.6"
    zen: telescope zenith angle

    The following are only needed when the input array is psf -
    pmask: pupil mask. when opd is used, it can be generated using opd image,
    we can put 0 or -1 or whatever here.
    when psf is used, this needs to be provided separately with same
    size as array
    imagedelta and fno are only needed when psf is used. use 0,0 for opd

    THE INTERNAL RESOLUTION THAT FFTS OPERATE ON IS VERY IMPORTANT
    TO THE ACCUARCY OF PSSN.
    WHEN TYPE='OPD', NRESO=SIZE(ARRAY,1)
    WHEN TYPE='PSF', NRESO=SIZE(PMASK,1)
       for the psf option, we can not first convert psf back to opd then
       start over,
       because psf=|exp(-2*OPD)|^2. information has been lost in the | |^2.
       we need to go forward with psf->mtf,
       and take care of the coordinates properly.
    """

    if array.ndim == 3:
        array2D = array[0, :, :].squeeze()

    if type == 'opd':
        try:
            m = max(array2D.shape)
        except NameError:
            m = max(array.shape)
        k = 1
    else:
        m = max(pmask.shape)
        # pupil needs to be padded k times larger to get imagedelta
        k = fno * wlum / imagedelta

    mtfa = createMTFatm(D, m, k, wlum, zen, r0inmRef)

    if type == 'opd':
        try:
            iad = (array2D != 0)
        except NameError:
            iad = (array != 0)
    elif type == 'psf':
        iad = padArray(pmask, m)

    # number of non-zero elements, used for normalization later
    # miad2 = np.count_nonzero(iad)

    # Perfect telescope
    opdt = np.zeros((m, m))
    psft = opd2psf(opdt, iad, wlum, 0, 0, 0, debugLevel)
    otft = psf2otf(psft)  # OTF of perfect telescope
    otfa = otft * mtfa  # add atmosphere to perfect telescope
    psfa = otf2psf(otfa)
    pssa = np.sum(psfa**2)  # atmospheric PSS = 1/neff_atm

    # Error;
    if type == 'opd':
        if array.ndim == 2:
            ninst = 1
        else:
            ninst = array.shape[0]
        for i in range(ninst):
            if array.ndim == 2:
                array2D = array
            else:
                array2D = array[i, :, :].squeeze()
            psfei = opd2psf(array2D, iad, wlum, 0, 0, 0, debugLevel)
            if i == 0:
                psfe = psfei
            else:
                psfe += psfei
        psfe = psfe / ninst
    else:
        psfe = padArray(array, m)
        psfe = psfe / np.sum(psfe) * np.sum(psft)

    otfe = psf2otf(psfe)  # OTF of error
    otftot = otfe * mtfa  # add atmosphere to error
    psftot = otf2psf(otftot)
    pss = np.sum(psftot**2)  # atmospheric + error PSS

    pssn = pss / pssa  # normalized PSS

    return pssn