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
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
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
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
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
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
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
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
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
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